Задание термопринтера WPF C# зависло в очереди печати после того, как приложение простаивает/свернуто, работает только пC#

Место общения программистов C#
Ответить
Anonymous
 Задание термопринтера WPF C# зависло в очереди печати после того, как приложение простаивает/свернуто, работает только п

Сообщение Anonymous »

Вопрос
У меня есть настольное приложение C# WPF, которое печатает счета на термальный принтер с использованием Пользовательской очереди печати (печать в стиле ESC/POS).
Проблема:

Если приложение бездействует или свернуто ~10 минут, печать перестает работать:
  • Задание печати создано успешно.
  • Оно появляется в очереди печати
  • Задание никогда не печатается (не приостановлено, нет ошибок) — Показать ожидание.
  • Исключение не создается. приложение
  • Если я перезапущу приложение, печать заработает немедленно
Сам принтер работает нормально и печатает нормально из других приложений.
Что работает:
  • Перезапуск приложения (не требуется перезагрузка системы или принтера)
Что НЕ помогает:
  • Возврат приложения на передний план
  • Повторная попытка печати
  • Принтер подключен к сети и реагирует
Среда:
  • Рабочий стол Windows
  • WPF (.NET)
  • Термопринтер
  • Приложение продолжает работать (не приостановлено и не закрыто)
Вопросы:
  • Что могло привести к зависанию заданий печати после того, как приложение было бездействующим/свернутым?
  • Временно Я исправил это, запустив в фоновом режиме. Плохая ли практика поддерживать работу приложения с помощью таймера/пульса?
  • Рекомендуется ли подход к:
    • Повторной инициализации объектов принтера перед каждой печатью?
    • Повторному созданию службы печати после бездействия?
    • Избегайте повторного использования полностью связанные с принтером объекты?
Я ищу лучшие практики для обеспечения надежности печати в долго работающих приложениях WPF, особенно с термопринтерами.

Код: Выделить всё

public class PrintQueueProcessor : IDisposable
{
private readonly IDbContextFactory _contextFactory;
private readonly ThermalPrinterService _thermalPrinterService;
private Timer? _processingTimer;
private Timer? _cleanupTimer;
private Timer? _keepAliveTimer;
private readonly object _lock = new();
private bool _isProcessing;
private bool _isRunning;
private CancellationTokenSource? _cts;
private Task? _currentProcessingTask;

public PrintQueueProcessor(
IDbContextFactory  contextFactory,
ThermalPrinterService thermalPrinterService)
{
_contextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory));
_thermalPrinterService = thermalPrinterService ?? throw new ArgumentNullException(nameof(thermalPrinterService));
Log.Information("PrintQueueProcessor initialized");
}

public void Start()
{
lock (_lock)
{
if (_isRunning)
{
Log.Warning("Print queue processor already running");
return;
}

_isRunning = true;
_cts = new CancellationTokenSource();

_processingTimer = new Timer(
ProcessPendingJobsCallback,
null,
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(3));

_cleanupTimer = new Timer(
CleanupCallback,
null,
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(5));

_keepAliveTimer = new Timer(
KeepAliveCallback,
null,
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(2));

Log.Information("✅ Print Queue Processor STARTED (with keep-alive)");
}
}

#region Windows Print Spooler API

