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:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user