fix: launch emulator detached so the 5s start timeout doesn't kill it#136
Open
holloandris wants to merge 2 commits into
Open
fix: launch emulator detached so the 5s start timeout doesn't kill it#136holloandris wants to merge 2 commits into
holloandris wants to merge 2 commits into
Conversation
`EmulatorAdapter.start()` launched the emulator via
`runEmulator([...], { timeoutMs: 5000 })`, assuming the timeout would just
stop waiting while the emulator kept running. But `ProcessRunner.run()` uses
`execa(..., { timeout })`, and execa's timeout SIGTERMs the child when it
fires. Since the emulator is a normal (non-detached) child of that execa
call, it gets killed ~5s after launch — long before a cold boot finishes
(30-60s). On hosts where the `emulator` launcher hasn't already forked a
detached qemu within 5s (e.g. macOS/Apple Silicon), the emulator dies and
`emulator-device list` reports `running: []`.
Launch the emulator fully detached instead, with no killing timeout:
add `ProcessRunner.runDetached()` /`runEmulatorDetached()` using
`spawn(cmd, args, { detached: true, stdio: "ignore" }).unref()`, and call it
from `start()`. The emulator now outlives the MCP process and boots normally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Comment on lines
+113
to
+117
| const child = spawn(command, args, { | ||
| detached: true, | ||
| stdio: "ignore", | ||
| }); | ||
| child.unref(); |
Contributor
There was a problem hiding this comment.
Unhandled spawn
'error' event can crash the process
If spawn() fails to exec the command (e.g. ENOENT when the emulator binary is missing or the path returned by getEmulatorPath() is wrong), Node.js emits an 'error' event on the returned ChildProcess. Because no listener is attached before child.unref(), Node.js re-throws it as an uncaughtException, which terminates the MCP process — turning a configuration problem into a hard crash with no useful error message to the caller.
Suggested change
| const child = spawn(command, args, { | |
| detached: true, | |
| stdio: "ignore", | |
| }); | |
| child.unref(); | |
| const child = spawn(command, args, { | |
| detached: true, | |
| stdio: "ignore", | |
| }); | |
| child.on("error", () => { | |
| // Detached process — ignore exec errors (e.g. ENOENT) to prevent | |
| // an unhandled 'error' event from crashing the MCP process. | |
| }); | |
| child.unref(); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/process-runner.ts
Line: 113-117
Comment:
**Unhandled spawn `'error'` event can crash the process**
If `spawn()` fails to exec the command (e.g. `ENOENT` when the emulator binary is missing or the path returned by `getEmulatorPath()` is wrong), Node.js emits an `'error'` event on the returned `ChildProcess`. Because no listener is attached before `child.unref()`, Node.js re-throws it as an `uncaughtException`, which terminates the MCP process — turning a configuration problem into a hard crash with no useful error message to the caller.
```suggestion
const child = spawn(command, args, {
detached: true,
stdio: "ignore",
});
child.on("error", () => {
// Detached process — ignore exec errors (e.g. ENOENT) to prevent
// an unhandled 'error' event from crashing the MCP process.
});
child.unref();
```
How can I resolve this? If you propose a fix, please make it concise.…rash the MCP Addresses PR review (Greptile P1): the detached spawn() had no 'error' listener, so an ENOENT/EACCES (e.g. wrong/missing emulator binary path) would emit an unhandled 'error' event and terminate the whole MCP process. Attach a handler that logs via the project logger; the failure is still surfaced to the caller through its own detection (EmulatorAdapter.start throws EMULATOR_START_FAILED when no device appears), so a misconfig is now a clean error instead of a hard crash. Adds runDetached tests covering the happy path and the missing-binary case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
|
Thanks for the review — good catch on the P1. Fixed in 0fa52b4:
|
Owner
|
@greptileai lets get this PR reviewed again with the latest changes |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EmulatorAdapter.start()launches the emulator withrunEmulator([...], { timeoutMs: 5000 })and a comment saying it's "Expected to 'timeout' as emulator runs forever." ButProcessRunner.run()usesexeca(..., { timeout }), and execa'stimeoutSIGTERMs the child process when it fires — it doesn't just stop awaiting. Because the emulator is a normal (non-detached) child of that execa call, it gets killed ~5s after launch, long before a cold boot completes (30–60s).On hosts where the
emulatorlauncher hasn't already forked a detachedqemuwithin those 5s (reproducible on macOS / Apple Silicon), the emulator dies andemulator-device listthen reportsrunning: []—startappears to "succeed" but no emulator survives.Fix
Launch the emulator fully detached, with no killing timeout:
ProcessRunner.runDetached()/runEmulatorDetached()usingspawn(cmd, args, { detached: true, stdio: "ignore" }).unref().runEmulatorDetached()fromEmulatorAdapter.start()instead of the timeout-boundedrunEmulator().The emulator now outlives the MCP process and boots normally. The existing
start()registration/diff logic and the-no-snapshot-load/-no-boot-animflags are unchanged.Test Plan
npm run validatepasses (build + lint + complexity + 669 tests + contracts).tests/adapters/emulator.test.tsto mockrunEmulatorDetached.emulator-device startleftrunning: []within ~5s; after, the emulator stays up and reachessys.boot_completed=1, and the full install → launch → UI-drive loop works end to end.Checklist
npm run validate)