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