Skip to content

scripts/gen-sdui-manifest.sh leaks its vite dev server when the dump fails — the orphan survives the EXIT trap and holds the shared heavy-verify flock for the whole container #9399

Description

@os-steve

Filed unassigned by the dev seat working #8893 (console pin bump). ⛔ Not fixed there — different file, out of that card's scope. No domain:* / target:* set.

What happened, measured

scripts/gen-sdui-manifest.sh starts the console dev server in the background and arms a cleanup trap:

pnpm --filter @object-ui/console exec vite dev --port 5180 > /tmp/sdui-dump-dev.log 2>&1&
DUMP_DEV_PID=$!trap'kill "$DUMP_DEV_PID" 2>/dev/null || true' EXIT

On 2026-08-17T22:29:53Z the manifest dump failed (Playwright browser revision mismatch — the documented docs/releases-maintenance.md case) and the script exited 1, printing its own failure message. The trap ran. The background process was still alive 20 minutes later:

 PID ELAPSED CMD
4605 20:12 node /opt/node22/bin/pnpm --filter @object-ui/console exec vite dev --port 5180

Two things make this more than an untidy process:

  1. It was no longer serving anything.ss -ltn showed nothing on 5180 and the vite child was gone; only the pnpm wrapper node process survived. kill "$DUMP_DEV_PID" evidently reached something other than the process that outlives the run — the wrapper re-execs, so $! is not the pid that matters.
  2. It held this container's shared heavy-verify flock. The run was wrapped, per agent discipline, in flock -E 99 -w 540 /tmp/os-heavy-verify.lock -c '...'. Background children inherit the lock fd, so the orphan inherited it and never released it:
$ fuser -v /tmp/os-heavy-verify.lock
USER PID ACCESS COMMAND
root 4605 f.... node
$ ls -l /proc/4605/fd | grep -c os-heavy-verify
1

Every subsequent acquisition in the container times out at exit 99. Two later attempts (540s and 300s) both queued out against this zombie. Any parallel agent doing a build or test run in this container is blocked by it for as long as it lives, with no signal other than a lock timeout.

Why it matters beyond one bad run

The failing path is not exotic — it is the documented one. docs/releases-maintenance.md records that in an agent dispatch container the Playwright build revision routinely does not match after a pin bump, and the script itself prints that pointer on failure. So the failure branch that leaks the server is the branch a dispatch container is expected to take, and the leak is invisible: the script's last words are its own tidy failure message.

The lock interaction turns a per-run annoyance into a container-wide stall, and the agent that caused it is not necessarily able to clear it (this seat's kill 4605 was refused by its permission layer, so the orphan is still running as this is filed).

Suggested shape (not a decision)

  • Kill the process group, not $!: start the server with setsid and kill -- -$PGID on EXIT, or track the pid that actually binds the port. The current kill reaches a wrapper that is not the survivor.
  • Have the trap verify the process is gone (poll briefly, then SIGKILL) and say so when it cannot. A cleanup that cannot fail silently is the whole point here.
  • Consider having the script open no inherited fds into background children (... & with 0<&- ...), so an orphan can never hold a caller's flock even if the kill misses. This is the part that converts a leak into a container-wide outage, and it is fixable independently of the kill.

Reproduction is cheap and does not need a broken Playwright: run the script with a PLAYWRIGHT_BROWSERS_PATH pointing at an empty directory, under flock, then check fuser -v on the lock file.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions