-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: remote control — Expo mobile app + desktop proxy #1693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0ec8ed7
7603114
ecfda12
b1413f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,3 +21,7 @@ apps/web/src/components/__screenshots__ | |
| .vitest-* | ||
| __screenshots__/ | ||
| .tanstack | ||
| .expo/ | ||
| /App.tsx | ||
| /app.json | ||
| ios/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import { spawn } from "node:child_process"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const desktopDir = resolve(__dirname, ".."); | ||
| const bunExecutable = process.execPath; | ||
| const childScriptNames = ["dev:bundle", "dev:electron"]; | ||
| const forcedShutdownTimeoutMs = 1_500; | ||
|
|
||
| const children = new Map(); | ||
| let shuttingDown = false; | ||
| let forcedShutdownTimer = null; | ||
| let exitCode = 0; | ||
| let exitSignal = null; | ||
|
|
||
| function maybeExit() { | ||
| if (children.size > 0) { | ||
| return; | ||
| } | ||
|
|
||
| if (forcedShutdownTimer !== null) { | ||
| clearTimeout(forcedShutdownTimer); | ||
| forcedShutdownTimer = null; | ||
| } | ||
|
|
||
| if (exitSignal !== null) { | ||
| process.kill(process.pid, exitSignal); | ||
| return; | ||
| } | ||
|
|
||
| process.exit(exitCode); | ||
| } | ||
|
|
||
| function stopRemainingChildren() { | ||
| for (const child of children.values()) { | ||
| child.kill("SIGTERM"); | ||
| } | ||
|
|
||
| if (forcedShutdownTimer !== null) { | ||
| return; | ||
| } | ||
|
|
||
| forcedShutdownTimer = setTimeout(() => { | ||
| for (const child of children.values()) { | ||
| child.kill("SIGKILL"); | ||
| } | ||
| }, forcedShutdownTimeoutMs); | ||
| forcedShutdownTimer.unref(); | ||
| } | ||
|
|
||
| function shutdown({ code = 0, signal = null } = {}) { | ||
| if (shuttingDown) { | ||
| if (code !== 0 && exitCode === 0) { | ||
| exitCode = code; | ||
| } | ||
| if (signal !== null && exitSignal === null) { | ||
| exitSignal = signal; | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| shuttingDown = true; | ||
| exitCode = code; | ||
| exitSignal = signal; | ||
| stopRemainingChildren(); | ||
| maybeExit(); | ||
| } | ||
|
|
||
| function startChild(scriptName) { | ||
| const child = spawn(bunExecutable, ["run", scriptName], { | ||
| cwd: desktopDir, | ||
| env: process.env, | ||
| stdio: "inherit", | ||
| }); | ||
|
|
||
| children.set(scriptName, child); | ||
|
|
||
| child.once("error", (error) => { | ||
| console.error(`[desktop-dev] Failed to start ${scriptName}`, error); | ||
| children.delete(scriptName); | ||
| shutdown({ code: 1 }); | ||
| }); | ||
macroscopeapp[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| child.once("exit", (code, signal) => { | ||
| children.delete(scriptName); | ||
|
|
||
| if (shuttingDown) { | ||
| if (code !== null && code !== 0 && exitCode === 0) { | ||
| exitCode = code; | ||
| } | ||
| if (signal !== null && exitSignal === null) { | ||
| exitSignal = signal; | ||
| } | ||
| maybeExit(); | ||
| return; | ||
| } | ||
|
|
||
| if (signal !== null) { | ||
| shutdown({ signal }); | ||
| return; | ||
| } | ||
|
|
||
| shutdown({ code: code ?? 1 }); | ||
| }); | ||
| } | ||
|
|
||
| for (const scriptName of childScriptNames) { | ||
| startChild(scriptName); | ||
| } | ||
|
|
||
| process.once("SIGINT", () => { | ||
| shutdown({ code: 130 }); | ||
| }); | ||
|
|
||
| process.once("SIGTERM", () => { | ||
| shutdown({ code: 143 }); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Signal lost in dev.mjs shutdown, uses exit code insteadLow Severity The SIGINT and SIGTERM handlers call |
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Signal re-delivery caught by own
oncehandlerLow Severity
When a child process dies from a signal during shutdown,
maybeExitcallsprocess.kill(process.pid, exitSignal)to re-signal itself. However, theprocess.once("SIGTERM")handler is still registered and catches that signal, callingshutdown()which returns early sinceshuttingDownis already true. Theoncehandler is then removed, but noprocess.exit()is ever called. The event loop drains and the process exits with code 0 instead of the intended non-zero exit code. The signal handlers need to be removed before re-signaling, e.g. viaprocess.removeAllListenerson the target signal.Additional Locations (1)
apps/desktop/scripts/dev.mjs#L115-L118Reviewed by Cursor Bugbot for commit 16ed6cc. Configure here.