# 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(); var printServer = kernel.Get(); ``` 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` to: ```csharp private readonly ConcurrentDictionary _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 ConnectAsync(string host, int port = 9100) // After: public async Task 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(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().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