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,57 @@
|
||||
namespace PrintService.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Application settings loaded from appsettings.json
|
||||
/// </summary>
|
||||
public class AppSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to monitor for incoming capture files
|
||||
/// </summary>
|
||||
public string CapturesPath { get; set; } = "Captures";
|
||||
|
||||
/// <summary>
|
||||
/// Path for queued files (after moving from Captures)
|
||||
/// </summary>
|
||||
public string QueuePath { get; set; } = "Queue";
|
||||
|
||||
/// <summary>
|
||||
/// Path for failed jobs
|
||||
/// </summary>
|
||||
public string ErrorPath { get; set; } = "Errors";
|
||||
|
||||
/// <summary>
|
||||
/// Default archive path
|
||||
/// </summary>
|
||||
public string ArchivePath { get; set; } = "Archive";
|
||||
|
||||
/// <summary>
|
||||
/// Maximum retry attempts before moving to error folder
|
||||
/// </summary>
|
||||
public int MaxRetryAttempts { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Debounce delay in milliseconds for file system watcher
|
||||
/// </summary>
|
||||
public int DebounceDelayMs { get; set; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Named pipe name for IPC communication
|
||||
/// </summary>
|
||||
public string PipeName { get; set; } = "PrintServicePipe";
|
||||
|
||||
/// <summary>
|
||||
/// Document type configurations
|
||||
/// </summary>
|
||||
public List<DocumentConfig> DocumentTypes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Queue state file path
|
||||
/// </summary>
|
||||
public string QueueStateFile { get; set; } = "queue_state.json";
|
||||
|
||||
/// <summary>
|
||||
/// Enable detailed logging
|
||||
/// </summary>
|
||||
public bool VerboseLogging { get; set; } = false;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
namespace PrintService.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for document types
|
||||
/// </summary>
|
||||
public class DocumentConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Name/identifier of the document type
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Printer name to send to
|
||||
/// </summary>
|
||||
public string PrinterName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Sequence of tray numbers to use (1-based, e.g., [3, 4, 1, 2])
|
||||
/// These will be mapped to physical PaperSource indexes
|
||||
/// </summary>
|
||||
public int[] TraySequence { get; set; } = Array.Empty<int>();
|
||||
|
||||
/// <summary>
|
||||
/// Font name to use for rendering
|
||||
/// </summary>
|
||||
public string FontName { get; set; } = "Courier New";
|
||||
|
||||
/// <summary>
|
||||
/// Font size in points
|
||||
/// </summary>
|
||||
public float FontSize { get; set; } = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// Vertical offset adjustment (in pixels)
|
||||
/// </summary>
|
||||
public int VerticalOffset { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal offset adjustment (in pixels)
|
||||
/// </summary>
|
||||
public int HorizontalOffset { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Text transformation rules (regex patterns to remove/replace)
|
||||
/// </summary>
|
||||
public List<TextTransform> Transformations { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to archive files after printing (true) or delete them (false)
|
||||
/// </summary>
|
||||
public bool ArchiveAfterPrint { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Archive folder path (if ArchiveAfterPrint is true)
|
||||
/// </summary>
|
||||
public string? ArchivePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Output folder for PDF files (when using "Microsoft Print to PDF")
|
||||
/// If specified, PDFs will be saved to this folder automatically
|
||||
/// </summary>
|
||||
public string? OutputPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Skip post-processing (archive/delete) to leave files in queue for repeated testing
|
||||
/// Useful for development and testing scenarios
|
||||
/// </summary>
|
||||
public bool SkipPostProcessing { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text transformation rule
|
||||
/// </summary>
|
||||
public class TextTransform
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex pattern to match
|
||||
/// </summary>
|
||||
public string Pattern { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Replacement text (empty string to remove)
|
||||
/// </summary>
|
||||
public string Replacement { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Description of what this transformation does
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace PrintService.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a print job in the queue
|
||||
/// </summary>
|
||||
public class PrintJob
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the job
|
||||
/// </summary>
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Path to the original capture file
|
||||
/// </summary>
|
||||
public string SourceFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the queued file (after being moved with GUID name)
|
||||
/// </summary>
|
||||
public string QueuedFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Document type (invoice, order, delivery, etc.)
|
||||
/// </summary>
|
||||
public string DocumentType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional order number
|
||||
/// </summary>
|
||||
public string? OrderNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the job was created
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// When the job was last updated
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Current status of the job
|
||||
/// </summary>
|
||||
public PrintJobStatus Status { get; set; } = PrintJobStatus.Pending;
|
||||
|
||||
/// <summary>
|
||||
/// Number of times this job has been attempted
|
||||
/// </summary>
|
||||
public int RetryCount { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Last error message if failed
|
||||
/// </summary>
|
||||
public string? LastError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original filename before moving to queue
|
||||
/// </summary>
|
||||
public string OriginalFilename { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of a print job
|
||||
/// </summary>
|
||||
public enum PrintJobStatus
|
||||
{
|
||||
Pending,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
RetryScheduled
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-PrintService-3e06b535-3a68-4806-9c5b-3a32bde82bf9</UserSecretsId>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Use x86 only for Release builds (for 32-bit target machines) -->
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
using PrintService;
|
||||
using PrintService.Models;
|
||||
using PrintService.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
IHost host = Host.CreateDefaultBuilder(args)
|
||||
.UseWindowsService(options =>
|
||||
{
|
||||
options.ServiceName = "LAAPC Print Service";
|
||||
})
|
||||
.ConfigureServices((hostContext, services) =>
|
||||
{
|
||||
// Bind configuration
|
||||
services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
|
||||
|
||||
// Register services
|
||||
services.AddSingleton<PrintQueueService>();
|
||||
services.AddSingleton<FileMonitorService>();
|
||||
services.AddSingleton<PrinterService>();
|
||||
services.AddSingleton<DocumentProcessor>();
|
||||
services.AddSingleton<IpcService>();
|
||||
|
||||
// Register the main worker
|
||||
services.AddHostedService<Worker>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
// Ensure directories exist
|
||||
var config = host.Services.GetRequiredService<IOptions<AppSettings>>().Value;
|
||||
Directory.CreateDirectory(config.QueuePath);
|
||||
Directory.CreateDirectory(config.ErrorPath);
|
||||
Directory.CreateDirectory(config.ArchivePath);
|
||||
|
||||
host.Run();
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"PrintService": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PrintService.Models;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Printing;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PrintService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Processes documents and renders them for printing
|
||||
/// </summary>
|
||||
public class DocumentProcessor
|
||||
{
|
||||
private readonly AppSettings _settings;
|
||||
private readonly ILogger<DocumentProcessor> _logger;
|
||||
|
||||
public DocumentProcessor(IOptions<AppSettings> settings, ILogger<DocumentProcessor> logger)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process and transform document content
|
||||
/// </summary>
|
||||
public string ProcessDocument(string content, DocumentConfig config)
|
||||
{
|
||||
var processed = content;
|
||||
|
||||
// Apply text transformations
|
||||
foreach (var transform in config.Transformations)
|
||||
{
|
||||
try
|
||||
{
|
||||
processed = Regex.Replace(processed, transform.Pattern, transform.Replacement);
|
||||
if (_settings.VerboseLogging)
|
||||
{
|
||||
_logger.LogDebug("Applied transformation: {Description}", transform.Description);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to apply transformation: {Pattern}", transform.Pattern);
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get document configuration by type
|
||||
/// </summary>
|
||||
public DocumentConfig? GetDocumentConfig(string documentType)
|
||||
{
|
||||
return _settings.DocumentTypes.FirstOrDefault(dt =>
|
||||
dt.Name.Equals(documentType, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read file content
|
||||
/// </summary>
|
||||
public string ReadFile(string filePath)
|
||||
{
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Archive or delete file after processing
|
||||
/// </summary>
|
||||
public void PostProcess(PrintJob job, DocumentConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Skip post-processing if configured (useful for testing)
|
||||
if (config.SkipPostProcessing)
|
||||
{
|
||||
_logger.LogInformation("Skipping post-processing for job {JobId} (file remains in queue)", job.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.ArchiveAfterPrint && !string.IsNullOrEmpty(config.ArchivePath))
|
||||
{
|
||||
Directory.CreateDirectory(config.ArchivePath);
|
||||
|
||||
var archiveFileName = Path.Combine(config.ArchivePath,
|
||||
$"{DateTime.Now:yyyyMMdd_HHmmss}_{job.OriginalFilename}");
|
||||
|
||||
if (File.Exists(job.QueuedFilePath))
|
||||
{
|
||||
File.Move(job.QueuedFilePath, archiveFileName);
|
||||
_logger.LogInformation("Archived job {JobId} to {Path}", job.Id, archiveFileName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (File.Exists(job.QueuedFilePath))
|
||||
{
|
||||
File.Delete(job.QueuedFilePath);
|
||||
_logger.LogInformation("Deleted file for job {JobId}", job.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to post-process job {JobId}", job.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PrintService.Models;
|
||||
|
||||
namespace PrintService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors the Captures folder for new files and moves them to queue
|
||||
/// </summary>
|
||||
public class FileMonitorService : IDisposable
|
||||
{
|
||||
private readonly AppSettings _settings;
|
||||
private readonly PrintQueueService _queueService;
|
||||
private readonly ILogger<FileMonitorService> _logger;
|
||||
private FileSystemWatcher? _watcher;
|
||||
private readonly Dictionary<string, DateTime> _pendingFiles = new();
|
||||
private readonly Timer _debounceTimer;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public FileMonitorService(
|
||||
IOptions<AppSettings> settings,
|
||||
PrintQueueService queueService,
|
||||
ILogger<FileMonitorService> logger)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_queueService = queueService;
|
||||
_logger = logger;
|
||||
|
||||
// Timer for debouncing file system events
|
||||
_debounceTimer = new Timer(ProcessPendingFiles, null,
|
||||
TimeSpan.FromMilliseconds(_settings.DebounceDelayMs),
|
||||
TimeSpan.FromMilliseconds(_settings.DebounceDelayMs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start monitoring the Captures folder
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (!Directory.Exists(_settings.CapturesPath))
|
||||
{
|
||||
Directory.CreateDirectory(_settings.CapturesPath);
|
||||
_logger.LogInformation("Created captures directory: {Path}", _settings.CapturesPath);
|
||||
}
|
||||
|
||||
_watcher = new FileSystemWatcher(_settings.CapturesPath)
|
||||
{
|
||||
Filter = "*.TXT",
|
||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
_watcher.Created += OnFileCreated;
|
||||
_watcher.Changed += OnFileChanged;
|
||||
|
||||
_logger.LogInformation("Started monitoring: {Path}", _settings.CapturesPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop monitoring
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (_watcher != null)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
_watcher.Dispose();
|
||||
_watcher = null;
|
||||
}
|
||||
_logger.LogInformation("Stopped monitoring");
|
||||
}
|
||||
|
||||
private void OnFileCreated(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingFiles[e.FullPath] = DateTime.Now;
|
||||
if (_settings.VerboseLogging)
|
||||
{
|
||||
_logger.LogDebug("File created: {Path}", e.FullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingFiles[e.FullPath] = DateTime.Now;
|
||||
if (_settings.VerboseLogging)
|
||||
{
|
||||
_logger.LogDebug("File changed: {Path}", e.FullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process files after debounce delay
|
||||
/// </summary>
|
||||
private void ProcessPendingFiles(object? state)
|
||||
{
|
||||
List<string> filesToProcess;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var cutoff = DateTime.Now.AddMilliseconds(-_settings.DebounceDelayMs);
|
||||
filesToProcess = _pendingFiles
|
||||
.Where(kvp => kvp.Value <= cutoff)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var file in filesToProcess)
|
||||
{
|
||||
_pendingFiles.Remove(file);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var filePath in filesToProcess)
|
||||
{
|
||||
ProcessFile(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a single file: move to queue with GUID name
|
||||
/// </summary>
|
||||
private void ProcessFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return; // File may have been moved or deleted
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var jobId = Guid.NewGuid();
|
||||
var queuedFileName = $"{jobId}.txt";
|
||||
var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName);
|
||||
|
||||
// Move file to queue with GUID name to prevent collisions
|
||||
File.Move(filePath, queuedPath);
|
||||
|
||||
_logger.LogInformation("Moved {FileName} to queue as {QueuedFile}", fileName, queuedFileName);
|
||||
|
||||
// Note: Job will be created when CLI sends command with document type
|
||||
// For now, we just have the file safely in the queue folder
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to process file: {Path}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually process a file (called from IPC when CLI provides metadata)
|
||||
/// </summary>
|
||||
public PrintJob CreateJobFromFile(string filePath, string documentType, string? orderNumber)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var jobId = Guid.NewGuid();
|
||||
var queuedFileName = $"{jobId}.txt";
|
||||
var queuedPath = Path.Combine(_settings.QueuePath, queuedFileName);
|
||||
|
||||
// Check if this document type has SkipPostProcessing enabled
|
||||
var config = _settings.DocumentTypes.FirstOrDefault(dt =>
|
||||
dt.Name.Equals(documentType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (config?.SkipPostProcessing == true)
|
||||
{
|
||||
// Copy instead of move for testing scenarios
|
||||
File.Copy(filePath, queuedPath, overwrite: true);
|
||||
_logger.LogInformation("Copied {FileName} to queue (test mode - source file preserved)", fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Move file to queue (normal operation)
|
||||
File.Move(filePath, queuedPath, overwrite: true);
|
||||
_logger.LogInformation("Moved {FileName} to queue", fileName);
|
||||
}
|
||||
|
||||
var job = new PrintJob
|
||||
{
|
||||
Id = jobId,
|
||||
SourceFilePath = filePath,
|
||||
QueuedFilePath = queuedPath,
|
||||
DocumentType = documentType,
|
||||
OrderNumber = orderNumber,
|
||||
OriginalFilename = fileName,
|
||||
Status = PrintJobStatus.Pending,
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
_logger.LogInformation("Created job {JobId} for {FileName} (type: {DocType})",
|
||||
job.Id, fileName, documentType);
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_debounceTimer?.Dispose();
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PrintService.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PrintService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the print job queue with persistence
|
||||
/// </summary>
|
||||
public class PrintQueueService
|
||||
{
|
||||
private readonly ConcurrentQueue<PrintJob> _queue = new();
|
||||
private readonly AppSettings _settings;
|
||||
private readonly ILogger<PrintQueueService> _logger;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public PrintQueueService(IOptions<AppSettings> settings, ILogger<PrintQueueService> logger)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_logger = logger;
|
||||
LoadQueueState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue a new print job
|
||||
/// </summary>
|
||||
public void Enqueue(PrintJob job)
|
||||
{
|
||||
_queue.Enqueue(job);
|
||||
_logger.LogInformation("Job {JobId} enqueued for document type: {DocType}", job.Id, job.DocumentType);
|
||||
SaveQueueState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to dequeue the next pending job
|
||||
/// </summary>
|
||||
public bool TryDequeue(out PrintJob? job)
|
||||
{
|
||||
bool result = _queue.TryDequeue(out job);
|
||||
if (result && job != null)
|
||||
{
|
||||
_logger.LogInformation("Job {JobId} dequeued", job.Id);
|
||||
SaveQueueState();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Peek at the next job without removing it
|
||||
/// </summary>
|
||||
public bool TryPeek(out PrintJob? job)
|
||||
{
|
||||
return _queue.TryPeek(out job);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get count of jobs in queue
|
||||
/// </summary>
|
||||
public int Count => _queue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Save queue state to disk
|
||||
/// </summary>
|
||||
private void SaveQueueState()
|
||||
{
|
||||
try
|
||||
{
|
||||
_semaphore.Wait();
|
||||
var jobs = _queue.ToArray();
|
||||
var json = JsonSerializer.Serialize(jobs, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(_settings.QueueStateFile, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to save queue state");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load queue state from disk
|
||||
/// </summary>
|
||||
private void LoadQueueState()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_settings.QueueStateFile))
|
||||
{
|
||||
var json = File.ReadAllText(_settings.QueueStateFile);
|
||||
var jobs = JsonSerializer.Deserialize<PrintJob[]>(json);
|
||||
|
||||
if (jobs != null)
|
||||
{
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
// Only reload pending or retry-scheduled jobs
|
||||
if (job.Status == PrintJobStatus.Pending || job.Status == PrintJobStatus.RetryScheduled)
|
||||
{
|
||||
_queue.Enqueue(job);
|
||||
}
|
||||
}
|
||||
_logger.LogInformation("Loaded {Count} jobs from queue state", _queue.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load queue state");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requeue a job (for retry)
|
||||
/// </summary>
|
||||
public void Requeue(PrintJob job)
|
||||
{
|
||||
job.RetryCount++;
|
||||
job.Status = PrintJobStatus.RetryScheduled;
|
||||
job.UpdatedAt = DateTime.Now;
|
||||
|
||||
if (job.RetryCount >= _settings.MaxRetryAttempts)
|
||||
{
|
||||
_logger.LogWarning("Job {JobId} exceeded max retries, moving to error folder", job.Id);
|
||||
MoveToErrorFolder(job);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Requeueing job {JobId}, attempt {Retry}", job.Id, job.RetryCount);
|
||||
_queue.Enqueue(job);
|
||||
SaveQueueState();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move a failed job to error folder
|
||||
/// </summary>
|
||||
private void MoveToErrorFolder(PrintJob job)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errorFileName = Path.Combine(_settings.ErrorPath,
|
||||
$"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}.txt");
|
||||
|
||||
if (File.Exists(job.QueuedFilePath))
|
||||
{
|
||||
File.Move(job.QueuedFilePath, errorFileName);
|
||||
}
|
||||
|
||||
// Save error details
|
||||
var errorInfo = Path.Combine(_settings.ErrorPath,
|
||||
$"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}_error.json");
|
||||
var json = JsonSerializer.Serialize(job, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(errorInfo, json);
|
||||
|
||||
job.Status = PrintJobStatus.Failed;
|
||||
SaveQueueState();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to move job {JobId} to error folder", job.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PrintService.Models;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Printing;
|
||||
|
||||
namespace PrintService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Handles printing to Windows print queue with tray control
|
||||
/// </summary>
|
||||
public class PrinterService
|
||||
{
|
||||
private readonly AppSettings _settings;
|
||||
private readonly ILogger<PrinterService> _logger;
|
||||
|
||||
public PrinterService(IOptions<AppSettings> settings, ILogger<PrinterService> logger)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print document to specified printer with tray sequence
|
||||
/// </summary>
|
||||
public void Print(string content, DocumentConfig config, string originalFileName)
|
||||
{
|
||||
if (config.TraySequence.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No tray sequence defined for document type");
|
||||
}
|
||||
|
||||
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
|
||||
var linesPerPage = CalculateLinesPerPage(config);
|
||||
var currentPageIndex = 0;
|
||||
|
||||
var printDoc = new PrintDocument
|
||||
{
|
||||
PrinterSettings = { PrinterName = config.PrinterName }
|
||||
};
|
||||
|
||||
// Handle PDF output if using Microsoft Print to PDF
|
||||
if (config.PrinterName.Equals("Microsoft Print to PDF", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrEmpty(config.OutputPath))
|
||||
{
|
||||
Directory.CreateDirectory(config.OutputPath);
|
||||
var pdfFileName = Path.GetFileNameWithoutExtension(originalFileName) + ".pdf";
|
||||
var pdfPath = Path.Combine(config.OutputPath, pdfFileName);
|
||||
|
||||
printDoc.PrinterSettings.PrintToFile = true;
|
||||
printDoc.PrinterSettings.PrintFileName = pdfPath;
|
||||
|
||||
_logger.LogInformation("PDF will be saved to: {Path}", pdfPath);
|
||||
}
|
||||
|
||||
// Verify printer exists
|
||||
if (!PrinterSettings.InstalledPrinters.Cast<string>().Contains(config.PrinterName))
|
||||
{
|
||||
throw new InvalidOperationException($"Printer '{config.PrinterName}' not found");
|
||||
}
|
||||
|
||||
printDoc.PrintPage += (sender, e) =>
|
||||
{
|
||||
if (e.Graphics == null || e.PageSettings == null)
|
||||
return;
|
||||
|
||||
// Select tray for current page
|
||||
var trayIndex = config.TraySequence[currentPageIndex % config.TraySequence.Length];
|
||||
|
||||
try
|
||||
{
|
||||
// Map logical tray number to PaperSource
|
||||
var paperSource = GetPaperSource(printDoc.PrinterSettings, trayIndex);
|
||||
if (paperSource != null)
|
||||
{
|
||||
e.PageSettings.PaperSource = paperSource;
|
||||
if (_settings.VerboseLogging)
|
||||
{
|
||||
_logger.LogDebug("Page {Page}: Using tray {Tray} ({Source})",
|
||||
currentPageIndex + 1, trayIndex, paperSource.SourceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to set tray {Tray}, using default", trayIndex);
|
||||
}
|
||||
|
||||
// Render page content
|
||||
RenderPage(e.Graphics, lines, currentPageIndex * linesPerPage, linesPerPage, config);
|
||||
|
||||
currentPageIndex++;
|
||||
e.HasMorePages = (currentPageIndex < config.TraySequence.Length);
|
||||
};
|
||||
|
||||
_logger.LogInformation("Printing to {Printer} with {Pages} pages",
|
||||
config.PrinterName, config.TraySequence.Length);
|
||||
|
||||
printDoc.Print();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render page content using Graphics API
|
||||
/// </summary>
|
||||
private void RenderPage(Graphics graphics, string[] lines, int startLine, int linesPerPage, DocumentConfig config)
|
||||
{
|
||||
var font = new Font(config.FontName, config.FontSize);
|
||||
var brush = Brushes.Black;
|
||||
var lineHeight = font.GetHeight(graphics);
|
||||
|
||||
var x = (float)config.HorizontalOffset;
|
||||
var y = (float)config.VerticalOffset;
|
||||
|
||||
var endLine = Math.Min(startLine + linesPerPage, lines.Length);
|
||||
|
||||
for (int i = startLine; i < endLine; i++)
|
||||
{
|
||||
if (i < lines.Length)
|
||||
{
|
||||
graphics.DrawString(lines[i], font, brush, x, y);
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate lines per page based on font and page size
|
||||
/// </summary>
|
||||
private int CalculateLinesPerPage(DocumentConfig config)
|
||||
{
|
||||
// Estimate: standard letter size is 11 inches, at 10pt font ~66 lines
|
||||
// This is simplified; in production you'd calculate based on actual page dimensions
|
||||
var estimatedLineHeight = config.FontSize * 1.2f; // points
|
||||
var pageHeightInPoints = 11 * 72; // 11 inches * 72 points per inch
|
||||
return (int)Math.Floor(pageHeightInPoints / estimatedLineHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get PaperSource by logical tray number
|
||||
/// </summary>
|
||||
private PaperSource? GetPaperSource(PrinterSettings printerSettings, int trayNumber)
|
||||
{
|
||||
// Try to find by RawKind (tray number)
|
||||
foreach (PaperSource source in printerSettings.PaperSources)
|
||||
{
|
||||
// Some printers use RawKind directly as tray number
|
||||
if (source.RawKind == trayNumber ||
|
||||
source.RawKind == (trayNumber + 256)) // Some drivers offset by 256
|
||||
{
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use by index if available
|
||||
if (trayNumber > 0 && trayNumber <= printerSettings.PaperSources.Count)
|
||||
{
|
||||
return printerSettings.PaperSources[trayNumber - 1];
|
||||
}
|
||||
|
||||
_logger.LogWarning("Could not map tray {Tray} to PaperSource", trayNumber);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List available paper sources for a printer (for debugging/configuration)
|
||||
/// </summary>
|
||||
public void ListPaperSources(string printerName)
|
||||
{
|
||||
var printDoc = new PrintDocument { PrinterSettings = { PrinterName = printerName } };
|
||||
|
||||
_logger.LogInformation("Available paper sources for {Printer}:", printerName);
|
||||
foreach (PaperSource source in printDoc.PrinterSettings.PaperSources)
|
||||
{
|
||||
_logger.LogInformation(" - {Name} (RawKind: {Kind})", source.SourceName, source.RawKind);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using PrintService.Models;
|
||||
using PrintService.Services;
|
||||
|
||||
namespace PrintService;
|
||||
|
||||
/// <summary>
|
||||
/// Main worker service that coordinates all components
|
||||
/// </summary>
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
private readonly PrintQueueService _queueService;
|
||||
private readonly FileMonitorService _fileMonitorService;
|
||||
private readonly PrinterService _printerService;
|
||||
private readonly DocumentProcessor _documentProcessor;
|
||||
private readonly IpcService _ipcService;
|
||||
|
||||
public Worker(
|
||||
ILogger<Worker> logger,
|
||||
PrintQueueService queueService,
|
||||
FileMonitorService fileMonitorService,
|
||||
PrinterService printerService,
|
||||
DocumentProcessor documentProcessor,
|
||||
IpcService ipcService)
|
||||
{
|
||||
_logger = logger;
|
||||
_queueService = queueService;
|
||||
_fileMonitorService = fileMonitorService;
|
||||
_printerService = printerService;
|
||||
_documentProcessor = documentProcessor;
|
||||
_ipcService = ipcService;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("LAAPC Print Service starting at: {time}", DateTimeOffset.Now);
|
||||
|
||||
try
|
||||
{
|
||||
// Start IPC server for CLI communication
|
||||
_ipcService.Start();
|
||||
|
||||
// Start file monitoring (optional - mainly using IPC for job creation)
|
||||
// _fileMonitorService.Start();
|
||||
|
||||
_logger.LogInformation("Print service started successfully");
|
||||
_logger.LogInformation("Queue has {Count} pending jobs", _queueService.Count);
|
||||
|
||||
// Main processing loop
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_queueService.TryDequeue(out var job) && job != null)
|
||||
{
|
||||
await ProcessJob(job, stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No jobs in queue, wait a bit
|
||||
await Task.Delay(500, stoppingToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected during shutdown, exit gracefully
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in processing loop");
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Shutdown requested during error delay
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected during shutdown
|
||||
_logger.LogInformation("Service shutdown requested");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "Fatal error in worker service");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("Print service stopping");
|
||||
_ipcService.Stop();
|
||||
_fileMonitorService.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a single print job
|
||||
/// </summary>
|
||||
private async Task ProcessJob(PrintJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Processing job {JobId} (Type: {DocType}, Order: {OrderNum})",
|
||||
job.Id, job.DocumentType, job.OrderNumber ?? "N/A");
|
||||
|
||||
job.Status = PrintJobStatus.Processing;
|
||||
job.UpdatedAt = DateTime.Now;
|
||||
|
||||
try
|
||||
{
|
||||
// Get document configuration
|
||||
var config = _documentProcessor.GetDocumentConfig(job.DocumentType);
|
||||
if (config == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No configuration found for document type: {job.DocumentType}");
|
||||
}
|
||||
|
||||
// Read file content
|
||||
if (!File.Exists(job.QueuedFilePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Queued file not found: {job.QueuedFilePath}");
|
||||
}
|
||||
|
||||
var content = _documentProcessor.ReadFile(job.QueuedFilePath);
|
||||
|
||||
// Process/transform content
|
||||
var processedContent = _documentProcessor.ProcessDocument(content, config);
|
||||
|
||||
// Print to Windows queue
|
||||
await Task.Run(() => _printerService.Print(processedContent, config, job.OriginalFilename), cancellationToken);
|
||||
|
||||
// Post-process (archive or delete)
|
||||
_documentProcessor.PostProcess(job, config);
|
||||
|
||||
// Mark as completed
|
||||
job.Status = PrintJobStatus.Completed;
|
||||
job.UpdatedAt = DateTime.Now;
|
||||
|
||||
_logger.LogInformation("Job {JobId} completed successfully", job.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to process job {JobId}", job.Id);
|
||||
|
||||
job.LastError = ex.Message;
|
||||
job.UpdatedAt = DateTime.Now;
|
||||
|
||||
// Requeue for retry
|
||||
_queueService.Requeue(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AppSettings": {
|
||||
"CapturesPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Captures",
|
||||
"QueuePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Queue",
|
||||
"ErrorPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Errors",
|
||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive",
|
||||
"MaxRetryAttempts": 3,
|
||||
"DebounceDelayMs": 200,
|
||||
"PipeName": "PrintServicePipe",
|
||||
"QueueStateFile": "C:\\Users\\Work\\Desktop\\LAAPC\\queue_state.json",
|
||||
"VerboseLogging": true,
|
||||
"DocumentTypes": [
|
||||
{
|
||||
"Name": "invoice",
|
||||
"PrinterName": "Microsoft Print to PDF",
|
||||
"TraySequence": [ 3, 4, 1, 2 ],
|
||||
"FontName": "Courier New",
|
||||
"FontSize": 10.0,
|
||||
"VerticalOffset": 0,
|
||||
"HorizontalOffset": 0,
|
||||
"ArchiveAfterPrint": true,
|
||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Invoices",
|
||||
"Transformations": [
|
||||
{
|
||||
"Pattern": "W1DUPLICATE INVW0",
|
||||
"Replacement": "",
|
||||
"Description": "Remove duplicate invoice marker"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "order",
|
||||
"PrinterName": "Brother HL-2270DW series Printer",
|
||||
"TraySequence": [ 4, 2, 1 ],
|
||||
"FontName": "Courier New",
|
||||
"FontSize": 10.0,
|
||||
"VerticalOffset": 0,
|
||||
"HorizontalOffset": 0,
|
||||
"ArchiveAfterPrint": true,
|
||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Orders",
|
||||
"Transformations": []
|
||||
},
|
||||
{
|
||||
"Name": "delivery",
|
||||
"PrinterName": "Brother HL-2270DW series Printer",
|
||||
"TraySequence": [ 3, 1 ],
|
||||
"FontName": "Courier New",
|
||||
"FontSize": 10.0,
|
||||
"VerticalOffset": 0,
|
||||
"HorizontalOffset": 0,
|
||||
"ArchiveAfterPrint": true,
|
||||
"ArchivePath": "C:\\Users\\Work\\Desktop\\LAAPC\\Archive\\Delivery",
|
||||
"Transformations": []
|
||||
},
|
||||
{
|
||||
"Name": "test",
|
||||
"PrinterName": "Microsoft Print to PDF",
|
||||
"TraySequence": [ 1 ],
|
||||
"FontName": "Courier New",
|
||||
"FontSize": 10.0,
|
||||
"VerticalOffset": 0,
|
||||
"HorizontalOffset": 0,
|
||||
"ArchiveAfterPrint": false,
|
||||
"OutputPath": "C:\\Users\\Work\\Desktop\\LAAPC\\Output",
|
||||
"SkipPostProcessing": true,
|
||||
"Transformations": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user