fix(errors): add actionable error messages for all 16 failure paths

This commit is contained in:
2026-08-08 18:21:17 -04:00
parent 47cfdee424
commit 854e7155f9
7 changed files with 435 additions and 9 deletions
+33
View File
@@ -25,3 +25,36 @@ Rather than pulling in `std.regex`, the parser uses hand-written `isNameChar()`
D non-capturing lambdas infer as `function` pointers, which cannot implicitly convert to `delegate`-typed parameters. The explicit `delegate` keyword (`scope f = delegate (...) { ... };`) forces the correct type. This pattern is needed for resolution seam delegates that production code will instantiate with captures (e.g. closing over `Config cfg`).
---
### Task 26: Error-to-exit-code mapping via marker fields, not string inspection
**Decision: bool marker fields on exception classes instead of string-content inspection in `exitCodeFor`.**
The plan initially suggested checking `e.msg.indexOf("not found")` to distinguish exit code 2 (package not found) from exit code 6 (network error) for `FetchException`. Instead, each exception class carries a boolean marker:
- `BuildException.toolMissing` — distinguishes tool-not-found (exit 7) from build failure (exit 4).
- `InstallException.toolMissing` — distinguishes tool-not-found (exit 7) from install failure (exit 5).
- `FetchException.notFound` — distinguishes package-not-found (exit 2) from network error (exit 6).
**Rationale**: String inspection is fragile — error messages may change, use different formatting, or get truncated. A dedicated field communicates intent unambiguously and survives message refactoring.
### Task 26: `TofuError` base class for explicit-exit exceptions
Rather than a separate `throwExit(int, string)` helper, `TofuError : Exception` with an `int exitCode` field lets callers throw the exception and have `exitCodeFor` read the code directly. This keeps the exit-mapping logic centralized (always go through `exitCodeFor`) while allowing callers to set explicit codes when they know them (e.g. in command stubs).
### Task 26: Lock file at `<cacheDir>/.lock` with PID-liveness check
The lock uses `kill(pid, 0)` (POSIX signal 0) to test whether the locking PID is still alive. A dead PID means the lock is stale and can be safely removed. This avoids the need for a separate lock-daemon or file-lock (flock/fcntl).
**Conservative assumption**: If `kill(pid, 0)` fails with anything other than ESRCH (e.g. EPERM), the PID is assumed alive. This errs on the side of safety — false-positive "another process" is better than concurrent writes.
### Task 26: `not implemented yet` stubs in dispatch for commands 20–24
Commands 20–24 (search, install, upgrade, remove, info) are separate tasks running in parallel. Since D cannot conditionally import modules at compile time, main.d dispatches via `final switch` on the `Command` enum with all cases present. For commands whose modules don't exist yet, a `logError("command '<x>' not implemented yet")` + `return 1` stub is used. These stubs are documented and will be replaced when tasks 20–24 land.
### Task 26: `write(2, ...)` POSIX syscall for signal handler, not `stderr.rawWrite`
DMD 2.112's `core.sys.posix.signal.signal` requires the handler to be `@nogc`. `std.stdio.File.rawWrite` is NOT `@nogc` (File is a GC-managed class). Instead, the handler uses the raw POSIX `write(2, msg.ptr, msg.length)` syscall from `core.sys.posix.unistd`, which is a direct C call and fully `@nogc`.
Similarly, `_exit(130)` from `core.sys.posix.unistd` is used instead of `core.stdc.stdlib.exit` — `_exit` does NOT run atexit handlers or flush stdio buffers, making it safe in a signal-handler context.
---