mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Workspace terminal shutdown only signaled an npm wrapper, leaving Electron alive with broken output pipes. Make the signal-handling runner the terminal-owned process and route external Electron termination through the existing quit lifecycle.
201 lines
4.8 KiB
JavaScript
201 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
import net from "node:net";
|
|
import path from "node:path";
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import { createRequire } from "node:module";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
createElectronSpawnOptions,
|
|
registerDevRunnerShutdownSignals,
|
|
resolveChildKillTarget,
|
|
} from "./dev-runner-config.mjs";
|
|
|
|
import { resolveDevElectronArgs } from "./dev-runner-args.mjs";
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const desktopDir = path.resolve(scriptDir, "..");
|
|
const rootDir = path.resolve(desktopDir, "../..");
|
|
const appDir = path.resolve(desktopDir, "../app");
|
|
const require = createRequire(import.meta.url);
|
|
const electron = require("electron");
|
|
|
|
const expoPort = Number(process.env.EXPO_PORT);
|
|
if (!Number.isInteger(expoPort) || expoPort <= 0) {
|
|
console.error("[dev] EXPO_PORT must be set before running desktop dev");
|
|
process.exit(1);
|
|
}
|
|
|
|
const expoDevUrl = process.env.EXPO_DEV_URL || `http://localhost:${expoPort}`;
|
|
const electronArgs = resolveDevElectronArgs(process.platform, process.argv.slice(2));
|
|
const colorEnv = {
|
|
FORCE_COLOR: process.env.FORCE_COLOR || "1",
|
|
npm_config_color: process.env.npm_config_color || "always",
|
|
};
|
|
const devBuildLabel = execFileSync("git", ["branch", "--show-current"], {
|
|
cwd: rootDir,
|
|
encoding: "utf8",
|
|
}).trim();
|
|
|
|
const children = new Map();
|
|
let stopping = false;
|
|
let exitCode = 0;
|
|
|
|
function prefixStream(name, stream, target) {
|
|
let buffered = "";
|
|
stream.setEncoding("utf8");
|
|
stream.on("data", (chunk) => {
|
|
buffered += chunk;
|
|
const lines = buffered.split(/\r?\n/);
|
|
buffered = lines.pop() ?? "";
|
|
for (const line of lines) {
|
|
target.write(line ? `[${name}] ${line}\n` : `[${name}]\n`);
|
|
}
|
|
});
|
|
stream.on("end", () => {
|
|
if (buffered) {
|
|
target.write(`[${name}] ${buffered}\n`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function spawnChild(name, command, args, options = {}) {
|
|
const child = spawn(command, args, {
|
|
cwd: rootDir,
|
|
env: {
|
|
...process.env,
|
|
...colorEnv,
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
...options,
|
|
});
|
|
|
|
const managedChild = {
|
|
process: child,
|
|
detached: options.detached === true,
|
|
};
|
|
children.set(name, managedChild);
|
|
prefixStream(name, child.stdout, process.stdout);
|
|
prefixStream(name, child.stderr, process.stderr);
|
|
|
|
child.on("error", (error) => {
|
|
console.error(`[${name}] failed to start: ${error.message}`);
|
|
exitCode = 1;
|
|
stopAll("SIGTERM");
|
|
});
|
|
|
|
child.on("exit", (code, signal) => {
|
|
children.delete(name);
|
|
if (!stopping) {
|
|
if (code !== 0) {
|
|
exitCode = code ?? 1;
|
|
console.error(`[${name}] exited with ${signal ?? code}`);
|
|
}
|
|
stopAll("SIGTERM");
|
|
}
|
|
});
|
|
|
|
return child;
|
|
}
|
|
|
|
function killChild({ process: child, detached }, signal) {
|
|
if (!child.pid || child.killed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
process.kill(resolveChildKillTarget(child.pid, detached), signal);
|
|
} catch {
|
|
// The child may have exited between the liveness check and the signal.
|
|
}
|
|
}
|
|
|
|
function stopAll(signal) {
|
|
if (stopping) {
|
|
return;
|
|
}
|
|
|
|
stopping = true;
|
|
for (const child of children.values()) {
|
|
killChild(child, signal);
|
|
}
|
|
|
|
const forceKill = setTimeout(() => {
|
|
for (const child of children.values()) {
|
|
killChild(child, "SIGKILL");
|
|
}
|
|
}, 2500);
|
|
forceKill.unref();
|
|
|
|
const finish = setInterval(() => {
|
|
if (children.size === 0) {
|
|
clearInterval(finish);
|
|
process.exit(exitCode);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
async function waitForPort(port, host = "127.0.0.1", timeoutMs = 60_000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
while (Date.now() < deadline) {
|
|
if (await canConnect(port, host)) {
|
|
return;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
|
|
throw new Error(`Timed out waiting for ${host}:${port}`);
|
|
}
|
|
|
|
function canConnect(port, host) {
|
|
return new Promise((resolve) => {
|
|
const socket = net.createConnection({ port, host });
|
|
socket.setTimeout(1000);
|
|
socket.once("connect", () => {
|
|
socket.destroy();
|
|
resolve(true);
|
|
});
|
|
socket.once("timeout", () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
socket.once("error", () => resolve(false));
|
|
});
|
|
}
|
|
|
|
registerDevRunnerShutdownSignals({ signalSource: process, stop: stopAll });
|
|
|
|
spawnChild("metro", "npx", ["expo", "start", "--port", String(expoPort)], {
|
|
cwd: appDir,
|
|
detached: true,
|
|
env: {
|
|
...process.env,
|
|
...colorEnv,
|
|
BROWSER: "none",
|
|
APP_VARIANT: "development",
|
|
EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL: devBuildLabel,
|
|
PASEO_WEB_PLATFORM: "electron",
|
|
},
|
|
});
|
|
|
|
try {
|
|
await waitForPort(expoPort);
|
|
} catch (error) {
|
|
console.error(`[dev] ${error.message}`);
|
|
exitCode = 1;
|
|
stopAll("SIGTERM");
|
|
}
|
|
|
|
if (!stopping) {
|
|
spawnChild(
|
|
"electron",
|
|
electron,
|
|
[...electronArgs, desktopDir],
|
|
createElectronSpawnOptions({
|
|
env: process.env,
|
|
colorEnv,
|
|
expoDevUrl,
|
|
}),
|
|
);
|
|
}
|