[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool OpenPrinter(string pPrinterName, out IntPtr phPrinter, IntPtr pDefault);

[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool ClosePrinter(IntPtr hPrinter);

[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool GetPrinter(IntPtr hPrinter, int Level, IntPtr pPrinter, int cbBuf, out int pcbNeeded);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct PRINTER_INFO_2
{
public string pServerName;
public string pPrinterName;
public string pShareName;
public string pPortName;
public string pDriverName;
public string pComment;
public string pLocation;
public IntPtr pDevMode;
public string pSepFile;
public string pPrintProcessor;
public string pDatatype;
public string pParameters;
public IntPtr pSecurityDescriptor;
public uint Attributes;
public uint Priority;
public uint DefaultPriority;
public uint StartTime;
public uint UntilTime;
public uint Status;
public uint cJobs;
public uint AveragePPM;
}

private bool PingPrinter(string printerName)
{
IntPtr hPrinter = IntPtr.Zero;
try
{
if (!OpenPrinter(printerName, out hPrinter, IntPtr.Zero))
{
Log.Warning("⚠️ Cannot open printer: {Printer}", printerName);
return false;
}

// Get printer info - this keeps connection alive
GetPrinter(hPrinter, 2, IntPtr.Zero, 0, out int needed);

if (needed >  0)
{
IntPtr pPrinterInfo = Marshal.AllocHGlobal(needed);
try
{
if (GetPrinter(hPrinter, 2, pPrinterInfo, needed, out _))
{
var info = Marshal.PtrToStructure(pPrinterInfo);
Log.Debug("🖨️ Printer '{Printer}' alive - Jobs: {Jobs}, Status: {Status}",
printerName, info.cJobs, info.Status);
return true;
}
}
finally
{
Marshal.FreeHGlobal(pPrinterInfo);
}
}

return true;
}
catch (Exception ex)
{
Log.Warning("⚠️ Printer ping failed: {Printer} - {Message}", printerName, ex.Message);
return false;
}
finally
{
if (hPrinter != IntPtr.Zero)
ClosePrinter(hPrinter);
}
}

#endregion

#region Timer Callbacks

private void ProcessPendingJobsCallback(object? state)
{
if (_isProcessing || !_isRunning || (_cts?.IsCancellationRequested ?? true))
return;

lock (_lock)
{
if (_isProcessing) return;
_isProcessing = true;
}

_currentProcessingTask = Task.Run(async () =>
{
try
{
await ProcessPendingJobsAsync(_cts!.Token);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
Log.Error(ex, "Error in ProcessPendingJobsAsync");
}
finally
{
lock (_lock)
{
_isProcessing = false;
}
}
});
}

private void CleanupCallback(object? state)
{
if (!_isRunning || (_cts?.IsCancellationRequested ?? true))
return;

_ = Task.Run(async () =>
{
try
{
await CleanupOldJobsAsync(_cts!.Token);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Log.Error(ex, "Error in CleanupOldJobsAsync");
}
});
}

private void KeepAliveCallback(object? state)
{
if (!_isRunning || (_cts?.IsCancellationRequested ?? true))
return;

_ = Task.Run(async () =>
{
try
{
await KeepPrintersAliveAsync(_cts!.Token);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Log.Debug("Keep-alive error: {Message}", ex.Message);
}
});
}

#endregion

#region Printer Keep-Alive

private async Task KeepPrintersAliveAsync(CancellationToken cancellationToken)
{
try
{
await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);

// Get unique printer names from recent print jobs
var recentPrinters = await context.PrintQueueJobs
.Where(j => j.CreatedAtUtc > DateTime.UtcNow.AddHours(-24))
.Select(j => j.PrinterName)
.Distinct()
.ToListAsync(cancellationToken);

// Also get printers from template mappings
var mappedPrinters = await context.PrinterTemplateMappings
.Where(m => m.IsActive && !string.IsNullOrEmpty(m.PrinterName))
.Select(m =>  m.PrinterName)
.Distinct()
.ToListAsync(cancellationToken);

var allPrinters = recentPrinters
.Union(mappedPrinters)
.Where(p => !string.IsNullOrWhiteSpace(p))
.Distinct()
.ToList();

foreach (var printerName in allPrinters)
{
cancellationToken.ThrowIfCancellationRequested();
PingPrinter(printerName!);
}
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
Log.Debug("KeepPrintersAliveAsync: {Message}", ex.Message);
}
}

#endregion

#region Job Processing

private async Task ProcessPendingJobsAsync(CancellationToken cancellationToken)
{
try
{
await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);

var pendingJobs = await context.PrintQueueJobs
.Where(j => j.Status == PrintJobStatus.Pending)
.OrderByDescending(j => j.Priority)
.ThenBy(j => j.CreatedAtUtc)
.Take(5)
.ToListAsync(cancellationToken);

foreach (var job in pendingJobs)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessSingleJobAsync(context, job, cancellationToken);
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Log.Error(ex, "Error in ProcessPendingJobsAsync");
}
}

private async Task ProcessSingleJobAsync(AppDbContext context, PrintQueueJob job, CancellationToken cancellationToken)
{
try
{
Log.Information("🖨️ Processing Job {JobId}: Bill={BillId}, Printer={Printer}",
job.Id, job.BillId, job.PrinterName);

job.Status = PrintJobStatus.Processing;
job.LastAttemptAtUtc = DateTime.UtcNow;
job.AttemptCount++;
await context.SaveChangesAsync(cancellationToken);

object? dataToPrint = null;

if (job.BillId.HasValue)
{
dataToPrint = await context.Bills
.Include(b => b.Items)
.Include(b => b.Payments)
.AsNoTracking()
.FirstOrDefaultAsync(b => b.Id == job.BillId.Value, cancellationToken);

if (dataToPrint == null)
throw new InvalidOperationException($"Bill {job.BillId} not found");
}
else if (!string.IsNullOrEmpty(job.Context))
{
var kotId = ExtractKotIdFromContext(job.Context);
if (kotId.HasValue)
{
dataToPrint = await context.Kots
.Include(k => k.Items)
.AsNoTracking()
.FirstOrDefaultAsync(k =>  k.Id == kotId.Value, cancellationToken);

if (dataToPrint == null)
throw new InvalidOperationException($"KOT {kotId} not found");
}
}

if (dataToPrint == null)
throw new InvalidOperationException("No data to print");

cancellationToken.ThrowIfCancellationRequested();

bool printSuccess = await _thermalPrinterService.PrintAsync(
job.PrinterName,
job.TemplateId,
dataToPrint);

if (printSuccess)
{
job.Status = PrintJobStatus.Completed;
job.CompletedAtUtc = DateTime.UtcNow;
job.ErrorMessage = null;
Log.Information("✅ Job {JobId} COMPLETED!", job.Id);
}
else
{
throw new Exception("PrintAsync returned false");
}

await context.SaveChangesAsync(cancellationToken);
}
catch (OperationCanceledException)
{
job.Status = PrintJobStatus.Pending;
job.AttemptCount = Math.Max(0, job.AttemptCount - 1);
await context.SaveChangesAsync(CancellationToken.None);
throw;
}
catch (Exception ex)
{
Log.Error(ex, "❌ Job {JobId} failed: {Message}", job.Id, ex.Message);

job.ErrorMessage = ex.Message;
job.Status = job.AttemptCount >= job.MaxRetries
? PrintJobStatus.Failed
: PrintJobStatus.Pending;

await context.SaveChangesAsync(CancellationToken.None);
}
}

private int? ExtractKotIdFromContext(string? context)
{
if (string.IsNullOrEmpty(context)) return null;

var parts = context.Split(',');
var kotPart = parts.FirstOrDefault(p => p.StartsWith("KOT:", StringComparison.OrdinalIgnoreCase));

if (kotPart != null)
{
var idParts = kotPart.Split(':');
if (idParts.Length > 1 && int.TryParse(idParts[1], out int kotId))
return kotId;
}

return null;
}

#endregion

#region Cleanup

public async Task CleanupOldJobsAsync(CancellationToken cancellationToken = default)
{
try
{
await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);

var cutoffDate = DateTime.UtcNow.AddDays(-3);

var oldJobs = await context.PrintQueueJobs
.Where(j => j.CreatedAtUtc < cutoffDate)
.Where(j => j.Status == PrintJobStatus.Completed || j.Status == PrintJobStatus.Failed)
.ToListAsync(cancellationToken);

if (oldJobs.Any())
{
context.PrintQueueJobs.RemoveRange(oldJobs);
await context.SaveChangesAsync(cancellationToken);
Log.Information("🧹 Cleaned up {Count} old jobs", oldJobs.Count);
}
}
catch (OperationCanceledException) { throw;  }
catch (Exception ex)
{
Log.Error(ex, "Error cleaning up old jobs");
}
}

#endregion

#region Lifecycle

public void Stop()
{
lock (_lock)
{
if (!_isRunning)
return;

Log.Information("Stopping PrintQueueProcessor...");

_isRunning = false;
_cts?.Cancel();

_processingTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_keepAliveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
}

try
{
_currentProcessingTask?.Wait(TimeSpan.FromSeconds(3));
}
catch (AggregateException) { }
catch (TaskCanceledException) { }

_processingTimer?.Dispose();
_cleanupTimer?.Dispose();
_keepAliveTimer?.Dispose();
_processingTimer = null;
_cleanupTimer = null;
_keepAliveTimer = null;

Log.Information("PrintQueueProcessor stopped");
}

public void Dispose()
{
Stop();
_cts?.Dispose();
_cts = null;
}

#endregion
}
Теги

Подробнее здесь: https://stackoverflow.com/questions/798 ... -minimized
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»