455 lines
11 KiB
Markdown
455 lines
11 KiB
Markdown
# 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
|