a job's controls can no longer freeze the platform

The server went down. It had not: it was STOPPED — eleven threads in state T, port
9000 still listening with connections queueing behind a process that would never
accept them. Indistinguishable from a crash unless you look, which is why it took a
terminal to see. `kill -CONT` brought it back with nothing lost.

I did it, with one missing line. `restartBackgroundTask` opened the job's output
file to hand the child a descriptor and never closed it, so the PLATFORM became a
holder of that file. `pidsHoldingFile` asks `fuser` who holds it — that is how Stop
finds a job with no recorded pid — so the platform was in the answer. The next Pause
sent SIGSTOP to it.

Every part worked as designed. Pause froze exactly what was holding the file, and
one of those things was the thing doing the freezing.

Two fixes, and the second matters more than the first:

**Close the descriptor.** The child gets its own duplicate at spawn, so closing
costs it nothing. That removes this instance.

**Never signal our own pids, whatever `fuser` says.** `pidsHoldingFile` now filters
`process.pid` and `process.ppid`. The leak is fixed; this is what makes the next one
survivable. Nothing that manages a job should be able to name the process doing the
managing — a rule worth having independently of the bug that suggested it.

Verified: after a restart this process holds the output file zero times, `fuser`
returns only the spawned shell and its child, and our own pid is filtered even when
present.

tsgo clean, frontend builds, 901 pass / 7 fail unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 14:28:16 +00:00
co-authored by Claude Opus 5
parent bc2bafe24a
commit bda5eb15de
2 changed files with 19 additions and 2 deletions
+8 -1
View File
@@ -744,10 +744,17 @@ export async function pidsHoldingFile(who: ChatIdentity, path: string): Promise<
const proc = Bun.spawn(argv, { stdout: 'pipe', stderr: 'ignore' });
const out = await new Response(proc.stdout).text();
await proc.exited;
// The platform's OWN pids are removed, always.
//
// Not defensive programming for its own sake: a leaked descriptor made the server a holder of a job's
// output file, and the next Pause `SIGSTOP`ped the whole platform through this list. The leak is fixed,
// and this is the guard that means the next one cannot do the same thing. Nothing that manages a job
// should ever be able to name the process doing the managing.
const self = new Set([process.pid, process.ppid]);
return out
.split(/\s+/)
.map((token) => Number(token.replace(/\D/g, '')))
.filter((n) => Number.isInteger(n) && n > 1);
.filter((n) => Number.isInteger(n) && n > 1 && !self.has(n));
}
/**