Files
2026-05-29 10:46:43 -05:00

341 lines
10 KiB
C#

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);
}
// Try to determine command type
var commandType = DetermineCommandType(message);
if (commandType == "GetQueueStatus")
{
var status = GetQueueStatus();
await SendQueueStatusResponse(pipeServer, status);
return;
}
// Handle print command
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>
/// Determine the type of command from JSON message
/// </summary>
private string? DetermineCommandType(string message)
{
try
{
using var doc = JsonDocument.Parse(message);
if (doc.RootElement.TryGetProperty("Command", out var cmdProp))
{
return cmdProp.GetString();
}
}
catch { }
return null;
}
/// <summary>
/// Get current queue status
/// </summary>
private QueueStatusResponse GetQueueStatus()
{
try
{
var jobs = _queueService.GetAllJobs();
var jobStatuses = jobs.Select(j => new JobStatus
{
Id = j.Id.ToString(),
FileName = j.OriginalFilename,
DocumentType = j.DocumentType,
Status = j.Status.ToString(),
PrinterName = null, // Will be set when processing starts
CurrentPage = null,
TotalPages = null
}).ToList();
return new QueueStatusResponse
{
Success = true,
Jobs = jobStatuses
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get queue status");
return new QueueStatusResponse
{
Success = false,
Message = $"Error: {ex.Message}"
};
}
}
/// <summary>
/// Send queue status response
/// </summary>
private async Task SendQueueStatusResponse(NamedPipeServerStream pipeServer, QueueStatusResponse response)
{
var json = JsonSerializer.Serialize(response);
var bytes = Encoding.UTF8.GetBytes(json);
await pipeServer.WriteAsync(bytes, 0, bytes.Length);
await pipeServer.FlushAsync();
}
/// <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;
}
/// <summary>
/// Queue status response for tray app
/// </summary>
public class QueueStatusResponse
{
public bool Success { get; set; }
public string? Message { get; set; }
public List<JobStatus> Jobs { get; set; } = new();
}
/// <summary>
/// Status of a single job
/// </summary>
public class JobStatus
{
public string Id { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string? PrinterName { get; set; }
public int? CurrentPage { get; set; }
public int? TotalPages { get; set; }
}