When managed Metro misses its readiness deadline while its main thread is blocked, the failed-start cleanup reports success but leaves a Metro grandchild running as an orphan. The orphan is reparented to launchd inside the launcher's process group, keeps the session's metro.log open and ignores SIGTERM even after the stall ends. The refusal is still an ordinary METRO_START_UNAVAILABLE with cleanup counted as proven, and the product's own rn_session stop_metro then answers alreadyStopped: true without seeing it. This is likely a specific root cause behind #969.
Evidence (independent QA of #998 at 2e183515c6266a7899de67d1022f7de0876f7aa8, plugin 1.0.6, iOS 26.4 simulator, 2026-09-10; report: #998 (comment))
- Reproduced in 2 of 2 synchronous-stall failures (default 90 s budget with a 150 s stall; 30 s configured budget with a 60 s stall). Refusal:
METRO_START_UNAVAILABLE: allocated Metro did not become authoritative (launcher alive at deadline; readiness deadline 90000 ms expired: listener absent 485 probes, probe unknown 0, unowned listener none) at 90.13 s, exit 2.
- Afterwards
stop_metro → alreadyStopped: true, while ps showed the orphan (ppid 1, launcher pgid) holding the log open. SIGTERM was ignored; SIGKILL was needed.
- Control: with an async stall (event loop free, 20 s budget), Metro exited on SIGTERM and nothing was orphaned. The defect needs a Metro that is alive with a blocked main thread at the deadline, a plausible shape for a slow synchronous cold start.
Repro
- Prepend an env-gated synchronous stall (for example
Atomics.wait for 150 s) to the app's metro.config.js.
- Build through the integrated launcher with the default readiness budget.
- After the refusal, run
rn_session stop_metro and list processes for the session's Metro port/log.
Impact: a leaked Metro survives with no owner the session can see, holds session resources, and can contend with the next attempt (#969). Cleanup is misreported as proven.
Suggested direction: in stopManagedMetroProcesses (packages/rn-dev-agent-core/src/session/managed-metro.ts), when no listener was ever observed, do not treat "launcher gone and port absent" as proven. After SIGTERM, require the launcher's process group to be empty (the processGroupExists check managed-metro-enforcement.ts already uses) and escalate to SIGKILL on the group before returning. Regression test: a SIGTERM-ignoring child.
When managed Metro misses its readiness deadline while its main thread is blocked, the failed-start cleanup reports success but leaves a Metro grandchild running as an orphan. The orphan is reparented to launchd inside the launcher's process group, keeps the session's
metro.logopen and ignores SIGTERM even after the stall ends. The refusal is still an ordinaryMETRO_START_UNAVAILABLEwith cleanup counted as proven, and the product's ownrn_session stop_metrothen answersalreadyStopped: truewithout seeing it. This is likely a specific root cause behind #969.Evidence (independent QA of #998 at
2e183515c6266a7899de67d1022f7de0876f7aa8, plugin 1.0.6, iOS 26.4 simulator, 2026-09-10; report: #998 (comment))METRO_START_UNAVAILABLE: allocated Metro did not become authoritative (launcher alive at deadline; readiness deadline 90000 ms expired: listener absent 485 probes, probe unknown 0, unowned listener none)at 90.13 s, exit 2.stop_metro→alreadyStopped: true, whilepsshowed the orphan (ppid 1, launcher pgid) holding the log open. SIGTERM was ignored; SIGKILL was needed.Repro
Atomics.waitfor 150 s) to the app'smetro.config.js.rn_session stop_metroand list processes for the session's Metro port/log.Impact: a leaked Metro survives with no owner the session can see, holds session resources, and can contend with the next attempt (#969). Cleanup is misreported as proven.
Suggested direction: in
stopManagedMetroProcesses(packages/rn-dev-agent-core/src/session/managed-metro.ts), when no listener was ever observed, do not treat "launcher gone and port absent" as proven. After SIGTERM, require the launcher's process group to be empty (theprocessGroupExistscheckmanaged-metro-enforcement.tsalready uses) and escalate to SIGKILL on the group before returning. Regression test: a SIGTERM-ignoring child.