Files
Print_server/JUNIOR_ENGINEER_TASKS.md
2026-01-21 10:37:33 +01:00

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