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,241 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PrintService.Models;
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PrintService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Named pipe server for IPC communication with CLI
|
||||
/// </summary>
|
||||
public class IpcService : IDisposable
|
||||
{
|
||||
private readonly AppSettings _settings;
|
||||
private readonly PrintQueueService _queueService;
|
||||
private readonly FileMonitorService _fileMonitorService;
|
||||
private readonly ILogger<IpcService> _logger;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task? _listenerTask;
|
||||
|
||||
public IpcService(
|
||||
IOptions<AppSettings> settings,
|
||||
PrintQueueService queueService,
|
||||
FileMonitorService fileMonitorService,
|
||||
ILogger<IpcService> logger)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_queueService = queueService;
|
||||
_fileMonitorService = fileMonitorService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start listening for IPC commands
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_listenerTask = Task.Run(() => ListenForConnections(_cancellationTokenSource.Token));
|
||||
_logger.LogInformation("IPC service started on pipe: {PipeName}", _settings.PipeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop listening
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_listenerTask?.Wait(TimeSpan.FromSeconds(5));
|
||||
_logger.LogInformation("IPC service stopped");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Listen for incoming connections
|
||||
/// </summary>
|
||||
private async Task ListenForConnections(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
NamedPipeServerStream? pipeServer = null;
|
||||
try
|
||||
{
|
||||
pipeServer = new NamedPipeServerStream(
|
||||
_settings.PipeName,
|
||||
PipeDirection.InOut,
|
||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||
PipeTransmissionMode.Message,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
await pipeServer.WaitForConnectionAsync(cancellationToken);
|
||||
|
||||
// Handle client in separate task, passing ownership of pipeServer
|
||||
var serverToHandle = pipeServer;
|
||||
pipeServer = null; // Clear reference so we don't dispose it in catch
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using (serverToHandle)
|
||||
{
|
||||
await HandleClient(serverToHandle);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
pipeServer?.Dispose();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in IPC listener");
|
||||
pipeServer?.Dispose();
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a client connection
|
||||
/// </summary>
|
||||
private async Task HandleClient(NamedPipeServerStream pipeServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!pipeServer.IsConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Read command
|
||||
var buffer = new byte[4096];
|
||||
var bytesRead = await pipeServer.ReadAsync(buffer, 0, buffer.Length);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
return; // Client disconnected
|
||||
}
|
||||
|
||||
var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||
|
||||
if (_settings.VerboseLogging)
|
||||
{
|
||||
_logger.LogDebug("Received IPC message: {Message}", message);
|
||||
}
|
||||
|
||||
var command = JsonSerializer.Deserialize<PrintCommand>(message);
|
||||
if (command == null)
|
||||
{
|
||||
await SendResponse(pipeServer, false, "Invalid command format");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process command
|
||||
var result = ProcessCommand(command);
|
||||
await SendResponse(pipeServer, result.Success, result.Message);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Client disconnected, this is normal
|
||||
if (_settings.VerboseLogging)
|
||||
{
|
||||
_logger.LogDebug("Client disconnected");
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
// Pipe communication error
|
||||
if (_settings.VerboseLogging)
|
||||
{
|
||||
_logger.LogDebug(ex, "Pipe communication error");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error handling IPC client");
|
||||
try
|
||||
{
|
||||
if (pipeServer.IsConnected)
|
||||
{
|
||||
await SendResponse(pipeServer, false, $"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a print command
|
||||
/// </summary>
|
||||
private (bool Success, string Message) ProcessCommand(PrintCommand command)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(command.FilePath) || !File.Exists(command.FilePath))
|
||||
{
|
||||
return (false, $"File not found: {command.FilePath}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(command.DocumentType))
|
||||
{
|
||||
return (false, "Document type is required");
|
||||
}
|
||||
|
||||
// Create job and enqueue
|
||||
var job = _fileMonitorService.CreateJobFromFile(
|
||||
command.FilePath,
|
||||
command.DocumentType,
|
||||
command.OrderNumber);
|
||||
|
||||
_queueService.Enqueue(job);
|
||||
|
||||
return (true, $"Job {job.Id} queued successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to process command");
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send response back to client
|
||||
/// </summary>
|
||||
private async Task SendResponse(NamedPipeServerStream pipeServer, bool success, string message)
|
||||
{
|
||||
var response = new PrintResponse
|
||||
{
|
||||
Success = success,
|
||||
Message = message
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(response);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
await pipeServer.WriteAsync(bytes, 0, bytes.Length);
|
||||
await pipeServer.FlushAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Command sent from CLI to service
|
||||
/// </summary>
|
||||
public 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 to CLI
|
||||
/// </summary>
|
||||
public class PrintResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
Reference in New Issue
Block a user