Initial commit: LAAPC Print Service - Windows service with multi-tray printing, IPC queue management, PDF testing support, and self-installing CLI

This commit is contained in:
Jason
2026-05-14 16:59:59 -05:00
commit cce9248089
23 changed files with 2974 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Use x86 only for Release builds (for 32-bit target machines) -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Pipes" Version="4.3.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="7.0.0" />
</ItemGroup>
</Project>
+232
View File
@@ -0,0 +1,232 @@
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
namespace PrintServiceCLI;
class Program
{
private const string PipeName = "PrintServicePipe";
private const int TimeoutMs = 5000;
static async Task<int> Main(string[] args)
{
try
{
// Handle special --install-service flag (called by elevated process)
if (args.Contains("--install-service"))
{
return ServiceManager.EnsureServiceRunning(silent: false) ? 0 : 1;
}
// Check for skip-service-check flag
bool skipServiceCheck = args.Contains("--skip-service-check");
// Parse arguments
var command = ParseArguments(args, out bool showHelp);
if (showHelp || command == null)
{
ShowUsage();
return command == null ? 1 : 0;
}
// Ensure service is running (unless testing)
if (!skipServiceCheck)
{
if (!ServiceManager.EnsureServiceRunning())
{
Console.Error.WriteLine("ERROR: Service is not available. Cannot process print request.");
return 1;
}
}
var result = await SendCommandToService(command);
if (result.Success)
{
Console.WriteLine($"SUCCESS: {result.Message}");
return 0;
}
else
{
Console.Error.WriteLine($"ERROR: {result.Message}");
return 1;
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"FATAL ERROR: {ex.Message}");
return 1;
}
}
/// <summary>
/// Parse command line arguments
/// </summary>
static PrintCommand? ParseArguments(string[] args, out bool showHelp)
{
showHelp = false;
string? filePath = null;
string? documentType = null;
string? orderNumber = null;
for (int i = 0; i < args.Length; i++)
{
switch (args[i].ToLower())
{
case "-f":
case "--file":
if (i + 1 < args.Length)
{
filePath = args[++i];
}
break;
case "-t":
case "--type":
if (i + 1 < args.Length)
{
documentType = args[++i];
}
break;
case "-o":
case "--order":
if (i + 1 < args.Length)
{
orderNumber = args[++i];
}
break;
case "-h":
case "--help":
case "?":
showHelp = true;
return null;
case "--skip-service-check":
case "--install-service":
// These flags are handled in Main, skip here
break;
}
}
// Validate required arguments
if (string.IsNullOrEmpty(filePath))
{
Console.Error.WriteLine("Error: File path is required (-f)");
return null;
}
if (string.IsNullOrEmpty(documentType))
{
Console.Error.WriteLine("Error: Document type is required (-t)");
return null;
}
if (!File.Exists(filePath))
{
Console.Error.WriteLine($"Error: File not found: {filePath}");
return null;
}
return new PrintCommand
{
FilePath = Path.GetFullPath(filePath),
DocumentType = documentType,
OrderNumber = orderNumber
};
}
/// <summary>
/// Send command to service via named pipe
/// </summary>
static async Task<PrintResponse> SendCommandToService(PrintCommand command)
{
using var pipeClient = new NamedPipeClientStream(
".",
PipeName,
PipeDirection.InOut,
PipeOptions.Asynchronous);
try
{
await pipeClient.ConnectAsync(TimeoutMs);
}
catch (TimeoutException)
{
return new PrintResponse
{
Success = false,
Message = "Service not responding. Is the LAAPC Print Service running?"
};
}
catch (IOException ex)
{
return new PrintResponse
{
Success = false,
Message = $"Cannot connect to service: {ex.Message}"
};
}
// Send command
var json = JsonSerializer.Serialize(command);
var bytes = Encoding.UTF8.GetBytes(json);
await pipeClient.WriteAsync(bytes, 0, bytes.Length);
await pipeClient.FlushAsync();
// Read response
var buffer = new byte[4096];
var bytesRead = await pipeClient.ReadAsync(buffer, 0, buffer.Length);
var responseJson = Encoding.UTF8.GetString(buffer, 0, bytesRead);
var response = JsonSerializer.Deserialize<PrintResponse>(responseJson);
return response ?? new PrintResponse { Success = false, Message = "Invalid response from service" };
}
/// <summary>
/// Show usage information
/// </summary>
static void ShowUsage()
{
Console.WriteLine("LAAPC Print Service CLI");
Console.WriteLine();
Console.WriteLine("Usage: PrintServiceCLI -f <filepath> -t <doctype> [-o <ordernumber>]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" -f, --file <path> Path to the capture file to print (required)");
Console.WriteLine(" -t, --type <type> Document type (required)");
Console.WriteLine(" Examples: invoice, order, delivery");
Console.WriteLine(" -o, --order <number> Optional order number");
Console.WriteLine(" -h, --help Show this help message");
Console.WriteLine(" --skip-service-check Skip service availability check (for testing)");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" PrintServiceCLI -f \"C:\\Captures\\file.txt\" -t invoice");
Console.WriteLine(" PrintServiceCLI -f \"C:\\Captures\\file.txt\" -t order -o 12345");
Console.WriteLine();
Console.WriteLine("For Development/Testing:");
Console.WriteLine(" PrintServiceCLI -f \"file.txt\" -t test --skip-service-check");
}
}
/// <summary>
/// Command to send to service
/// </summary>
class PrintCommand
{
public string FilePath { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string? OrderNumber { get; set; }
}
/// <summary>
/// Response from service
/// </summary>
class PrintResponse
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
+290
View File
@@ -0,0 +1,290 @@
using System.Diagnostics;
using System.ServiceProcess;
using System.Security.Principal;
namespace PrintServiceCLI;
/// <summary>
/// Manages Windows Service installation and verification
/// </summary>
public static class ServiceManager
{
private const string ServiceName = "LAAPC Print Service";
private const string ServiceInstallPath = @"C:\ProgramData\LAAPC";
private const string ServiceExeName = "PrintService.exe";
private static bool? _serviceAvailable = null; // Cache result
/// <summary>
/// Check if service is installed and running, with auto-install option
/// </summary>
public static bool EnsureServiceRunning(bool silent = false)
{
// Return cached result if already checked
if (_serviceAvailable.HasValue)
return _serviceAvailable.Value;
try
{
// Check if service exists and is running
using var service = new ServiceController(ServiceName);
service.Refresh();
if (service.Status == ServiceControllerStatus.Running)
{
_serviceAvailable = true;
return true;
}
// Service exists but not running - try to start it
if (service.Status == ServiceControllerStatus.Stopped)
{
if (!silent)
Console.WriteLine("Service is stopped. Starting...");
service.Start();
service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10));
_serviceAvailable = true;
return true;
}
_serviceAvailable = false;
return false;
}
catch (InvalidOperationException)
{
// Service doesn't exist - offer to install
if (silent)
{
_serviceAvailable = false;
return false;
}
return PromptAndInstall();
}
catch (Exception ex)
{
if (!silent)
Console.Error.WriteLine($"Error checking service: {ex.Message}");
_serviceAvailable = false;
return false;
}
}
/// <summary>
/// Prompt user and install service if they agree
/// </summary>
private static bool PromptAndInstall()
{
Console.WriteLine();
Console.WriteLine("====================================================");
Console.WriteLine("LAAPC Print Service is not installed on this PC.");
Console.WriteLine("====================================================");
Console.WriteLine();
Console.WriteLine("The service must be installed locally to process print jobs.");
Console.WriteLine("This is a one-time setup that requires administrator access.");
Console.WriteLine();
Console.Write("Install the service now? [Y/N]: ");
var response = Console.ReadLine()?.Trim().ToUpper();
if (response != "Y" && response != "YES")
{
Console.WriteLine("Installation cancelled. Service is required for printing.");
_serviceAvailable = false;
return false;
}
return InstallService();
}
/// <summary>
/// Install and start the service with UAC elevation
/// </summary>
private static bool InstallService()
{
try
{
Console.WriteLine();
Console.WriteLine("Installing service...");
// Check if running as admin
if (!IsAdministrator())
{
Console.WriteLine("Requesting administrator privileges...");
return InstallWithElevation();
}
// Copy service files to local machine
var cliPath = AppContext.BaseDirectory;
var serviceSourcePath = Path.Combine(Path.GetDirectoryName(cliPath)!, ServiceExeName);
if (!File.Exists(serviceSourcePath))
{
// Try relative path
serviceSourcePath = Path.Combine(cliPath, "..", "PrintService", ServiceExeName);
serviceSourcePath = Path.GetFullPath(serviceSourcePath);
}
if (!File.Exists(serviceSourcePath))
{
Console.Error.WriteLine($"ERROR: Could not find {ServiceExeName}");
Console.Error.WriteLine($"Expected location: {serviceSourcePath}");
_serviceAvailable = false;
return false;
}
// Create installation directory
Directory.CreateDirectory(ServiceInstallPath);
// Copy service executable and dependencies
var serviceDestPath = Path.Combine(ServiceInstallPath, ServiceExeName);
CopyServiceFiles(Path.GetDirectoryName(serviceSourcePath)!, ServiceInstallPath);
Console.WriteLine($"Files copied to: {ServiceInstallPath}");
// Install Windows Service using sc.exe
var scInstall = Process.Start(new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = $"create \"{ServiceName}\" binPath=\"{serviceDestPath}\" start=auto",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
scInstall?.WaitForExit();
if (scInstall?.ExitCode != 0)
{
Console.Error.WriteLine("ERROR: Failed to install service");
_serviceAvailable = false;
return false;
}
Console.WriteLine("Service installed successfully.");
// Start the service
Console.WriteLine("Starting service...");
var scStart = Process.Start(new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = $"start \"{ServiceName}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
scStart?.WaitForExit();
if (scStart?.ExitCode != 0)
{
Console.Error.WriteLine("WARNING: Service installed but failed to start");
Console.Error.WriteLine("Try running: sc start \"LAAPC Print Service\"");
_serviceAvailable = false;
return false;
}
// Wait for service to be ready
Thread.Sleep(2000);
Console.WriteLine("Service started successfully!");
Console.WriteLine();
_serviceAvailable = true;
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"ERROR: Installation failed: {ex.Message}");
_serviceAvailable = false;
return false;
}
}
/// <summary>
/// Restart process with admin elevation
/// </summary>
private static bool InstallWithElevation()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = Process.GetCurrentProcess().MainModule?.FileName ?? "PrintServiceCLI.exe",
Arguments = "--install-service",
UseShellExecute = true,
Verb = "runas" // Trigger UAC prompt
};
var process = Process.Start(startInfo);
process?.WaitForExit();
if (process?.ExitCode == 0)
{
Console.WriteLine("Service installed successfully.");
_serviceAvailable = true;
return true;
}
Console.Error.WriteLine("Installation was cancelled or failed.");
_serviceAvailable = false;
return false;
}
catch (Exception ex)
{
Console.Error.WriteLine($"ERROR: Could not elevate privileges: {ex.Message}");
_serviceAvailable = false;
return false;
}
}
/// <summary>
/// Copy service files from source to destination
/// </summary>
private static void CopyServiceFiles(string sourceDir, string destDir)
{
// Copy main executable
var sourceExe = Path.Combine(sourceDir, ServiceExeName);
var destExe = Path.Combine(destDir, ServiceExeName);
File.Copy(sourceExe, destExe, overwrite: true);
// Copy all DLLs and config files
foreach (var file in Directory.GetFiles(sourceDir))
{
var fileName = Path.GetFileName(file);
var ext = Path.GetExtension(file).ToLower();
if (ext == ".dll" || ext == ".json" || ext == ".config")
{
var destFile = Path.Combine(destDir, fileName);
File.Copy(file, destFile, overwrite: true);
}
}
// Create necessary folders
var capturesPath = Path.Combine(destDir, "Captures");
var queuePath = Path.Combine(destDir, "Queue");
var archivePath = Path.Combine(destDir, "Archive");
var errorPath = Path.Combine(destDir, "Error");
var outputPath = Path.Combine(destDir, "Output");
Directory.CreateDirectory(capturesPath);
Directory.CreateDirectory(queuePath);
Directory.CreateDirectory(archivePath);
Directory.CreateDirectory(errorPath);
Directory.CreateDirectory(outputPath);
}
/// <summary>
/// Check if running with administrator privileges
/// </summary>
private static bool IsAdministrator()
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
}