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