341 lines
10 KiB
C#
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; }
|
|
}
|