refactoring
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(grep:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,12 @@ public class HeartbeatBackgroundTask
|
|||||||
|
|
||||||
public HeartbeatBackgroundTask(
|
public HeartbeatBackgroundTask(
|
||||||
EpsonPrintServiceConfiguration configuration,
|
EpsonPrintServiceConfiguration configuration,
|
||||||
ILogger logger)
|
ILogger logger,
|
||||||
|
HttpClient httpClient)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClient = new HttpClient();
|
_httpClient = httpClient;
|
||||||
_interval = TimeSpan.FromSeconds(30);
|
_interval = TimeSpan.FromSeconds(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ var config = JsonSerializer.Deserialize<EpsonPrintServiceConfiguration>(
|
|||||||
File.ReadAllText("config.json"));
|
File.ReadAllText("config.json"));
|
||||||
|
|
||||||
kernel.Bind<IDiscoveryService>().To<DiscoveryService>().InSingletonScope();
|
kernel.Bind<IDiscoveryService>().To<DiscoveryService>().InSingletonScope();
|
||||||
|
kernel.Bind<HttpClient>().ToConstant(new HttpClient { Timeout = TimeSpan.FromSeconds(30) }).InSingletonScope();
|
||||||
|
|
||||||
|
|
||||||
kernel.Bind<EpsonPrintServiceConfiguration>().ToConstant(config);
|
kernel.Bind<EpsonPrintServiceConfiguration>().ToConstant(config);
|
||||||
|
|||||||
@@ -11,17 +11,19 @@ public class EpsonPrintService: IPrintService
|
|||||||
private readonly IWrapperPrinterFactory _wrapperPrinterFactory;
|
private readonly IWrapperPrinterFactory _wrapperPrinterFactory;
|
||||||
private readonly IPrinterConfigurationSource _printerConfigurationSource;
|
private readonly IPrinterConfigurationSource _printerConfigurationSource;
|
||||||
private readonly IReceiptConverterFactory _receiptConverterFactory;
|
private readonly IReceiptConverterFactory _receiptConverterFactory;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
public EpsonPrintService(ILogger logger, IPrinterFactory printerFactory, IWrapperPrinterFactory wrapperPrinterFactory, IPrinterConfigurationSource printerConfigurationSource, IReceiptConverterFactory receiptConverterFactory)
|
public EpsonPrintService(ILogger logger, IPrinterFactory printerFactory, IWrapperPrinterFactory wrapperPrinterFactory, IPrinterConfigurationSource printerConfigurationSource, IReceiptConverterFactory receiptConverterFactory, HttpClient httpClient)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_printerFactory = printerFactory;
|
_printerFactory = printerFactory;
|
||||||
_wrapperPrinterFactory = wrapperPrinterFactory;
|
_wrapperPrinterFactory = wrapperPrinterFactory;
|
||||||
_printerConfigurationSource = printerConfigurationSource;
|
_printerConfigurationSource = printerConfigurationSource;
|
||||||
_receiptConverterFactory = receiptConverterFactory;
|
_receiptConverterFactory = receiptConverterFactory;
|
||||||
|
_httpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> PrintAsync(string printerIp, PrintJob job)
|
public async Task<PrintResult> PrintAsync(string printerIp, PrintJob job)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -65,10 +67,7 @@ public class EpsonPrintService: IPrintService
|
|||||||
// Download only if not already cached
|
// Download only if not already cached
|
||||||
if (!File.Exists(cachedLogoPath))
|
if (!File.Exists(cachedLogoPath))
|
||||||
{
|
{
|
||||||
using var httpClient = new HttpClient();
|
var logoData = await _httpClient.GetByteArrayAsync(job.Document.LogoUrl);
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
var logoData = await httpClient.GetByteArrayAsync(job.Document.LogoUrl);
|
|
||||||
await File.WriteAllBytesAsync(cachedLogoPath, logoData);
|
await File.WriteAllBytesAsync(cachedLogoPath, logoData);
|
||||||
|
|
||||||
_logger.LogInformation("Downloaded logo {LogoFileName} from {LogoUrl}", logoFileName, job.Document.LogoUrl);
|
_logger.LogInformation("Downloaded logo {LogoFileName} from {LogoUrl}", logoFileName, job.Document.LogoUrl);
|
||||||
@@ -84,8 +83,18 @@ public class EpsonPrintService: IPrintService
|
|||||||
await printerAdapter.SetFontSizeAsync(configuration.FontSize);
|
await printerAdapter.SetFontSizeAsync(configuration.FontSize);
|
||||||
|
|
||||||
// Convert receipt content to print commands using the factory
|
// Convert receipt content to print commands using the factory
|
||||||
var converter = _receiptConverterFactory.Create(job.Document.ReceiptType, printerId.Value);
|
List<PrintCommand> commands;
|
||||||
var commands = converter.Convert(job.Document.Content);
|
try
|
||||||
|
{
|
||||||
|
var converter = _receiptConverterFactory.Create(job.Document.ReceiptType, printerId.Value);
|
||||||
|
commands = converter.Convert(job.Document.Content);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Conversion error for print job on printer {PrinterUid}", printerIp);
|
||||||
|
return PrintResult.Fail(PrintErrorType.ConversionError);
|
||||||
|
}
|
||||||
|
|
||||||
await printerAdapter.PrintAsync(commands);
|
await printerAdapter.PrintAsync(commands);
|
||||||
await Task.Delay(200);
|
await Task.Delay(200);
|
||||||
status = await epsonPrinter.GetPrinterStatusAsync();
|
status = await epsonPrinter.GetPrinterStatusAsync();
|
||||||
@@ -98,10 +107,10 @@ public class EpsonPrintService: IPrintService
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogWarning( e, "Failed to print job for printer {PrinterUid}", printerIp);
|
_logger.LogWarning(e, "Failed to print job for printer {PrinterUid}", printerIp);
|
||||||
return false;
|
return PrintResult.Fail(PrintErrorType.Other);
|
||||||
}
|
}
|
||||||
return true;
|
return PrintResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,5 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
public interface IPrintService
|
public interface IPrintService
|
||||||
{
|
{
|
||||||
Task<bool> PrintAsync(string printerIp, PrintJob job);
|
Task<PrintResult> PrintAsync(string printerIp, PrintJob job);
|
||||||
}
|
}
|
||||||
|
|||||||
6
Inspectron.Epson/Queue/PrintErrorType.cs
Normal file
6
Inspectron.Epson/Queue/PrintErrorType.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
public enum PrintErrorType
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
ConversionError,
|
||||||
|
Other
|
||||||
|
}
|
||||||
14
Inspectron.Epson/Queue/PrintResult.cs
Normal file
14
Inspectron.Epson/Queue/PrintResult.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
public class PrintResult
|
||||||
|
{
|
||||||
|
public bool Success { get; }
|
||||||
|
public PrintErrorType ErrorType { get; }
|
||||||
|
|
||||||
|
private PrintResult(bool success, PrintErrorType errorType)
|
||||||
|
{
|
||||||
|
Success = success;
|
||||||
|
ErrorType = errorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PrintResult Ok() => new(true, PrintErrorType.None);
|
||||||
|
public static PrintResult Fail(PrintErrorType errorType) => new(false, errorType);
|
||||||
|
}
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using System.Collections.Concurrent;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
public class PrintServer
|
public class PrintServer
|
||||||
{
|
{
|
||||||
private readonly IPrintService _printService;
|
private readonly IPrintService _printService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Dictionary<string, PrinterQueue> _printerQueues;
|
private readonly ConcurrentDictionary<string, PrinterQueue> _printerQueues;
|
||||||
|
|
||||||
public PrintServer(IPrintService printService, ILogger logger)
|
public PrintServer(IPrintService printService, ILogger logger)
|
||||||
{
|
{
|
||||||
_printService = printService;
|
_printService = printService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_printerQueues = new Dictionary<string, PrinterQueue>();
|
_printerQueues = new ConcurrentDictionary<string, PrinterQueue>();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterPrinter(string printerIp)
|
public void RegisterPrinter(string printerIp)
|
||||||
{
|
{
|
||||||
if (_printerQueues.ContainsKey(printerIp))
|
var queue = new PrinterQueue(printerIp, _printService, _logger);
|
||||||
return;
|
if (_printerQueues.TryAdd(printerIp, queue))
|
||||||
|
{
|
||||||
var queue = new PrinterQueue(printerIp, _printService,_logger);
|
queue.Start();
|
||||||
_printerQueues[printerIp] = queue;
|
Console.WriteLine($"Printer {printerIp} registered");
|
||||||
queue.Start();
|
}
|
||||||
Console.WriteLine($"Printer {printerIp} registered");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnregisterPrinter(string printerIp)
|
public void UnregisterPrinter(string printerIp)
|
||||||
{
|
{
|
||||||
if (_printerQueues.TryGetValue(printerIp, out var queue))
|
if (_printerQueues.TryRemove(printerIp, out var queue))
|
||||||
{
|
{
|
||||||
queue.StopAsync().Wait();
|
queue.StopAsync().Wait();
|
||||||
_printerQueues.Remove(printerIp);
|
|
||||||
Console.WriteLine($"Printer {printerIp} unregistered");
|
Console.WriteLine($"Printer {printerIp} unregistered");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,15 +73,22 @@ public class PrinterQueue
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Call the actual print function
|
// Call the actual print function
|
||||||
bool success = await _printService.PrintAsync(PrinterIp, job);
|
var result = await _printService.PrintAsync(PrinterIp, job);
|
||||||
|
|
||||||
if (!success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
// Re-queue with retry logic
|
if (result.ErrorType == PrintErrorType.ConversionError)
|
||||||
job.RetryCount++;
|
{
|
||||||
_logger.LogWarning("Job failed, retrying ({RetryCount}/3)", job.RetryCount);
|
_logger.LogWarning("Job failed due to conversion error, not re-queuing");
|
||||||
await Task.Delay(5000, cancellationToken); // Wait before retry
|
}
|
||||||
EnqueuePriority(job);
|
else
|
||||||
|
{
|
||||||
|
// Re-queue with retry logic
|
||||||
|
job.RetryCount++;
|
||||||
|
_logger.LogWarning("Job failed, retrying ({RetryCount}/3)", job.RetryCount);
|
||||||
|
await Task.Delay(5000, cancellationToken); // Wait before retry
|
||||||
|
EnqueuePriority(job);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
454
JUNIOR_ENGINEER_TASKS.md
Normal file
454
JUNIOR_ENGINEER_TASKS.md
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
# Junior Engineer Task List - Code Improvements
|
||||||
|
|
||||||
|
This document contains actionable tasks derived from the code analysis in `plan.md`. Complete tasks in priority order. After each fix, run:
|
||||||
|
```bash
|
||||||
|
dotnet build Inspectron.Epson.slnx
|
||||||
|
dotnet test EpsonTest/EpsonTest.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRITICAL PRIORITY (Fix First)
|
||||||
|
|
||||||
|
### Task 1: Move API Key to Environment Variable
|
||||||
|
**File:** `EpsonPrintService/config.json:4`
|
||||||
|
|
||||||
|
**Problem:** API key stored in plaintext - security risk.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Remove `ApiKey` from `config.json`
|
||||||
|
2. In `EpsonPrintService/Program.cs`, read from environment:
|
||||||
|
```csharp
|
||||||
|
var apiKey = Environment.GetEnvironmentVariable("EPSON_API_KEY")
|
||||||
|
?? throw new InvalidOperationException("EPSON_API_KEY environment variable not set");
|
||||||
|
```
|
||||||
|
3. Update `EpsonPrintServiceConfiguration` class to accept apiKey via constructor or property
|
||||||
|
4. Add to systemd service file `epson.service`:
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
Environment="EPSON_API_KEY=your-key-here"
|
||||||
|
```
|
||||||
|
5. Document the required environment variable in README
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Fix HttpClient Anti-Pattern
|
||||||
|
**Files:**
|
||||||
|
- `EpsonPrintService/HeartbeatBackgroundTask.cs:24`
|
||||||
|
- `Inspectron.Epson/PrintServer/PrintServices/EpsonPrintService.cs:68`
|
||||||
|
|
||||||
|
**Problem:** Creating new HttpClient per request causes socket exhaustion.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. In `HeartbeatBackgroundTask.cs`:
|
||||||
|
- Change constructor to accept `HttpClient` as parameter
|
||||||
|
- Remove `_httpClient = new HttpClient();` line
|
||||||
|
- Keep using `_httpClient` field
|
||||||
|
|
||||||
|
2. In `Program.cs` where HeartbeatBackgroundTask is created:
|
||||||
|
```csharp
|
||||||
|
// Create ONE HttpClient instance for the app lifetime
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
var heartbeatTask = new HeartbeatBackgroundTask(httpClient, ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In `EpsonPrintService.cs:68`:
|
||||||
|
- Add `HttpClient _httpClient` field to class
|
||||||
|
- Inject via constructor
|
||||||
|
- Replace `using var httpClient = new HttpClient();` with `_httpClient`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add Graceful Shutdown
|
||||||
|
**File:** `EpsonPrintService/Program.cs:51-82`
|
||||||
|
|
||||||
|
**Problem:** PrintLoop and PrintServer never call StopAsync() - jobs lost on Ctrl+C.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Store references to started services:
|
||||||
|
```csharp
|
||||||
|
var printLoop = kernel.Get<PrintLoop>();
|
||||||
|
var printServer = kernel.Get<PrintServer>();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In the Console.CancelKeyPress handler (or before Environment.Exit):
|
||||||
|
```csharp
|
||||||
|
Console.CancelKeyPress += async (sender, e) =>
|
||||||
|
{
|
||||||
|
e.Cancel = true; // Prevent immediate exit
|
||||||
|
Console.WriteLine("Shutting down gracefully...");
|
||||||
|
|
||||||
|
await printLoop.StopAsync();
|
||||||
|
await printServer.StopAsync();
|
||||||
|
|
||||||
|
cts.Cancel();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ensure `PrintLoop` and `PrintServer` have `StopAsync()` methods that properly clean up
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Add Null Check for PrinterId
|
||||||
|
**File:** `Inspectron.Epson/PrintServer/PrintServices/EpsonPrintService.cs:35-44`
|
||||||
|
|
||||||
|
**Problem:** `GetPrinterIdAsync()` can return null but `.Value` accessed without check.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Find this code block:
|
||||||
|
```csharp
|
||||||
|
var printerId = await epsonPrinter.GetPrinterIdAsync();
|
||||||
|
var printerAdapter = _wrapperPrinterFactory.CreatePrinterFromId(printerId.Value, ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add null check:
|
||||||
|
```csharp
|
||||||
|
var printerId = await epsonPrinter.GetPrinterIdAsync();
|
||||||
|
if (printerId == null || !printerId.HasValue)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to get printer ID from {PrinterIp}", printerIp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var printerAdapter = _wrapperPrinterFactory.CreatePrinterFromId(printerId.Value, ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Fix Race Condition in PrintServer
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintServer.cs:19-23`
|
||||||
|
|
||||||
|
**Problem:** TOCTOU bug - two threads could create duplicate queues.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Change dictionary type from `Dictionary<string, PrinterQueue>` to:
|
||||||
|
```csharp
|
||||||
|
private readonly ConcurrentDictionary<string, PrinterQueue> _printerQueues = new();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Replace this pattern:
|
||||||
|
```csharp
|
||||||
|
if (_printerQueues.ContainsKey(printerIp))
|
||||||
|
return;
|
||||||
|
var queue = new PrinterQueue(...);
|
||||||
|
_printerQueues[printerIp] = queue;
|
||||||
|
```
|
||||||
|
|
||||||
|
With atomic operation:
|
||||||
|
```csharp
|
||||||
|
var queue = _printerQueues.GetOrAdd(printerIp, ip =>
|
||||||
|
{
|
||||||
|
var newQueue = new PrinterQueue(ip, _printService, _logger);
|
||||||
|
newQueue.StartAsync(); // Start the queue
|
||||||
|
Console.WriteLine($"Printer {ip} registered");
|
||||||
|
return newQueue;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add `using System.Collections.Concurrent;` at top of file
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HIGH PRIORITY
|
||||||
|
|
||||||
|
### Task 6: Add CancellationToken Support
|
||||||
|
**File:** `Inspectron.Epson/EpsonPrinter.cs` - Lines 52, 96, 129, 205, 335, 408, etc.
|
||||||
|
|
||||||
|
**Problem:** Async methods can't be cancelled by callers.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Find all public async methods in EpsonPrinter.cs
|
||||||
|
2. Add optional parameter to each:
|
||||||
|
```csharp
|
||||||
|
// Before:
|
||||||
|
public async Task<bool> ConnectAsync(string host, int port = 9100)
|
||||||
|
|
||||||
|
// After:
|
||||||
|
public async Task<bool> ConnectAsync(string host, int port = 9100, CancellationToken cancellationToken = default)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Pass cancellationToken to internal async calls:
|
||||||
|
```csharp
|
||||||
|
await _stream.WriteAsync(data, cancellationToken);
|
||||||
|
await _stream.ReadAsync(buffer, cancellationToken);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Update interface `IEpsonPrinter` if one exists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Add Exception Handling to Fire-and-Forget Tasks
|
||||||
|
**Files:**
|
||||||
|
- `Inspectron.Epson/PrintServer/PrintLoop.cs:29`
|
||||||
|
- `EpsonPrintService/Program.cs:71-72`
|
||||||
|
|
||||||
|
**Problem:** Unobserved exceptions crash the process.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. In `PrintLoop.cs`, replace:
|
||||||
|
```csharp
|
||||||
|
_ = Task.Run(Loop);
|
||||||
|
```
|
||||||
|
With:
|
||||||
|
```csharp
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Loop();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "PrintLoop crashed unexpectedly");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In `Program.cs`, replace:
|
||||||
|
```csharp
|
||||||
|
_ = discoveryTask.StartAsync(cts.Token);
|
||||||
|
_ = heartbeatTask.StartAsync(cts.Token);
|
||||||
|
```
|
||||||
|
With:
|
||||||
|
```csharp
|
||||||
|
var discoveryRunning = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try { await discoveryTask.StartAsync(cts.Token); }
|
||||||
|
catch (Exception ex) { Console.WriteLine($"Discovery error: {ex.Message}"); }
|
||||||
|
});
|
||||||
|
|
||||||
|
var heartbeatRunning = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try { await heartbeatTask.StartAsync(cts.Token); }
|
||||||
|
catch (Exception ex) { Console.WriteLine($"Heartbeat error: {ex.Message}"); }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Implement IDisposable for Resource Cleanup
|
||||||
|
**Files:**
|
||||||
|
- `EpsonPrintService/HeartbeatBackgroundTask.cs`
|
||||||
|
- `Inspectron.Epson/Queue/PrinterQueue.cs:13-14`
|
||||||
|
- `Inspectron.Epson/EpsonPrinter.cs:757-761`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **HeartbeatBackgroundTask.cs** - Add IDisposable:
|
||||||
|
```csharp
|
||||||
|
public class HeartbeatBackgroundTask : IDisposable
|
||||||
|
{
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_httpClient?.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **PrinterQueue.cs** - Add IDisposable:
|
||||||
|
```csharp
|
||||||
|
public class PrinterQueue : IDisposable
|
||||||
|
{
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_semaphore?.Dispose();
|
||||||
|
_cts?.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **EpsonPrinter.cs** - Fix DisposeAsync:
|
||||||
|
```csharp
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Replace .Wait() with await
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintServer.cs:32`
|
||||||
|
|
||||||
|
**Problem:** `.Wait()` can cause deadlocks.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Find method containing:
|
||||||
|
```csharp
|
||||||
|
queue.StopAsync().Wait();
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make the method async:
|
||||||
|
```csharp
|
||||||
|
// Before:
|
||||||
|
public void StopQueue(string printerIp)
|
||||||
|
|
||||||
|
// After:
|
||||||
|
public async Task StopQueueAsync(string printerIp)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Replace `.Wait()` with `await`:
|
||||||
|
```csharp
|
||||||
|
await queue.StopAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Update all callers to use `await StopQueueAsync()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: Add Configuration Validation at Startup
|
||||||
|
**File:** `EpsonPrintService/Program.cs:20-21`
|
||||||
|
|
||||||
|
**Problem:** No validation after deserializing config.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. After config deserialization, add validation:
|
||||||
|
```csharp
|
||||||
|
var config = JsonSerializer.Deserialize<EpsonPrintServiceConfiguration>(configJson);
|
||||||
|
|
||||||
|
if (config == null)
|
||||||
|
throw new InvalidOperationException("Failed to parse config.json");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(config.GroupId))
|
||||||
|
throw new InvalidOperationException("GroupId is required in config.json");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(config.RestaurantId))
|
||||||
|
throw new InvalidOperationException("RestaurantId is required in config.json");
|
||||||
|
|
||||||
|
if (config.PrinterConfigurations == null || config.PrinterConfigurations.Count == 0)
|
||||||
|
Console.WriteLine("Warning: No printer configurations found");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MEDIUM PRIORITY
|
||||||
|
|
||||||
|
### Task 11: Replace Console.WriteLine with ILogger
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintServer.cs:25, 34, 64`
|
||||||
|
|
||||||
|
Replace all `Console.WriteLine()` calls with proper logging:
|
||||||
|
```csharp
|
||||||
|
// Before:
|
||||||
|
Console.WriteLine($"Printer {printerIp} registered");
|
||||||
|
|
||||||
|
// After:
|
||||||
|
_logger.LogInformation("Printer {PrinterIp} registered", printerIp);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 12: Fix Logger Factory Singleton
|
||||||
|
**File:** `EpsonPrintService/Program.cs:33-40`
|
||||||
|
|
||||||
|
Create factory once:
|
||||||
|
```csharp
|
||||||
|
// Create once at startup
|
||||||
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
||||||
|
{
|
||||||
|
builder.AddConsole();
|
||||||
|
builder.SetMinimumLevel(LogLevel.Debug);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind as singleton for all injections
|
||||||
|
kernel.Bind<ILoggerFactory>().ToConstant(loggerFactory);
|
||||||
|
kernel.Bind(typeof(ILogger<>)).ToMethod(ctx =>
|
||||||
|
loggerFactory.CreateLogger(ctx.Request.Service.GetGenericArguments()[0]));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 13: Extract Magic Numbers to Constants
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrinterQueue.cs:87-88`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Add at top of class:
|
||||||
|
private const int MaxRetryCount = 3;
|
||||||
|
|
||||||
|
// Replace:
|
||||||
|
if (job.RetryCount >= 3)
|
||||||
|
// With:
|
||||||
|
if (job.RetryCount >= MaxRetryCount)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 14: Fix Static Thread Safety
|
||||||
|
**File:** `Inspectron.Epson/EpsonPrinter.cs:38-43`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly object _initLock = new();
|
||||||
|
private static bool _initialized;
|
||||||
|
|
||||||
|
private static void RegisterCodepages()
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
|
||||||
|
lock (_initLock)
|
||||||
|
{
|
||||||
|
if (_initialized) return; // Double-check pattern
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LOW PRIORITY
|
||||||
|
|
||||||
|
### Task 15: Remove Commented-Out Code
|
||||||
|
- `EpsonPrintService/Program.cs:27-28` - Delete old factory bindings
|
||||||
|
- `Inspectron.Epson/PrintServer/Printers/TM-T30III.cs:25` - Delete dead code
|
||||||
|
|
||||||
|
### Task 16: Resolve TODO Comments
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintJob.cs:5`
|
||||||
|
- Either change `IP` property to proper type or remove the TODO comment
|
||||||
|
|
||||||
|
### Task 17: Remove Unused Serilog Packages
|
||||||
|
**File:** `EpsonPrintService/EpsonPrintService.csproj:14-16`
|
||||||
|
```bash
|
||||||
|
dotnet remove EpsonPrintService/EpsonPrintService.csproj package Serilog
|
||||||
|
dotnet remove EpsonPrintService/EpsonPrintService.csproj package Serilog.Extensions.Logging
|
||||||
|
dotnet remove EpsonPrintService/EpsonPrintService.csproj package Serilog.Sinks.Console
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 18: Fix Inconsistent Logging Style
|
||||||
|
Change string interpolation to structured logging:
|
||||||
|
```csharp
|
||||||
|
// Before:
|
||||||
|
_logger.LogWarning($"Printer {printerIp} not found");
|
||||||
|
|
||||||
|
// After:
|
||||||
|
_logger.LogWarning("Printer {PrinterIp} not found", printerIp);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
After completing all tasks:
|
||||||
|
|
||||||
|
- [ ] `dotnet build Inspectron.Epson.slnx` - No errors
|
||||||
|
- [ ] `dotnet test EpsonTest/EpsonTest.csproj` - All tests pass
|
||||||
|
- [ ] Test printer connectivity manually
|
||||||
|
- [ ] Verify systemd service starts/stops cleanly
|
||||||
|
- [ ] Verify graceful shutdown with Ctrl+C
|
||||||
|
- [ ] Check no Console.WriteLine in library code (only logging)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you're unsure about any task:
|
||||||
|
1. Read the original issue in `plan.md` for more context
|
||||||
|
2. Look at existing patterns in the codebase
|
||||||
|
3. Ask a senior engineer before making architectural changes
|
||||||
262
plan.md
Normal file
262
plan.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# Code Improvement Analysis: EpsonPrintService & Inspectron.Epson
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Analysis of both projects revealed **50+ distinct issues** across security, reliability, performance, and maintainability. The most critical issues involve resource management, thread safety, and missing cancellation support.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Issues (Fix Immediately)
|
||||||
|
|
||||||
|
### 1. Security: Plaintext API Key
|
||||||
|
**File:** `EpsonPrintService/config.json:4`
|
||||||
|
```json
|
||||||
|
"ApiKey": "Pd8X/VtXLWgl5Djy4ksjdHC3xboeoh5Jig3Qo4277GQ="
|
||||||
|
```
|
||||||
|
- API key stored in plaintext JSON file
|
||||||
|
- **Fix:** Move to environment variables or secure configuration provider
|
||||||
|
|
||||||
|
### 2. HttpClient Anti-Pattern (Socket Exhaustion Risk)
|
||||||
|
**File:** `EpsonPrintService/HeartbeatBackgroundTask.cs:24`
|
||||||
|
```csharp
|
||||||
|
_httpClient = new HttpClient(); // Created per instance
|
||||||
|
```
|
||||||
|
**File:** `Inspectron.Epson/PrintServer/PrintServices/EpsonPrintService.cs:68`
|
||||||
|
```csharp
|
||||||
|
using var httpClient = new HttpClient(); // Created per request
|
||||||
|
```
|
||||||
|
- **Fix:** Inject singleton HttpClient or use IHttpClientFactory
|
||||||
|
|
||||||
|
### 3. Missing Graceful Shutdown
|
||||||
|
**File:** `EpsonPrintService/Program.cs:51-82`
|
||||||
|
- `PrintLoop` and `PrintServer` never call `StopAsync()` on exit
|
||||||
|
- Queued print jobs are lost on Ctrl+C
|
||||||
|
- **Fix:** Store references and call shutdown methods before exiting
|
||||||
|
|
||||||
|
### 4. NullReferenceException Risk
|
||||||
|
**File:** `Inspectron.Epson/PrintServer/PrintServices/EpsonPrintService.cs:35-44`
|
||||||
|
```csharp
|
||||||
|
var printerId = await epsonPrinter.GetPrinterIdAsync();
|
||||||
|
var printerAdapter = _wrapperPrinterFactory.CreatePrinterFromId(printerId.Value, ...);
|
||||||
|
```
|
||||||
|
- `GetPrinterIdAsync()` can return null but `.Value` is called without check
|
||||||
|
- **Fix:** Add null check before accessing `.Value`
|
||||||
|
|
||||||
|
### 5. Thread Safety: Race Condition in PrintServer
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintServer.cs:19-23`
|
||||||
|
```csharp
|
||||||
|
if (_printerQueues.ContainsKey(printerIp))
|
||||||
|
return;
|
||||||
|
var queue = new PrinterQueue(...);
|
||||||
|
_printerQueues[printerIp] = queue;
|
||||||
|
```
|
||||||
|
- TOCTOU bug: two threads could create duplicate queues
|
||||||
|
- **Fix:** Use `ConcurrentDictionary.GetOrAdd()` or `TryAdd()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High Priority Issues
|
||||||
|
|
||||||
|
### 6. Missing CancellationToken Support
|
||||||
|
**File:** `Inspectron.Epson/EpsonPrinter.cs` - Lines 52, 96, 129, 205, 335, 408, etc.
|
||||||
|
- Core async methods don't accept CancellationToken
|
||||||
|
- Callers cannot cancel long-running operations
|
||||||
|
- **Fix:** Add optional `CancellationToken cancellationToken = default` to all async methods
|
||||||
|
|
||||||
|
### 7. Fire-and-Forget Tasks Without Exception Handling
|
||||||
|
**File:** `Inspectron.Epson/PrintServer/PrintLoop.cs:29`
|
||||||
|
```csharp
|
||||||
|
_= Task.Run(Loop); // No exception handling
|
||||||
|
```
|
||||||
|
**File:** `EpsonPrintService/Program.cs:71-72`
|
||||||
|
```csharp
|
||||||
|
_ = discoveryTask.StartAsync(cts.Token);
|
||||||
|
_ = heartbeatTask.StartAsync(cts.Token);
|
||||||
|
```
|
||||||
|
- Unobserved exceptions will terminate the process
|
||||||
|
- **Fix:** Use exception handler or track tasks for proper completion
|
||||||
|
|
||||||
|
### 8. Resource Leaks (IDisposable Not Implemented)
|
||||||
|
**File:** `EpsonPrintService/HeartbeatBackgroundTask.cs` - No IDisposable
|
||||||
|
- HttpClient never disposed
|
||||||
|
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrinterQueue.cs:13-14`
|
||||||
|
- SemaphoreSlim and CancellationTokenSource never disposed
|
||||||
|
|
||||||
|
**File:** `Inspectron.Epson/EpsonPrinter.cs:757-761`
|
||||||
|
```csharp
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
await Task.CompletedTask; // Missing GC.SuppressFinalize
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Fix:** Implement proper disposal pattern with try/finally
|
||||||
|
|
||||||
|
### 9. Synchronous Wait in Async Context
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintServer.cs:32`
|
||||||
|
```csharp
|
||||||
|
queue.StopAsync().Wait(); // Deadlock risk
|
||||||
|
```
|
||||||
|
- **Fix:** Use `await` or proper async pattern
|
||||||
|
|
||||||
|
### 10. Configuration Not Validated at Startup
|
||||||
|
**File:** `EpsonPrintService/Program.cs:20-21`
|
||||||
|
```csharp
|
||||||
|
var config = JsonSerializer.Deserialize<EpsonPrintServiceConfiguration>(...);
|
||||||
|
// No null check or validation
|
||||||
|
```
|
||||||
|
- **Fix:** Validate all required fields immediately after deserialization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Medium Priority Issues
|
||||||
|
|
||||||
|
### 11. Console.WriteLine in Library Code
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintServer.cs:25, 34, 64`
|
||||||
|
```csharp
|
||||||
|
Console.WriteLine($"Printer {printerIp} registered");
|
||||||
|
```
|
||||||
|
- Library code should use ILogger only
|
||||||
|
- **Fix:** Replace with `_logger.LogInformation()`
|
||||||
|
|
||||||
|
### 12. Logger Factory Created Repeatedly
|
||||||
|
**File:** `EpsonPrintService/Program.cs:33-40`
|
||||||
|
- Creates new LoggerFactory for every ILogger injection
|
||||||
|
- **Fix:** Create factory once and bind as singleton
|
||||||
|
|
||||||
|
### 13. Magic Numbers Throughout
|
||||||
|
**File:** `Inspectron.Epson/EpsonPrinter.cs:263, 273, 282, 327, 463`
|
||||||
|
- Hard-coded ESC/POS bytes scattered in code
|
||||||
|
- **Fix:** Move to `EpsonCommands` static class
|
||||||
|
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrinterQueue.cs:87-88`
|
||||||
|
```csharp
|
||||||
|
if (job.RetryCount >= 3) // Magic number
|
||||||
|
```
|
||||||
|
- **Fix:** Extract to configurable constant
|
||||||
|
|
||||||
|
### 14. SOLID Violations
|
||||||
|
|
||||||
|
**SRP Violation - EpsonPrinter.cs (763 lines)**
|
||||||
|
- Handles: connection, status, image processing, text printing, cutting
|
||||||
|
- **Fix:** Split into smaller classes (ConnectionManager, StatusProvider, ImagePrinter)
|
||||||
|
|
||||||
|
**SRP Violation - EpsonPrintServiceConfiguration**
|
||||||
|
- Implements both `IPrinterConfigurationSource` AND `IAssignedPrinterRepository`
|
||||||
|
- **Fix:** Separate into two classes
|
||||||
|
|
||||||
|
### 15. Code Duplication
|
||||||
|
|
||||||
|
**Image Conversion:**
|
||||||
|
- `ConvertToRasterData()` and `ConvertToColumnFormat()` share 50%+ logic
|
||||||
|
- **Fix:** Extract common image processing pipeline
|
||||||
|
|
||||||
|
**Image Loading:**
|
||||||
|
- Same file loading pattern repeated 4 times in EpsonPrinter.cs
|
||||||
|
- **Fix:** Create private helper method
|
||||||
|
|
||||||
|
### 16. Performance Issue in Image Conversion
|
||||||
|
**File:** `Inspectron.Epson/EpsonImageConverter.cs:243-247`
|
||||||
|
```csharp
|
||||||
|
image.ProcessPixelRows(accessor => { ... }); // Called per pixel!
|
||||||
|
```
|
||||||
|
- O(n²) performance instead of O(n)
|
||||||
|
- **Fix:** Call ProcessPixelRows once and cache accessor
|
||||||
|
|
||||||
|
### 17. Missing Input Validation
|
||||||
|
- `ConnectAsync()` doesn't validate IP format
|
||||||
|
- `PrintTextAsync()` doesn't validate text encoding
|
||||||
|
- `FeedLinesAsync()` doesn't validate line count > 0
|
||||||
|
- **Fix:** Add guard clauses with ArgumentException
|
||||||
|
|
||||||
|
### 18. Static State Thread Safety
|
||||||
|
**File:** `Inspectron.Epson/EpsonPrinter.cs:38-43`
|
||||||
|
```csharp
|
||||||
|
private static bool _initialized;
|
||||||
|
private static void RegisterCodepages()
|
||||||
|
{
|
||||||
|
if (_initialized) return; // No lock!
|
||||||
|
Encoding.RegisterProvider(...);
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Fix:** Use lock or `Interlocked.CompareExchange()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Low Priority Issues
|
||||||
|
|
||||||
|
### 19. Commented-Out Code
|
||||||
|
- `EpsonPrintService/Program.cs:27-28` - Old factory bindings
|
||||||
|
- `Inspectron.Epson/PrintServer/Printers/TM-T30III.cs:25` - Dead code
|
||||||
|
- **Fix:** Remove or track in issue tracker
|
||||||
|
|
||||||
|
### 20. TODO Comments Left in Code
|
||||||
|
**File:** `Inspectron.Epson/Queue/PrintJob.cs:5`
|
||||||
|
```csharp
|
||||||
|
// todo: change to ip address
|
||||||
|
public string IP { get; set; }
|
||||||
|
```
|
||||||
|
- **Fix:** Resolve or create proper issue
|
||||||
|
|
||||||
|
### 21. Unused Serilog Dependencies
|
||||||
|
**File:** `EpsonPrintService/EpsonPrintService.csproj:14-16`
|
||||||
|
- Serilog packages referenced but not used
|
||||||
|
- **Fix:** Remove unused packages
|
||||||
|
|
||||||
|
### 22. Inconsistent Logging
|
||||||
|
- String interpolation used instead of structured logging placeholders
|
||||||
|
- Log levels inconsistent (LogWarning for config errors that should fail startup)
|
||||||
|
- **Fix:** Use `LogWarning("Printer {PrinterId} not found", printerIp)` pattern
|
||||||
|
|
||||||
|
### 23. API Design Inconsistencies
|
||||||
|
- `PrintTextAsync()` vs `PrintTextAndCutAsync()` naming
|
||||||
|
- 3 overloads of `PrintImageBitModeAsync()` and `LoadImageAsync()`
|
||||||
|
- Interface doesn't match all implementation overloads
|
||||||
|
- **Fix:** Rationalize method naming and signatures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested Refactoring Projects
|
||||||
|
|
||||||
|
### Project 1: Resource Management Overhaul
|
||||||
|
- Implement IAsyncDisposable properly across all classes
|
||||||
|
- Fix all HttpClient instantiation
|
||||||
|
- Add proper cleanup on shutdown
|
||||||
|
- **Scope:** ~10 files, ~200 lines changed
|
||||||
|
|
||||||
|
### Project 2: Async Pattern Modernization
|
||||||
|
- Add CancellationToken to all async methods
|
||||||
|
- Fix fire-and-forget patterns
|
||||||
|
- Replace synchronous waits
|
||||||
|
- **Scope:** ~8 files, ~150 lines changed
|
||||||
|
|
||||||
|
### Project 3: Thread Safety Fixes
|
||||||
|
- Fix race conditions in PrintServer and PrinterQueue
|
||||||
|
- Add proper locking for static initialization
|
||||||
|
- **Scope:** ~4 files, ~50 lines changed
|
||||||
|
|
||||||
|
### Project 4: Split EpsonPrinter Class
|
||||||
|
- Extract ConnectionManager
|
||||||
|
- Extract StatusProvider
|
||||||
|
- Extract ImagePrinter
|
||||||
|
- Keep EpsonPrinter as facade
|
||||||
|
- **Scope:** ~5 new files, ~800 lines reorganized
|
||||||
|
|
||||||
|
### Project 5: Configuration & DI Cleanup
|
||||||
|
- Add configuration validation
|
||||||
|
- Fix Logger factory singleton
|
||||||
|
- Separate configuration concerns
|
||||||
|
- Move secrets to environment variables
|
||||||
|
- **Scope:** ~5 files, ~100 lines changed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After implementing any changes:
|
||||||
|
1. Build the solution: `dotnet build Inspectron.Epson.slnx`
|
||||||
|
2. Run existing tests: `dotnet test EpsonTest/EpsonTest.csproj`
|
||||||
|
3. Test printer connectivity manually with EpsonTest project
|
||||||
|
4. Verify systemd service starts/stops cleanly
|
||||||
Reference in New Issue
Block a user