mirror of
https://github.com/expo/expo-agent-cli.git
synced 2026-11-15 03:58:08 -10:00
## Problem
After a `dev` CLI process gets SIGKILL, its Metro keeps serving the port
with no lock. `dev:stop` and `dev:stop --eas` report "not running" and
leave it up; only `dev:stop --port <n> --force` stops it. SIGKILL cannot
be caught, and the lock dies with the process.
## Fix
- The lock holder keeps a run record beside the lock:
`.expo/agent-cli-dev-run.json`, with its pid, the port, and the pid that
listens there (read with `lsof`, never a request to Metro). The lock
handle writes it once Metro reported its port, only for the port the
lock answers for, only when one process listens there, and not after the
dev server exited. Release removes it (the `finally` and the `exit`
hook), and so does a `dev:stop` that stopped the run through its lock,
since a Windows stop (`taskkill /F`) skips the holder's cleanup. A
SIGKILL skips all of these, so the record outlives a killed run.
- A lock-less server is a killed run's orphan when the record's pid is
gone (a zombie counts as gone), the only listener on the record's port
is the pid the record names, and its `/status` names this project. Two
listeners on one port (`expo start --localhost` on `[::1]` beside a
Metro on `127.0.0.1`) prove neither. A Metro a person started (`npx expo
start`) is another process, even on the port of a run that died with its
own Metro (a closed terminal), so `dev:stop` stops it only with
`--force`, as on main. Judging the record only reads; `dev:stop` removes
a spent one.
- `dev:stop`, with no `--port` or with the record's port, signals the
orphan's pid without `--force` and reports `untracked: true`, `forced:
false`.
- A signal that cannot reach the process (another user's) is reported at
once, and the How line names its owner instead of `--signal SIGKILL`.
## Red test
`e2e/__tests__/stop-test.ts`, "after a SIGKILL of a foreground dev": a
real `dev --ios` with a listening stub Metro, SIGKILL of the CLI pid
only.
- "dev:stop stops the dev server it left with no lock, without --force",
with no flags and with `--port`: red on main (`stopped: false`, `port:
null`, the orphan still answers).
- "leaves a dev server started by hand running": a run dies with its
Metro and leaves its record, then a stub `expo start` with no agent-cli
parent takes that port. Green on main; red on a rule that matched the
record's port alone.
- "never takes a hand-started dev server on [::1] for the run that
served beside it" (darwin, in tier0-e2e-macos): red on a lookup that
took the first `lsof` pid.
Unit: `lock-test` and `publishPort-test` (the record's life, also with
no `--port`, where the note once came before the lock),
`untrackedDevServer-test` (the verdict table and a real zombie),
`stopAsync-test` (a signal that cannot be sent; a stop through the lock
ends the record).
## Live check
`apps/eas-example` (expo 58.0.5), real Metro, `dev --web --port 8395`,
at the top of this stack:
- Real Metro listens as one process on `*:8395`, so the record names it:
`{pid: 75684, port: 8395, serverPid: 75761}`.
- SIGKILL of the CLI pid only: pid 75761 kept serving. Plain `dev:stop`
printed `stopped: true, untracked: true, forced: false, signal:
SIGTERM`; pid 75761 was gone, nothing listened on 8395, and the record
was gone.
- A run killed together with its Metro (record left), then `npx expo
start --web --port 8395` by hand (pid 77558): `dev:stop` said
`not-running`, `dev:stop --port 8395` said `foreign-dev-server`, pid
77558 kept serving, and the spent record was removed.
- A run stopped through its lock removes its record.
## Watch
An orphan is found only through its record. A run killed before Metro
reported its port, one whose `exit` hook ran (an uncaught error) while
its Metro survived, or one followed by a run that serves elsewhere and
writes its own record, leaves no usable record, so that Metro counts as
anyone's server, as on main. Refusing to start beside an orphan, and one
record per port, were considered and rejected: the case is rare, and
losing the proof can only spare a server, never stop the wrong one (LLP
0004 §Daemonization).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>