Skip to content

Commit 54aadbf

Browse files
fix(setup): survive a vanished port owner, and stop flagging our own containers
Two failures from one compose re-run: - 'Kill it for me' crashed setup with 'kill() failed: ESRCH: No such process'. The owner list is an lsof snapshot, so the process can exit before the signal lands — which is the outcome we wanted, not an error. ESRCH now counts as freed, EPERM warns that it must be stopped by hand, and anything else warns; the loop re-probes either way instead of aborting a setup that had already written .env. - Compose mode demanded 3000/3002 be free even when this stack was the one holding them, so re-running setup against a running install reported its own realtime container as a blocker and offered to kill Docker's listener. 'docker compose up -d' reconciles its own containers, so skip the check when the project already has some. A foreign process is still caught, and a foreign container still surfaces as a bind error from compose.
1 parent 6eec210 commit 54aadbf

2 files changed

Lines changed: 37 additions & 4 deletions

File tree

scripts/setup/modes/compose.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ import { glyph, theme } from '../theme.ts'
2525
* is fatal here: compose can't come up while the ports are held.
2626
*/
2727
async function ensureComposePortsFree(composeFile: string): Promise<void> {
28+
// This stack already holding 3000/3002 is not a conflict — `docker compose up
29+
// -d` reconciles its own containers. Without this, re-running setup against a
30+
// running install reports its own realtime container as a blocker and offers
31+
// to kill Docker's listener, which is never the right move. A genuinely
32+
// foreign process still gets caught below, and a foreign *container* surfaces
33+
// as a clear bind error from compose itself.
34+
const ours = spawnSync('docker', ['compose', '-f', composeFile, 'ps', '-q'], {
35+
cwd: ROOT,
36+
encoding: 'utf8',
37+
})
38+
if (ours.status === 0 && ours.stdout.trim()) {
39+
p.log.step('Existing Sim containers hold :3000/:3002 — compose will reconcile them')
40+
return
41+
}
2842
if (await ensurePortsFree([3000, 3002])) return
2943
throw new SetupError('ports 3000/3002 are in use', [
3044
`free the ports, then re-run: ${theme.command('bun run setup')}`,

scripts/setup/ports.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
12
import { type PortOwnerInfo, portOpen, portOwner } from './detect.ts'
23
import { waitFor } from './probes.ts'
34
import * as p from './prompter.ts'
@@ -54,12 +55,30 @@ export async function ensurePortsFree(ports: number[]): Promise<boolean> {
5455

5556
if (choice === 'abort') return false
5657
if (choice === 'kill') {
58+
const killed: string[] = []
5759
for (const b of killable) {
58-
if (b.owner) process.kill(b.owner.pid, 'SIGKILL')
60+
if (!b.owner) continue
61+
const label = `${b.owner.command} (pid ${b.owner.pid})`
62+
try {
63+
process.kill(b.owner.pid, 'SIGKILL')
64+
killed.push(label)
65+
} catch (error) {
66+
// The owner list came from an lsof snapshot, so the process may have
67+
// exited in between — that is the outcome we wanted, not a failure.
68+
// A signal we're not allowed to send is worth saying out loud, but
69+
// never fatal: the loop re-probes and offers the choice again.
70+
const code = (error as NodeJS.ErrnoException).code
71+
if (code === 'ESRCH') killed.push(`${label} — already gone`)
72+
else if (code === 'EPERM') {
73+
p.log.warn(
74+
`Not allowed to kill ${label} — stop it yourself, then choose "check again".`
75+
)
76+
} else {
77+
p.log.warn(`Could not kill ${label}: ${getErrorMessage(error)}`)
78+
}
79+
}
5980
}
60-
p.log.step(
61-
`Killed ${killable.map((b) => `${b.owner?.command} (pid ${b.owner?.pid})`).join(', ')}`
62-
)
81+
if (killed.length > 0) p.log.step(`Killed ${killed.join(', ')}`)
6382
// SIGKILL is async — the kernel releases the listening socket a beat after
6483
// the process dies, so re-checking immediately would still see the port
6584
// held. Wait for the killed ports to actually free before looping.

0 commit comments

Comments
 (0)