Files
iccery-v2-mac/docs/03-ipc-and-process-manager.md
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 4c2863e52c Add bootstrap docs: AGENTS.md, BUILD-PLAN.md, LICENCE.md, vendored spec (docs/), brand assets
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 17:36:50 +01:00

65 lines
3.6 KiB
Markdown

# 03 — IPC and process manager
The host never waits on a child from the request that spawned it (except `printcal`/`applycal`, which use captured `.output()`). Streaming tools go through a process manager that:
1. Rejects a duplicate `id` while that child is still mapped (#116).
2. Pipes stdin/stdout/stderr.
3. Sets `ARGYLL_NOT_INTERACTIVE=1`.
4. On Windows sets `CREATE_NO_WINDOW` (`0x08000000`) (#46).
5. Splits stdout: lines beginning `ROW_COLORS_JSON: ` become `process:json_row` (prefix stripped); everything else is `process:stdout`.
6. Reaps the child on natural exit **or** kill signal; then emits `process:exit`.
7. Drops stdin from the map on kill so writers fail fast.
## Commands the rewrite must expose
| Command | Args | Returns |
|---------|------|---------|
| `spawn_process` | `{ id, binary, args }` | `()` — unused by current JS (registered only) |
| `send_stdin` | `{ id, input }` | `()``input` is the **exact bytes**, already including `\n` |
| `kill_process` | `{ id }` | `()` |
| `kill_all_processes` | — | `usize` count signaled |
| `resolve_binary` | `{ binaryName }` | absolute path string |
| `run_targen` / `run_printtarg` / `run_chartread` / `run_average` / `run_colprof` / `run_profcheck` / `extract_gamut` / `detect_instruments` | typed configs | `()` after spawn (not after exit) |
| `generate_calibration_target` / `compute_calibration_curves` / `apply_calibration` | typed | captured result |
Frontend waits for `process:exit` with matching `id`. **Never** assume invoke() resolves when the tool finishes.
## Deadlock history (must not regress)
### ICCery #84 (P0)
Early ProcessManager held a `Mutex<Child>` across `Child::wait()`. `send_stdin` needed the same mutex → Calibrate/Retry hung. Dropping the mutex also dropped `ChildStdin` at spawn, closing the pipe immediately.
**Invariant:** stdin handle lives in its own map, independent of wait. Wait runs in a background task with a oneshot kill channel.
### ArgyllCMS fork #24 + ICCery #134
On Windows, `SetNamedPipeHandleState(PIPE_NOWAIT)` **fails on anonymous pipes** created by `Stdio::piped()`. Argyll's `con_char(wait=0)` then `ReadFile`s a blocking pipe during `uicallback`, so the instrument trigger thread never sees the button and the lamp never lights.
Fork fix: `PeekNamedPipe` before `ReadFile` (`spectro/conv.c`). ICCery always sets `ARGYLL_NOT_INTERACTIVE=1` so Argyll uses the pipe path, not a console.
**Invariant:** ship the Gronod fork (or equivalent PeekNamedPipe patch). Stock Argyll 3.5.0 will hang interactive chartread on Windows.
### Kill on window close (#147, #149)
`chartread` outlives the UI if not killed. XY tables need `q\n` first to park the head, then kill. On `CloseRequested` / `Exit`, `kill_all` is mandatory. Frontend Cancel in `TABLE_*` states sends `q\n` then kills.
## stdin protocol
`send_stdin` writes UTF-8 bytes and flushes. ICCery strings (see [05](05-argyll-fork.md) §12.5):
| UI | Bytes | Meaning in real chartread |
|----|-------|---------------------------|
| Calibrate / Retry strip / Accept (many states) | `" \n"` or `"\n"` | Space or Return = trigger (`DUIH_TRIG`) |
| Done & Save | `"d\n"` | finish and write `.ti3` |
| Skip | `"s\n"` | **not a skip in real strip mode** — treated as trigger. Mock/UI invention. Rewrite should verify against fork `chartread.c` before advertising Skip. |
| Undo | `"u\n"` | same caveat |
| XY cancel | `"q\n"` | abort / park |
| Warning accept | `"\n"` | "use it anyway" |
Always include the newline. Argyll line-buffers prompts.
## Logging hygiene
Spawn argv is logged with home directories rewritten to `~` (case-insensitive on Windows). Raw argv only at debug.