Skip to content

killRenderTree depends on ps, so a host that denies ps fails every Build after the render already succeeded #199

Description

@goatyyc

Summary

On a host that denies process enumeration, every hypit build that renders through hyperframes.local is reported as failed after the frames have already been rendered and the video encoded. The only thing that fails is the post-render cleanup probe.

A finished render is discarded, and --follow never prints Build complete.

Where

packages/provider-hyperframes-local/src/capture-process.ts

  • L28 — await exec("ps", ["-A", "-o", "pid=,ppid="], …) — descendant discovery
  • L43 — await exec("ps", ["-p", String(child), "-o", "stat="], …) — liveness probe
  • L78–81 — kill() turns any rejection from killRenderTree into failure = "Render cleanup failed; …", which close then rejects with
  • L115 — void kill() runs on the completed path, not only on error paths

Because of L115, the failure is not an edge case on this host: it is every Build.

Why ps fails here

macOS sandboxes — for example an agent/IDE host that applies a sandbox-exec profile — commonly deny process enumeration while still allowing libproc-backed queries:

$ ps -A
zsh: operation not permitted: /bin/ps

$ node -e 'require("node:child_process").execFile("ps",["-A"],()=>{})'
Error: spawn EPERM          # thrown from child_process.spawn

$ pgrep -P $$
62917                       # works

So execFile("ps", …) rejects before ps ever runs, and killRenderTree cannot distinguish "no descendants" from "cannot enumerate".

Evidence

Host: macOS 15.7 (24G222), arm64 · Node v22.22.2 · @hypit/hypit 0.1.9 · @hyperframes/engine 0.7.101

node bin/hypit.mjs build examples/semantic-composition/chat.svrun \
  --workspace examples/semantic-composition \
  --runtime examples/semantic-composition/hypit.runtime.json --follow

Execution log (hypit logs bld_20260915T160124951Z_58CF657E92), tail:

rendering frames
diagnostic: Capture worker 1: 120 frames; startup 730 ms; seek 101 ms; prepare 55 ms; PNG 2431 ms
diagnostic: Capture worker 0: 120 frames; startup 729 ms; seek 102 ms; prepare 58 ms; PNG 2449 ms
diagnostic: capturing frames: 2706 ms
encoding video
failed: Render cleanup failed; Error: spawn EPERM

hypit inspect:

Reason  Command need:…:request-visual-render ended without a stored result:
        Endpoint hyperframes.local failed render-visual: Render cleanup failed; Error: spawn EPERM

240/240 frames captured, encode started, Build discarded. The browser launch itself is healthy — [BrowserManager] Browser launched (HeadlessChrome/151.0.7922.71, screenshot, gl=--use-gl=angle --use-angle=metal, headlessShell=true, platform=darwin).

Two independent problems

  1. Descendant discovery does not need ps. Asking the kernel about one parent at a time is enough, and the queries that work on the restricted host are the cheaper ones. ps -A also reads every process on the machine to answer a question about one subtree.

  2. A cleanup probe should not discard a finished render. L114 already states the intent — "Terminate any leftover descendants before releasing capacity" — which is resource hygiene, not a correctness condition on the output. The worker reported completed and the media pipeline produced a file; failing the Build here loses work that exists.

Direction (verified locally)

Both ps uses in killRenderTree replaced:

  • descendants — breadth-first over exec("pgrep", ["-P", String(pid)]), treating exit code 1 as "no children"
  • liveness — process.kill(pid, 0); ESRCH means the process is gone
  • the wait loop degrades to a timeout instead of raising, since the next point cannot be expressed with kill(pid, 0)

One caveat worth knowing: process.kill(pid, 0) cannot distinguish a zombie from a running process, which is exactly what the stat column was read for. In practice this is benign here — the worker is this process's own child and is reaped on close, and Chrome grandchildren are reparented — so the loop exits promptly. But it is the one behaviour the ps -o stat= check got that the replacement does not.

With only that change, the same command on the same host:

✓ Build complete
  Build  bld_20260915T160325985Z_F0C3B7BCC5

hypit get bld_20260915T160325985Z_F0C3B7BCC5 --output final.video … --to <path> then exports an 8s, 540x960, 30fps H.264 + AAC file with the expected picture.

Happy to open a PR with the change if that is useful — otherwise it stays a local patch.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions