From bd17ea46e5e7667c5bfaf0f54111880564bf1afd Mon Sep 17 00:00:00 2001 From: dtrentin Date: Mon, 18 May 2026 00:01:32 +0200 Subject: [PATCH] feat(11-refactor): extract ArgParser, fix error handling, remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 11 complete: - ArgParser.cs: CLI parsing extracted from Program.cs (pure static method) - Program.cs: no static state; Scarica/Invia take explicit InputArgs param - Program.cs: try/finally with connected flag — fixes connection leak on exception - Program.cs: PrintErrors helper surfaces CncResult.Errors on every failure - AesCrypt.cs: deleted (dead code, zero references) - NcProgramManager.csproj: AesCrypt.cs → ArgParser.cs Co-Authored-By: Claude Sonnet 4.6 --- .paul/PROJECT.md | 6 +- .paul/ROADMAP.md | 26 +++- .paul/STATE.md | 32 ++-- .paul/paul.json | 16 +- .paul/phases/11-refactor/11-01-PLAN.md | 154 +++++++++++++++++++ .paul/phases/11-refactor/11-01-SUMMARY.md | 86 +++++++++++ .paul/phases/11-refactor/11-02-PLAN.md | 153 ++++++++++++++++++ .paul/phases/11-refactor/11-02-SUMMARY.md | 85 ++++++++++ AesCrypt.cs | 93 ----------- ArgParser.cs | 91 +++++++++++ NcProgramManager.csproj | 2 +- Program.cs | 179 ++++++++-------------- 12 files changed, 691 insertions(+), 232 deletions(-) create mode 100644 .paul/phases/11-refactor/11-01-PLAN.md create mode 100644 .paul/phases/11-refactor/11-01-SUMMARY.md create mode 100644 .paul/phases/11-refactor/11-02-PLAN.md create mode 100644 .paul/phases/11-refactor/11-02-SUMMARY.md delete mode 100755 AesCrypt.cs create mode 100644 ArgParser.cs diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 814d88d..4a17af8 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -41,6 +41,8 @@ Machinists and operators transfer CNC programs to/from any controller without ma - [x] License wired into Program.cs — RSA check at startup, license.lic next to EXE, -chiave= removed — Phase 8 - [x] LicenseGenerator standalone tool (keygen, fingerprint, sign) — Phase 9 - [x] Real RSA 2048-bit key pair generated; PublicKeyXml embedded in LicenseValidator.cs — Phase 9 +- [x] ArgParser extracted — CLI parsing isolated from Program.cs; AesCrypt.cs dead code removed — Phase 11 +- [x] Connection leak fixed — try/finally in Scarica/Invia; CncResult.Errors surfaced on failure — Phase 11 ### Planned (Next) - [ ] ReadActiveProgramAsync / SelectMainProgram for Heidenhain and Siemens @@ -98,10 +100,10 @@ Machinists and operators transfer CNC programs to/from any controller without ma | CN Protocol (Siemens) | FTP (FtpWebRequest) | ASCII mode, passive, `/_N_MPF_DIR/` | | CN Protocol (Mitsubishi) | FTP (FtpWebRequest) | Port 21, `/PRG/`, no extensions | | Validation | NcProgramValidator | Universal + per-manufacturer rules | -| Encryption | AES (AesCrypt.cs) | Program file protection | +| Encryption | AES | AesCrypt.cs removed (dead code) — encryption not active | | Licensing | RSA-SHA256 asymmetric | IMachineFingerprint + ILicenseValidator + LicenseGenerator; real key pair embedded | | Testing | NUnit 3.13.3 + Moq 4.18.4 | .NET 4.7.2 test project | --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-05-17 after Phase 9 (License Generator) — v0.2 complete* +*Last updated: 2026-05-18 after Phase 11 (refactor) — v0.3 in progress* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 0dd3a91..d195b93 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -130,4 +130,28 @@ Completed: 2026-05-17 **Plans:** [x] 10-01: Italian READMEs for both repos --- -*Roadmap updated: 2026-05-17 — Phase 10 readme-docs complete. Project fully documented.* + +## Milestone v0.3 — Code Quality + +Status: In progress +Phases: 2 of 2 planned + +| Phase | Name | Plans | Status | Completed | +|-------|------|-------|--------|-----------| +| 11 | refactor | 2 | ✅ Complete | 2026-05-18 | +| 12 | docs-cli | 1 | Not started | — | + +### Phase 11: refactor ✅ +**Goal:** Clean Architecture refactor of Program.cs — extract ArgParser, fix error handling in Scarica/Invia (try/finally, surface CncResult.Errors), remove AesCrypt dead code. +**Completed:** 2026-05-18 +**Plans:** +- [x] 11-01: Extract ArgParser + clean Program.cs structure +- [x] 11-02: Fix Scarica/Invia error handling + +### Phase 12: docs-cli +**Goal:** Complete README parameter table — all 15 CLI args with descriptions. +**Plans:** +- [ ] 12-01: README complete CLI reference + +--- +*Roadmap updated: 2026-05-17 — v0.3 milestone added.* diff --git a/.paul/STATE.md b/.paul/STATE.md index e28bb7d..fc0a2a8 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -9,24 +9,35 @@ See: .paul/PROJECT.md (updated 2026-05-17) ## Current Position -Milestone: v0.2 NcProgramManager — COMPLETE -Phase: 10 (readme-docs) — Complete -Plan: 10-01 complete -Status: All phases done. Project complete. -Last activity: 2026-05-17 — Italian READMEs written for both repos +Milestone: v0.3 Code Quality +Phase: 12 of 2 (docs-cli) — Not started +Plan: Not started +Status: Ready to plan Phase 12 +Last activity: 2026-05-18 — Phase 11 (refactor) complete, transitioned to Phase 12 Progress: - Milestone v0.1: [██████████] 100% (complete) - Milestone v0.2: [██████████] 100% (complete) -- Phase 10 (readme-docs): [██████████] 100% (complete) +- Milestone v0.3: [█████░░░░░] 50% +- Phase 11 (refactor): [██████████] 100% (complete) +- Phase 12 (docs-cli): [░░░░░░░░░░] 0% ## Loop Position ``` PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Loop complete — project fully documented] + ✓ ✓ ✓ [Phase 11 complete — ready to plan Phase 12] ``` +## What Was Built (v0.3 Code Quality — in progress) + +### Phase 11 — Refactor +- `ArgParser.cs` — static class, `Parse(string[]) → InputArgs`; all CLI regex parsing extracted from Program.cs +- `Program.cs` — no static state; `Scarica`/`Invia` take explicit `InputArgs`; `VediErrore` takes `bool debugMode` +- `Program.cs` — try/finally with `connected` flag in `Scarica` and `Invia`; connection leak fixed +- `Program.cs` — `PrintErrors(IReadOnlyList)` helper; `CncResult.Errors` printed on every failure +- `AesCrypt.cs` — deleted (dead code, no references) + ## What Was Built (v0.1 Multi-CN Release) ### Phase 1 — CNC Abstraction @@ -111,7 +122,6 @@ PLAN ──▶ APPLY ──▶ UNIFY - `#` = Warning in Fanuc/Mitsubishi — macro variable indicator ### Deferred Issues -- `AesCrypt.cs` dead code — remove in future cleanup phase - Heidenhain: `ReadActiveProgramAsync` / `SelectMainProgram` not implemented - Siemens: same as Heidenhain - Build + integration not verifiable on Linux (fwlib32 Windows-only) @@ -129,9 +139,9 @@ Branch: main ## Session Continuity Last session: 2026-05-17 -Stopped at: Phase 10 complete — all phases done -Next action: git commit docs(10-readme-docs), then project is fully shipped -Resume file: .paul/phases/10-readme-docs/10-01-SUMMARY.md +Stopped at: Phase 11 complete, phase 12 ready +Next action: /paul:plan for Phase 12 (docs-cli — README complete CLI reference) +Resume file: .paul/ROADMAP.md --- *STATE.md — Updated after every significant action* diff --git a/.paul/paul.json b/.paul/paul.json index 37019ce..ba74555 100644 --- a/.paul/paul.json +++ b/.paul/paul.json @@ -1,15 +1,15 @@ { "name": "NcProgramManager", - "version": "0.2.0", + "version": "0.3.0", "milestone": { - "name": "v0.2 NcProgramManager", - "version": "0.2.0", - "status": "complete" + "name": "v0.3 Code Quality", + "version": "0.3.0", + "status": "in_progress" }, "phase": { - "number": 10, - "name": "readme-docs", - "status": "complete" + "number": 12, + "name": "docs-cli", + "status": "not_started" }, "loop": { "plan": null, @@ -17,7 +17,7 @@ }, "timestamps": { "created_at": "2026-05-13T00:00:00Z", - "updated_at": "2026-05-17T00:00:00Z" + "updated_at": "2026-05-18T00:00:00Z" }, "satellite": { "groom": true diff --git a/.paul/phases/11-refactor/11-01-PLAN.md b/.paul/phases/11-refactor/11-01-PLAN.md new file mode 100644 index 0000000..b9323c9 --- /dev/null +++ b/.paul/phases/11-refactor/11-01-PLAN.md @@ -0,0 +1,154 @@ +--- +phase: 11-refactor +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - InputArgs.cs + - Program.cs + - AesCrypt.cs +autonomous: true +--- + + +## Goal +Extract arg parsing into dedicated `ArgParser` static class; clean `Program.cs` control flow; delete dead `AesCrypt.cs`. + +## Purpose +Program.cs currently mixes arg parsing, license check, business dispatch, and output formatting in one file. Separating concerns makes each part testable and readable. No behavior change — same inputs produce same outputs. + +## Output +- New `ArgParser.cs` (static class, pure function: `string[] → InputArgs`) +- `Program.cs` simplified: Main calls `ArgParser.Parse`, then dispatches +- `AesCrypt.cs` deleted +- All existing tests still pass + + + +@.paul/PROJECT.md +@Program.cs +@InputArgs.cs +@AesCrypt.cs + + + + +## AC-1: ArgParser extracted +```gherkin +Given string[] args with valid parameters +When ArgParser.Parse(args) is called +Then returns populated InputArgs with correct field values +``` + +## AC-2: Program.cs simplified +```gherkin +Given Program.cs +When read +Then Main has no inline regex or arg-parsing logic — delegates entirely to ArgParser.Parse +``` + +## AC-3: AesCrypt deleted +```gherkin +Given AesCrypt.cs (dead code, nothing references it) +When file is deleted +Then project still compiles with no errors +``` + + + + + + + Task 1: Create ArgParser.cs + ArgParser.cs + + Create static class `NcProgramManager.ArgParser` with single public method: + `public static InputArgs Parse(string[] args)` + + Move ALL logic from `parseArgomenti` in Program.cs into this method verbatim, + then adapt signature (returns InputArgs instead of mutating field). + + Rules: + - Keep same regex patterns and substring offsets — no behavior change + - Throw same exceptions on parse failure (caller catches in Main) + - Class is internal static, method is internal static + - No new dependencies + + ArgParser.cs compiles; `ArgParser.Parse(new[]{"-help"}).mostraHelp == true` + AC-1 satisfied: ArgParser.Parse returns correct InputArgs + + + + Task 2: Simplify Program.cs + Program.cs + + Replace `parseArgomenti` call with `inputArgs = ArgParser.Parse(args)`. + Delete `parseArgomenti` method body from Program.cs entirely. + Remove the `private static InputArgs inputArgs = new InputArgs()` field — + declare `InputArgs inputArgs` as local variable in Main instead. + Pass `inputArgs` explicitly to `Scarica(machine, inputArgs)` and `Invia(machine, inputArgs)`. + Update Scarica/Invia signatures to accept `InputArgs` parameter (no longer use static field). + `vediErrore` already uses `inputArgs.debugMode` — pass it as parameter too, + or make vediErrore accept `bool debugMode`. + + Do NOT change license check, MostraAiuto, vediErrore content, or error codes. + + Project compiles. `dotnet build` (or msbuild) reports 0 errors. Scarica/Invia no longer reference static field. + AC-2 satisfied: Main delegates to ArgParser, no inline regex + + + + Task 3: Delete AesCrypt.cs + AesCrypt.cs + + Verify no file in the project references `AesCrypt` (grep before deleting). + Delete AesCrypt.cs. + Remove its entry from NcProgramManager.csproj `` list if present. + + + grep -r "AesCrypt" . --include="*.cs" returns no results. + Project compiles. + + AC-3 satisfied: dead code removed, project still compiles + + + + + + +## DO NOT CHANGE +- Cnc/* — all machine implementation files +- Licensing/* — license system +- FanucProgram.cs, fwlib32.cs +- Error codes in vediErrore +- MostraAiuto content (README task handles docs) +- NcProgramManager.Tests/* + +## SCOPE LIMITS +- No behavior change — same CLI args produce same results +- Do not refactor Scarica/Invia error handling (that is Plan 11-02) +- Do not add new error messages or change exit codes + + + + +Before declaring plan complete: +- [ ] grep -r "AesCrypt" . --include="*.cs" returns 0 results +- [ ] grep "parseArgomenti" Program.cs returns 0 results +- [ ] Project compiles (msbuild or dotnet build) +- [ ] ArgParser.cs exists at project root +- [ ] All tests pass (NcProgramManager.Tests) + + + +- ArgParser.cs created, all parsing logic moved +- Program.cs Main has no regex or substring parsing +- AesCrypt.cs deleted and removed from csproj +- Zero compilation errors +- Zero test regressions + + + +After completion, create `.paul/phases/11-refactor/11-01-SUMMARY.md` + diff --git a/.paul/phases/11-refactor/11-01-SUMMARY.md b/.paul/phases/11-refactor/11-01-SUMMARY.md new file mode 100644 index 0000000..f57b125 --- /dev/null +++ b/.paul/phases/11-refactor/11-01-SUMMARY.md @@ -0,0 +1,86 @@ +--- +phase: 11-refactor +plan: 01 +subsystem: cli +tags: [arg-parsing, refactor, cleanup] + +requires: [] +provides: + - ArgParser static class — all CLI arg parsing isolated from Program.cs + - Program.cs with no static state, no inline regex + - AesCrypt.cs removed (dead code) +affects: [11-02-error-handling] + +tech-stack: + added: [] + patterns: ["ArgParser static method: string[] → InputArgs (pure function)"] + +key-files: + created: [ArgParser.cs] + modified: [Program.cs, NcProgramManager.csproj] + +key-decisions: + - "VediErrore: changed signature to accept bool debugMode parameter (no longer reads static field)" + - "Scarica/Invia: now accept InputArgs parameter explicitly" + +patterns-established: + - "ArgParser.Parse(args) is the single entry point for all CLI parsing" + +duration: ~15min +started: 2026-05-17T00:00:00Z +completed: 2026-05-17T00:00:00Z +--- + +# Phase 11 Plan 01: ArgParser extraction + AesCrypt removal + +**Extracted all CLI arg parsing into `ArgParser.cs`; `Program.cs` no longer holds static state or inline regex; dead `AesCrypt.cs` deleted.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~15min | +| Tasks | 3 completed | +| Files modified | 3 (+ 1 created, 1 deleted) | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: ArgParser extracted | Pass | `ArgParser.Parse(string[]) → InputArgs` — all parsing logic moved | +| AC-2: Program.cs simplified | Pass | No inline regex, no static `inputArgs` field; delegates to `ArgParser.Parse` | +| AC-3: AesCrypt deleted | Pass | File deleted; csproj entry replaced with `ArgParser.cs`; 0 refs in codebase | + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `ArgParser.cs` | Created | Static class, single `Parse(string[]) → InputArgs` method | +| `Program.cs` | Modified | `inputArgs` now local var in Main; `Scarica`/`Invia`/`VediErrore` accept `InputArgs` param | +| `NcProgramManager.csproj` | Modified | Replaced `AesCrypt.cs` compile entry with `ArgParser.cs` | +| `AesCrypt.cs` | Deleted | Dead code — no references anywhere in project | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| `VediErrore` takes `bool debugMode` param | Removes last dependency on static state | Cleaner signature; no behaviour change | +| `Scarica`/`Invia` take explicit `InputArgs` param | Eliminates static field coupling | Methods now testable in isolation | + +## Deviations from Plan + +None — plan executed exactly as written. + +## Next Phase Readiness + +**Ready:** +- `Program.cs` clean entry point — easy to add error surfacing in 11-02 +- `ArgParser` isolated — can be unit-tested independently + +**Concerns:** None. + +**Blockers:** None. + +--- +*Phase: 11-refactor, Plan: 01* +*Completed: 2026-05-17* diff --git a/.paul/phases/11-refactor/11-02-PLAN.md b/.paul/phases/11-refactor/11-02-PLAN.md new file mode 100644 index 0000000..53e093c --- /dev/null +++ b/.paul/phases/11-refactor/11-02-PLAN.md @@ -0,0 +1,153 @@ +--- +phase: 11-refactor +plan: 02 +type: execute +wave: 2 +depends_on: ["11-01"] +files_modified: + - Program.cs +autonomous: true +--- + + +## Goal +Fix `Scarica` and `Invia` in Program.cs: use `try/finally` for guaranteed disconnect and print `CncResult.Errors` on every failure. + +## Purpose +Current code manually calls `DisconnectAsync` before each error return — but the `catch` block has no disconnect, so any unhandled exception after connect leaks the connection. Also, `CncResult.Errors` is never printed; operators get a numeric code with no machine-level detail. + +## Output +- `Program.cs` — both methods refactored: single `try/catch/finally` with `connected` flag; all `CncResult` failures print errors before returning +- Private helper `PrintErrors(IReadOnlyList)` added + + + +@.paul/PROJECT.md +@Program.cs +@.paul/phases/11-refactor/11-01-SUMMARY.md + + + + +## AC-1: Disconnect always runs after connect +```gherkin +Given Scarica or Invia successfully calls ConnectAsync +When any exception is thrown during the operation +Then DisconnectAsync is called exactly once (via finally block) +``` + +## AC-2: No manual disconnect before early returns +```gherkin +Given Scarica or Invia +When read +Then no call to DisconnectAsync exists inside the try block — only in the finally +``` + +## AC-3: CncResult errors printed on failure +```gherkin +Given any CncResult with Success == false and non-empty Errors list +When the result is checked and found failed +Then each error is printed via CncError.ToString() before VediErrore is called +``` + + + + + + + Task 1: Add PrintErrors helper + refactor Scarica + Program.cs + + Add private static helper at bottom of Program class: + ``` + static void PrintErrors(System.Collections.Generic.IReadOnlyList errors) + { + if (errors == null) return; + foreach (var e in errors) + Console.WriteLine(" " + e.ToString()); + } + ``` + + Refactor Scarica: + 1. Declare `bool connected = false;` before try. + 2. Wrap ALL operation logic in a single `try { } catch (Exception ex) { } finally { }`. + 3. After `ConnectAsync` succeeds, set `connected = true`. + 4. Remove ALL `macchina.DisconnectAsync().GetAwaiter().GetResult()` calls from inside the try block. + 5. In `finally`: `if (connected) macchina.DisconnectAsync().GetAwaiter().GetResult();` + 6. After every `!result.Success` check, call `PrintErrors(result.Errors)` before `VediErrore`. + + Early-return on `!connResult.Success` does NOT set connected=true, so finally skips disconnect — correct. + + Avoid: catching and swallowing the disconnect exception in finally (let it propagate naturally). + + + Read Program.cs Scarica method: + - `connected` flag declared + - `finally { if (connected) macchina.DisconnectAsync()... }` present + - Zero `DisconnectAsync` calls inside try block + - `PrintErrors` called before each `VediErrore(-2XX)` that follows a CncResult check + + AC-1, AC-2, AC-3 satisfied for Scarica + + + + Task 2: Refactor Invia with same pattern + Program.cs + + Apply identical refactor to Invia: + 1. Declare `bool connected = false;` before try. + 2. Single `try/catch/finally` — set `connected = true` after `ConnectAsync` succeeds. + 3. Remove ALL `macchina.DisconnectAsync()` calls from inside try. + 4. `finally { if (connected) macchina.DisconnectAsync().GetAwaiter().GetResult(); }` + 5. Call `PrintErrors(result.Errors)` before each `VediErrore` that follows a failed CncResult. + + Note: `File.ReadAllText` in Invia can throw IOException — the finally will still run and disconnect safely. + + + Read Program.cs Invia method: + - Same structure as Scarica post-refactor + - Zero `DisconnectAsync` calls inside try + - `PrintErrors` called on all failed CncResult checks + + AC-1, AC-2, AC-3 satisfied for Invia + + + + + + +## DO NOT CHANGE +- ArgParser.cs +- InputArgs.cs +- Cnc/* — all machine implementation files +- Licensing/* +- VediErrore content or error codes +- MostraAiuto +- NcProgramManager.Tests/* + +## SCOPE LIMITS +- Only Program.cs touched +- Do not change method signatures +- Do not add new error codes +- Do not change the connection/operation logic — only the cleanup and error display + + + + +Before declaring plan complete: +- [ ] grep "DisconnectAsync" Program.cs — all results inside `finally` blocks only +- [ ] grep "PrintErrors" Program.cs — called before every VediErrore that follows a CncResult check +- [ ] `connected` flag declared in both Scarica and Invia +- [ ] Project compiles (no new errors) + + + +- Both Scarica and Invia use try/catch/finally with connected flag +- DisconnectAsync called only in finally +- PrintErrors called on every CncResult failure +- Zero compilation errors + + + +After completion, create `.paul/phases/11-refactor/11-02-SUMMARY.md` + diff --git a/.paul/phases/11-refactor/11-02-SUMMARY.md b/.paul/phases/11-refactor/11-02-SUMMARY.md new file mode 100644 index 0000000..27ec6a3 --- /dev/null +++ b/.paul/phases/11-refactor/11-02-SUMMARY.md @@ -0,0 +1,85 @@ +--- +phase: 11-refactor +plan: 02 +subsystem: cli +tags: [error-handling, refactor, connection-management] + +requires: + - phase: 11-01 + provides: "Scarica/Invia with explicit InputArgs parameter — enables this plan's refactor" +provides: + - try/finally disconnect guarantee in Scarica and Invia + - PrintErrors helper — surfaces CncResult.Errors on every failure + - Connection leak bug fixed (catch block previously had no disconnect) +affects: [] + +tech-stack: + added: [] + patterns: ["try/finally with connected flag for resource cleanup after async connect"] + +key-files: + created: [] + modified: [Program.cs] + +key-decisions: + - "connected flag (bool) preferred over nested try — simpler than tracking state across multiple early returns" + - "PrintErrors prints to stdout before VediErrore — operator sees machine detail then error code" + +patterns-established: + - "Resource cleanup pattern: bool connected = false; try { ... connected = true; } finally { if (connected) Disconnect(); }" + +duration: ~10min +started: 2026-05-18T00:00:00Z +completed: 2026-05-18T00:00:00Z +--- + +# Phase 11 Plan 02: Scarica/Invia error handling refactor + +**Fixed connection leak in Scarica/Invia (try/finally with `connected` flag); `CncResult.Errors` now printed before every error code.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~10min | +| Tasks | 2 completed | +| Files modified | 1 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Disconnect always runs after connect | Pass | `finally { if (connected) DisconnectAsync() }` in both methods | +| AC-2: No manual disconnect inside try | Pass | Zero `DisconnectAsync` calls inside try blocks — verified by grep | +| AC-3: CncResult errors printed on failure | Pass | `PrintErrors(result.Errors)` called at all 8 CncResult failure sites | + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `Program.cs` | Modified | try/finally pattern, `connected` flag, `PrintErrors` helper added | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| `bool connected` flag over nested try | Simpler than inner try; clearly marks connect/disconnect boundary | Easy to read, no double-catch complexity | +| `PrintErrors` before `VediErrore` | Operator sees machine-level detail first, then summary code | Better diagnostics; no behavior change otherwise | + +## Deviations from Plan + +None — plan executed exactly as written. + +## Next Phase Readiness + +**Ready:** +- Phase 11 complete — `Program.cs` clean, `ArgParser` isolated, errors surfaced +- Phase 12 (docs-cli) can proceed: README needs full CLI param table + +**Concerns:** None. + +**Blockers:** None. + +--- +*Phase: 11-refactor, Plan: 02* +*Completed: 2026-05-18* diff --git a/AesCrypt.cs b/AesCrypt.cs deleted file mode 100755 index fe0a204..0000000 --- a/AesCrypt.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; - -namespace NcProgramManager -{ - class AesCrypt - { - - public static byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV) - { - // Check arguments. - if (plainText == null || plainText.Length <= 0) - throw new ArgumentNullException("plainText"); - if (Key == null || Key.Length <= 0) - throw new ArgumentNullException("Key"); - if (IV == null || IV.Length <= 0) - throw new ArgumentNullException("IV"); - byte[] encrypted; - - // Create an Aes object - // with the specified key and IV. - using (Aes aesAlg = Aes.Create()) - { - aesAlg.Key = Key; - aesAlg.IV = IV; - - // Create an encryptor to perform the stream transform. - ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); - - // Create the streams used for encryption. - using (MemoryStream msEncrypt = new MemoryStream()) - { - using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) - { - using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) - { - //Write all data to the stream. - swEncrypt.Write(plainText); - } - encrypted = msEncrypt.ToArray(); - } - } - } - - // Return the encrypted bytes from the memory stream. - return encrypted; - } - - public static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV) - { - // Check arguments. - if (cipherText == null || cipherText.Length <= 0) - throw new ArgumentNullException("cipherText"); - if (Key == null || Key.Length <= 0) - throw new ArgumentNullException("Key"); - if (IV == null || IV.Length <= 0) - throw new ArgumentNullException("IV"); - - // Declare the string used to hold - // the decrypted text. - string plaintext = null; - - // Create an Aes object - // with the specified key and IV. - using (Aes aesAlg = Aes.Create()) - { - aesAlg.Key = Key; - aesAlg.IV = IV; - - // Create a decryptor to perform the stream transform. - ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); - - // Create the streams used for decryption. - using (MemoryStream msDecrypt = new MemoryStream(cipherText)) - { - using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) - { - using (StreamReader srDecrypt = new StreamReader(csDecrypt)) - { - - // Read the decrypted bytes from the decrypting stream - // and place them in a string. - plaintext = srDecrypt.ReadToEnd(); - } - } - } - } - - return plaintext; - } - } - } diff --git a/ArgParser.cs b/ArgParser.cs new file mode 100644 index 0000000..3c54303 --- /dev/null +++ b/ArgParser.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using NcProgramManager.Cnc.Models; + +namespace NcProgramManager +{ + internal static class ArgParser + { + public static InputArgs Parse(string[] args) + { + var inputArgs = new InputArgs(); + var arguments = new List(args); + + var help = arguments.Find(it => new Regex("-help.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(help)) + { + inputArgs.mostraHelp = true; + return inputArgs; + } + + var azione = arguments.Find(it => new Regex("-azione=.*", RegexOptions.IgnoreCase).IsMatch(it)); + inputArgs.azione = azione.Substring(8); + + var manufacturer = arguments.Find(it => new Regex("-manufacturer=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(manufacturer)) + { + string mfgVal = manufacturer.Substring(14).Trim().ToLowerInvariant(); + switch (mfgVal) + { + case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break; + case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break; + case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break; + default: inputArgs.manufacturer = CncManufacturer.Fanuc; break; + } + } + + string tipo = arguments.Find(it => new Regex("-tipo=.*", RegexOptions.IgnoreCase).IsMatch(it)); + inputArgs.tipo = !string.IsNullOrEmpty(tipo) ? Int32.Parse(tipo.Substring(6)) : 3; + + var ip = arguments.Find(it => new Regex("-ip=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(ip)) + inputArgs.ip = ip.Substring(4); + + var porta = arguments.Find(it => new Regex("-porta=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(porta)) + inputArgs.porta = porta.Substring(7); + + var nodo = arguments.Find(it => new Regex("-nodo=.*", RegexOptions.IgnoreCase).IsMatch(it)); + inputArgs.nodoHssb = !string.IsNullOrEmpty(nodo) ? Int32.Parse(nodo.Substring(6)) : -1; + + var username = arguments.Find(it => new Regex("-username=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(username)) + inputArgs.username = username.Substring(10); + + var password = arguments.Find(it => new Regex("-password=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(password)) + inputArgs.password = password.Substring(10); + + var compatibility = arguments.Find(it => new Regex("-compatibility.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(compatibility)) + inputArgs.compatibilityMode = true; + + var commento = arguments.Find(it => new Regex("-commento=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(commento)) + inputArgs.commentoProgramma = commento.Substring(10); + + var path = arguments.Find(it => new Regex("-path=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(path)) + inputArgs.pathCNC = Int16.Parse(path.Substring(6)); + + var pathprogramma = arguments.Find(it => new Regex("-pathprogramma=.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(pathprogramma)) + inputArgs.pathLocaleProgramma = pathprogramma.Substring(15); + + var hasToolOffsetData = arguments.Find(it => new Regex("-tooloffset.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(hasToolOffsetData)) + inputArgs.hasToolOffsetData = true; + + var hasWorkZeroOffsetData = arguments.Find(it => new Regex("-workzero.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(hasWorkZeroOffsetData)) + inputArgs.hasWorkZeroOffsetData = true; + + var debugMode = arguments.Find(it => new Regex("-debug.*", RegexOptions.IgnoreCase).IsMatch(it)); + if (!string.IsNullOrEmpty(debugMode)) + inputArgs.debugMode = true; + + return inputArgs; + } + } +} diff --git a/NcProgramManager.csproj b/NcProgramManager.csproj index 082bc80..7a6c087 100755 --- a/NcProgramManager.csproj +++ b/NcProgramManager.csproj @@ -72,7 +72,7 @@ - + diff --git a/Program.cs b/Program.cs index b2a80f9..e38a367 100755 --- a/Program.cs +++ b/Program.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text.RegularExpressions; using NcProgramManager.Cnc; using NcProgramManager.Cnc.Models; using NcProgramManager.Licensing; @@ -10,46 +9,44 @@ namespace NcProgramManager { internal class Program { - private static InputArgs inputArgs = new InputArgs(); - public static int Main(string[] args) { + InputArgs inputArgs; try { - parseArgomenti(args); + inputArgs = ArgParser.Parse(args); } catch (Exception e) { Console.Write("Gestore Programmi FANUC: "); Console.WriteLine(e.Message); Console.WriteLine("Errore parametri input. Consulta `-help' per maggiori informazioni."); - vediErrore(-101); + VediErrore(-101, false); return -101; } if (inputArgs.mostraHelp) { MostraAiuto(); - vediErrore(0); + VediErrore(0, inputArgs.debugMode); return 0; } - string _licFingerprint = new MachineFingerprint().GetFingerprint(); - string _licBase64; - string _licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic"); - if (!LicenseFile.TryRead(_licPath, out _licBase64) - || !new LicenseValidator().Validate(_licFingerprint, _licBase64)) + string licFingerprint = new MachineFingerprint().GetFingerprint(); + string licBase64; + string licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "license.lic"); + if (!LicenseFile.TryRead(licPath, out licBase64) + || !new LicenseValidator().Validate(licFingerprint, licBase64)) { Console.WriteLine("Licenza ERRATA"); - vediErrore(-100); + VediErrore(-100, inputArgs.debugMode); return -1; } - // Validate required params bool needsIp = inputArgs.manufacturer != CncManufacturer.Fanuc || inputArgs.nodoHssb == -1; if (inputArgs.azione == "" || (needsIp && string.IsNullOrEmpty(inputArgs.ip)) || inputArgs.pathLocaleProgramma == "") { - vediErrore(-102); + VediErrore(-102, inputArgs.debugMode); return -102; } @@ -70,10 +67,10 @@ namespace NcProgramManager switch (inputArgs.azione.ToUpperInvariant()) { - case "SCARICA": return Scarica(machine); - case "INVIA": return Invia(machine); + case "SCARICA": return Scarica(machine, inputArgs); + case "INVIA": return Invia(machine, inputArgs); default: - vediErrore(-103); + VediErrore(-103, inputArgs.debugMode); return -103; } } @@ -84,31 +81,33 @@ namespace NcProgramManager } } - static int Scarica(ICncMachine macchina) + static int Scarica(ICncMachine macchina, InputArgs inputArgs) { Console.WriteLine("RICEVI DA CNC-->"); + bool connected = false; try { var connResult = macchina.ConnectAsync().GetAwaiter().GetResult(); if (!connResult.Success) { Console.WriteLine("Macchina non connessa"); - vediErrore(-201); + PrintErrors(connResult.Errors); + VediErrore(-201, inputArgs.debugMode); return -201; } + connected = true; Console.WriteLine("Inizio RICEZIONE NCProgram"); var progResult = macchina.ReadProgramAsync(inputArgs.pathCNC.ToString()).GetAwaiter().GetResult(); Console.WriteLine("Fine RICEZIONE NCProgram"); if (!progResult.Success || string.IsNullOrEmpty(progResult.Value)) { - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-204); + PrintErrors(progResult.Errors); + VediErrore(-204, inputArgs.debugMode); return -204; } FanucProgram programma = new FanucProgram(progResult.Value); - IFanucMachine fanuc = macchina as IFanucMachine; if (inputArgs.hasToolOffsetData) @@ -116,8 +115,7 @@ namespace NcProgramManager if (fanuc == null) { Console.WriteLine("ToolOffset non supportato per questo produttore"); - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-205); + VediErrore(-205, inputArgs.debugMode); return -205; } Console.WriteLine("Inizio RICEZIONE ToolOffsetData"); @@ -125,8 +123,8 @@ namespace NcProgramManager Console.WriteLine("Fine RICEZIONE ToolOffsetData"); if (!toolResult.Success || string.IsNullOrEmpty(toolResult.Value)) { - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-205); + PrintErrors(toolResult.Errors); + VediErrore(-205, inputArgs.debugMode); return -205; } programma.setValue(toolResult.Value, FanucProgram.FILE_TYPE.TOOL_OFFEST_DATA); @@ -137,8 +135,7 @@ namespace NcProgramManager if (fanuc == null) { Console.WriteLine("WorkZeroOffset non supportato per questo produttore"); - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-206); + VediErrore(-206, inputArgs.debugMode); return -206; } Console.WriteLine("Inizio RICEZIONE WorkZeroOffsetData"); @@ -146,38 +143,45 @@ namespace NcProgramManager Console.WriteLine("Fine RICEZIONE WorkZeroOffsetData"); if (!workResult.Success || string.IsNullOrEmpty(workResult.Value)) { - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-206); + PrintErrors(workResult.Errors); + VediErrore(-206, inputArgs.debugMode); return -206; } programma.setValue(workResult.Value, FanucProgram.FILE_TYPE.WORK_ZERO_OFFSET); } - macchina.DisconnectAsync().GetAwaiter().GetResult(); File.WriteAllText(inputArgs.pathLocaleProgramma, programma.ToString()); - vediErrore(0); + VediErrore(0, inputArgs.debugMode); return 0; } catch (Exception ex) { Console.WriteLine(ex.Message); - vediErrore(-202); + VediErrore(-202, inputArgs.debugMode); return -202; } + finally + { + if (connected) + macchina.DisconnectAsync().GetAwaiter().GetResult(); + } } - static int Invia(ICncMachine macchina) + static int Invia(ICncMachine macchina, InputArgs inputArgs) { Console.WriteLine("INVIA A CNC-->"); + bool connected = false; try { var connResult = macchina.ConnectAsync().GetAwaiter().GetResult(); if (!connResult.Success) { Console.WriteLine("Macchina non connessa"); - vediErrore(-201); + PrintErrors(connResult.Errors); + VediErrore(-201, inputArgs.debugMode); return -201; } + connected = true; FanucProgram programma = new FanucProgram(File.ReadAllText(inputArgs.pathLocaleProgramma)); if (!string.IsNullOrEmpty(inputArgs.commentoProgramma)) @@ -197,8 +201,8 @@ namespace NcProgramManager Console.WriteLine("Fine INVIO NCProgram"); if (!writeResult.Success) { - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-204); + PrintErrors(writeResult.Errors); + VediErrore(-204, inputArgs.debugMode); return -204; } } @@ -210,8 +214,7 @@ namespace NcProgramManager if (fanuc == null) { Console.WriteLine("ToolOffset non supportato per questo produttore"); - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-205); + VediErrore(-205, inputArgs.debugMode); return -205; } Console.WriteLine("Inizio INVIO ToolOffsetData"); @@ -219,8 +222,8 @@ namespace NcProgramManager Console.WriteLine("Fine INVIO ToolOffsetData"); if (!toolResult.Success) { - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-205); + PrintErrors(toolResult.Errors); + VediErrore(-205, inputArgs.debugMode); return -205; } } @@ -230,8 +233,7 @@ namespace NcProgramManager if (fanuc == null) { Console.WriteLine("WorkZeroOffset non supportato per questo produttore"); - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-206); + VediErrore(-206, inputArgs.debugMode); return -206; } Console.WriteLine("Inizio INVIO WorkZeroOffsetData"); @@ -239,25 +241,36 @@ namespace NcProgramManager Console.WriteLine("Fine INVIO WorkZeroOffsetData"); if (!workResult.Success) { - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(-206); + PrintErrors(workResult.Errors); + VediErrore(-206, inputArgs.debugMode); return -206; } } - macchina.DisconnectAsync().GetAwaiter().GetResult(); - vediErrore(0); + VediErrore(0, inputArgs.debugMode); return 0; } catch (Exception ex) { Console.WriteLine(ex.Message); - vediErrore(-202); + VediErrore(-202, inputArgs.debugMode); return -202; } + finally + { + if (connected) + macchina.DisconnectAsync().GetAwaiter().GetResult(); + } } - static void vediErrore(int errore) + static void PrintErrors(IReadOnlyList errors) + { + if (errors == null) return; + foreach (var e in errors) + Console.WriteLine(" " + e.ToString()); + } + + static void VediErrore(int errore, bool debugMode) { Console.WriteLine("Errore:" + errore); Console.WriteLine(); @@ -274,7 +287,7 @@ namespace NcProgramManager Console.WriteLine("-206 : Errore in fase invio/ricezione work zero offest data"); Console.WriteLine("-207 : Posizionare il CN in MDI o EDIT per eseguire l'invio di un programma"); Console.WriteLine("-208 : Programma CN Caricato ma non settato a main."); - if (inputArgs.debugMode) + if (debugMode) Console.ReadLine(); } @@ -336,71 +349,5 @@ namespace NcProgramManager Console.WriteLine("-207 : Posizionare il CN in MDI o EDIT per eseguire l'invio di un programma"); Console.WriteLine("-208 : Programma CN Caricato ma non settato a main."); } - - private static void parseArgomenti(string[] args) - { - List arguments = new List(args); - var help = arguments.Find(it => new Regex("-help.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(help)) - { - inputArgs.mostraHelp = true; - return; - } - var azione = arguments.Find(it => new Regex("-azione=.*", RegexOptions.IgnoreCase).IsMatch(it)); - inputArgs.azione = azione.Substring(8); - var manufacturer = arguments.Find(it => new Regex("-manufacturer=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(manufacturer)) - { - string mfgVal = manufacturer.Substring(14).Trim().ToLowerInvariant(); - switch (mfgVal) - { - case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break; - case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break; - case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break; - default: inputArgs.manufacturer = CncManufacturer.Fanuc; break; - } - } - string tipo = arguments.Find(it => new Regex("-tipo=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(tipo)) - inputArgs.tipo = Int32.Parse(tipo.Substring(6)); - else inputArgs.tipo = 3; - var ip = arguments.Find(it => new Regex("-ip=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(ip)) - inputArgs.ip = ip.Substring(4); - var porta = arguments.Find(it => new Regex("-porta=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(porta)) - inputArgs.porta = porta.Substring(7); - var nodo = arguments.Find(it => new Regex("-nodo=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(nodo)) - inputArgs.nodoHssb = Int32.Parse(nodo.Substring(6)); - else inputArgs.nodoHssb = -1; - var username = arguments.Find(it => new Regex("-username=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(username)) - inputArgs.username = username.Substring(10); - var password = arguments.Find(it => new Regex("-password=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(password)) - inputArgs.password = password.Substring(10); - var compatibility = arguments.Find(it => new Regex("-compatibility.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(compatibility)) - inputArgs.compatibilityMode = true; - var commento = arguments.Find(it => new Regex("-commento=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(commento)) - inputArgs.commentoProgramma = commento.Substring(10); - var path = arguments.Find(it => new Regex("-path=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(path)) - inputArgs.pathCNC = Int16.Parse(path.Substring(6)); - var pathprogramma = arguments.Find(it => new Regex("-pathprogramma=.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(pathprogramma)) - inputArgs.pathLocaleProgramma = pathprogramma.Substring(15); - var hasToolOffestData = arguments.Find(it => new Regex("-tooloffset.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(hasToolOffestData)) - inputArgs.hasToolOffsetData = true; - var hasWorkZeroOffsetData = arguments.Find(it => new Regex("-workzero.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(hasWorkZeroOffsetData)) - inputArgs.hasWorkZeroOffsetData = true; - var debugMode = arguments.Find(it => new Regex("-debug.*", RegexOptions.IgnoreCase).IsMatch(it)); - if (!string.IsNullOrEmpty(debugMode)) - inputArgs.debugMode = true; - } } }