ProcessManager: shared process factory and one ROW_COLORS_JSON emitter #84

Closed
opened 2026-09-10 18:23:14 +01:00 by gronod · 1 comment
Owner

Summary

ProcessManager.runStreaming and runCaptured both construct Process, attach pipes, set childEnvironment (ARGYLL_NOT_INTERACTIVE=1), log sanitized argv, and install both a terminationHandler and a Task.detached { waitUntilExit() } watchdog (#50/#52).

ROW_COLORS_JSON: prefix handling is copied in ingestOutput, flushPartialLine, and maybeFinalize.

Extract internals only. Do not merge streaming and captured APIs — captured has no stdin, different EOF story, and is the printcal/applycal/CUPS path.

Spec refs

  • docs/03-ipc-and-process-manager.md
  • Issue 2 contract: exclusive ids (#116), stdin independent of wait (#84), json-row isolation, killAll on quit (#147/#149)
  • ProcessManager.rowColorsPrefix = "ROW_COLORS_JSON: " (space after colon)

Scope

In

  • makeProcess(binary:arguments:workingDirectory:environment:standardInput:) private helper that returns the configured Process + pipes.
  • Shared spawn log line (spawn vs spawn(captured) prefix can stay).
  • Shared watchdog attachment (terminationHandler + detached waitUntilExit).
  • emitStdoutLine(id:line:) used by ingest / partial flush / finalize:
    • if line has prefix → emit .jsonRow with prefix stripped and do not also emit .stdout
    • else emit .stdout
  • Stderr stays .stderr + warn log.

Out

  • Changing captured Box continuation (leave it unless a test proves a race).
  • Changing pre-kill XY q\n + 500 ms hook.
  • Moving CUPS off runCaptured.

Full solution

  • childEnvironment remains the single place that forces ARGYLL_NOT_INTERACTIVE=1.
  • Duplicate-id rejection stays at the top of both public methods before makeProcess.
  • Finalization watchdog (forceKill + forceFinalize after 2 s) stays streaming-only.

Rewrite invariants

  • json-row lines never appear as stdout events.
  • killAll still covers both children and captured.
  • Fast-exit children still emit exit once (watchdog exists because terminationHandler can lose the race).

Dependencies

Blocks-on: none. Can land in parallel with the runner extract; rebase if both touch spawn logging tests.

Test

Existing Tests/ICCeryCoreTests/ProcessManagerTests.swift must stay the contract:

  • duplicate-id rejection
  • stdin survives while child waits
  • json-row isolation (prefix stripped, not duplicated as stdout)
  • kill emits exit
  • killAll drains streaming + captured
  • runCaptured returns full stdout/stderr
  • home sanitisation of logged argv

Add:

  • a fixture that prints a partial ROW_COLORS_JSON: { line without newline; flushPartialLine must emit jsonRow not stdout
  • finalize-with-unterminated-tail: child exits with a trailing json-row fragment → jsonRow on finalize
  • captured + streaming watchdog: fixture that exits before the handler runs still produces exactly one exit event

Acceptance criteria

  • Streaming and captured still distinct public APIs.
  • Prefix parsing exists in one function.
  • Process construction exists in one function.
  • ProcessManager unit tests green including the two new tail/partial cases.
## Summary `ProcessManager.runStreaming` and `runCaptured` both construct `Process`, attach pipes, set `childEnvironment` (`ARGYLL_NOT_INTERACTIVE=1`), log sanitized argv, and install **both** a `terminationHandler` and a `Task.detached { waitUntilExit() }` watchdog (#50/#52). `ROW_COLORS_JSON: ` prefix handling is copied in `ingestOutput`, `flushPartialLine`, and `maybeFinalize`. Extract internals only. Do **not** merge streaming and captured APIs — captured has no stdin, different EOF story, and is the printcal/applycal/CUPS path. ## Spec refs - `docs/03-ipc-and-process-manager.md` - Issue 2 contract: exclusive ids (#116), stdin independent of wait (#84), json-row isolation, killAll on quit (#147/#149) - `ProcessManager.rowColorsPrefix = "ROW_COLORS_JSON: "` (space after colon) ## Scope **In** - `makeProcess(binary:arguments:workingDirectory:environment:standardInput:)` private helper that returns the configured `Process` + pipes. - Shared spawn log line (`spawn` vs `spawn(captured)` prefix can stay). - Shared watchdog attachment (`terminationHandler` + detached `waitUntilExit`). - `emitStdoutLine(id:line:)` used by ingest / partial flush / finalize: - if line has prefix → emit `.jsonRow` with prefix stripped and **do not** also emit `.stdout` - else emit `.stdout` - Stderr stays `.stderr` + warn log. **Out** - Changing captured `Box` continuation (leave it unless a test proves a race). - Changing pre-kill XY `q\n` + 500 ms hook. - Moving CUPS off `runCaptured`. ## Full solution - `childEnvironment` remains the single place that forces `ARGYLL_NOT_INTERACTIVE=1`. - Duplicate-id rejection stays at the top of both public methods before `makeProcess`. - Finalization watchdog (`forceKill` + `forceFinalize` after 2 s) stays streaming-only. ## Rewrite invariants - json-row lines never appear as `stdout` events. - `killAll` still covers both `children` and `captured`. - Fast-exit children still emit `exit` once (watchdog exists because `terminationHandler` can lose the race). ## Dependencies Blocks-on: none. Can land in parallel with the runner extract; rebase if both touch spawn logging tests. ## Test Existing `Tests/ICCeryCoreTests/ProcessManagerTests.swift` must stay the contract: - duplicate-id rejection - stdin survives while child waits - json-row isolation (prefix stripped, not duplicated as stdout) - kill emits exit - killAll drains streaming + captured - runCaptured returns full stdout/stderr - home sanitisation of logged argv Add: - a fixture that prints a **partial** `ROW_COLORS_JSON: {` line without newline; `flushPartialLine` must emit `jsonRow` not `stdout` - finalize-with-unterminated-tail: child exits with a trailing json-row fragment → `jsonRow` on finalize - captured + streaming watchdog: fixture that exits before the handler runs still produces exactly one `exit` event ## Acceptance criteria - [ ] Streaming and captured still distinct public APIs. - [ ] Prefix parsing exists in one function. - [ ] Process construction exists in one function. - [ ] ProcessManager unit tests green including the two new tail/partial cases.
gronod added this to the M7 — Deduplicate & consolidate (develop) milestone 2026-09-10 18:23:14 +01:00
gronod self-assigned this 2026-09-10 18:23:14 +01:00
Author
Owner

Implementation originally landed in stacked commit d4261ba via PR #87.
Completion/verification landed in PR #100 at e48c3f6.

Acceptance evidence:

[x] Centralised private spawn logging in ProcessManager preserving exact prefixes and LogSanitizer.

[x] Added deterministic tests for partial ROW_COLORS_JSON flush and exit tail finalisation.

[x] Verified fast-exit watchdogs emit exactly one exit event on streaming and captured paths.

[x] Verified ARGYLL_NOT_INTERACTIVE=1 in captured child environment.

[x] Mixed streaming + captured killAll verified.

Verification at milestone/m8-consolidation 891a504ee7:

targeted suites: passed (27 tests)

full universal ICCeryCoreTests: 339 passed, 0 failed

full ICCeryUITests: 29 passed, 0 failed

Closing manually after code and tests are present on the milestone branch.

Implementation originally landed in stacked commit d4261ba via PR #87. Completion/verification landed in PR #100 at e48c3f6. Acceptance evidence: [x] Centralised private spawn logging in ProcessManager preserving exact prefixes and LogSanitizer. [x] Added deterministic tests for partial ROW_COLORS_JSON flush and exit tail finalisation. [x] Verified fast-exit watchdogs emit exactly one exit event on streaming and captured paths. [x] Verified ARGYLL_NOT_INTERACTIVE=1 in captured child environment. [x] Mixed streaming + captured killAll verified. Verification at milestone/m8-consolidation 891a504ee722037ab15e01c1ae801a4d4cb9415a: targeted suites: passed (27 tests) full universal ICCeryCoreTests: 339 passed, 0 failed full ICCeryUITests: 29 passed, 0 failed Closing manually after code and tests are present on the milestone branch.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gronod/iccery-v2-mac#84