Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
3.6 KiB
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:
- Rejects a duplicate
idwhile that child is still mapped (#116). - Pipes stdin/stdout/stderr.
- Sets
ARGYLL_NOT_INTERACTIVE=1. - On Windows sets
CREATE_NO_WINDOW(0x08000000) (#46). - Splits stdout: lines beginning
ROW_COLORS_JSON:becomeprocess:json_row(prefix stripped); everything else isprocess:stdout. - Reaps the child on natural exit or kill signal; then emits
process:exit. - 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 ReadFiles 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 §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.