Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
resolveElectronLaunchCommand,
} from "./electron-launcher.mjs";
import { acquireRunnerRecord, releaseRunnerRecord } from "./dev-runner-control.mjs";
import { createCrashLoopBreaker } from "./dev-runner-guards.mjs";
import { createCrashLoopBreaker, devAppPattern, devBackendPattern } from "./dev-runner-guards.mjs";
import { waitForResources } from "./wait-for-resources.mjs";

const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();
Expand Down Expand Up @@ -49,6 +49,8 @@ const watchedDirectories = [
{ directory: "../server/dist", files: new Set(["bin.mjs"]) },
];
const forcedShutdownTimeoutMs = 1_500;
const backendShutdownTimeoutMs = 4_000;
const backendPollIntervalMs = 100;
const restartDebounceMs = 120;
const childTreeGracePeriodMs = 1_200;
const orphanCheckIntervalMs = 1_000;
Expand Down Expand Up @@ -124,16 +126,34 @@ function cleanupStaleDevApps() {
return;
}

const escapedDesktopDir = desktopDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
NodeChildProcess.spawnSync(
"pkill",
["-f", "--", `(^| )--t3code-dev-root=${escapedDesktopDir}( |$)`],
{
NodeChildProcess.spawnSync("pkill", ["-f", "--", devAppPattern(desktopDir)], {
stdio: "ignore",
});
}

// pkill and pgrep exit 0 only when the pattern matched a process.
function matchDevBackends(command, args) {
return (
NodeChildProcess.spawnSync(command, [...args, "-f", "--", devBackendPattern(desktopDir)], {
stdio: "ignore",
},
}).status === 0
);
}

async function stopStaleDevBackends() {
if (hostPlatform === "win32" || !matchDevBackends("pkill", ["-TERM"])) {
return;
}

for (let waited = 0; waited < backendShutdownTimeoutMs; waited += backendPollIntervalMs) {
await new Promise((resolve) => setTimeout(resolve, backendPollIntervalMs));
if (!matchDevBackends("pgrep", [])) {
return;
}
}
matchDevBackends("pkill", ["-KILL"]);
}

function startApp() {
if (shuttingDown || currentApp !== null) {
return;
Expand Down Expand Up @@ -194,6 +214,8 @@ function restartAfterFailure(app) {
async function stopApp() {
const app = currentApp;
if (!app) {
// A crashed Electron is already gone, but its backend may still hold the T3 home.
await stopStaleDevBackends();
return;
}

Expand Down Expand Up @@ -228,6 +250,7 @@ async function stopApp() {
finish();
}, forcedShutdownTimeoutMs).unref();
});
await stopStaleDevBackends();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function scheduleRestart() {
Expand Down Expand Up @@ -316,6 +339,7 @@ const orphanCheck = setInterval(() => {

startWatchers();
cleanupStaleDevApps();
await stopStaleDevBackends();
startApp();

process.once("SIGINT", () => {
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/scripts/dev-runner-guards.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import * as NodePath from "node:path";

const RUNNER_SCRIPT_NAME = "dev-electron.mjs";

const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

// `pkill -f` patterns for the Electron apps a runner launches from one checkout.
export function devAppPattern(desktopDir) {
return `(^| )--t3code-dev-root=${escapeRegExp(desktopDir)}( |$)`;
}

// The backend Electron spawns carries no dev-root tag and is the runner's grandchild, so neither
// the tag nor `pkill -P` reaches it once its Electron parent is gone. A hung one keeps the T3
// home's ownership lock and every later start fails with ServerAlreadyRunningError.
export function devBackendPattern(desktopDir) {
const serverEntry = NodePath.join(NodePath.dirname(desktopDir), "server", "dist", "bin.mjs");
return `(^| )${escapeRegExp(serverEntry)} --bootstrap-fd( |$)`;
}

export function parseRunnerRecord(text) {
let parsed;
try {
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/scripts/dev-runner-guards.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { assert, describe, it } from "vite-plus/test";
import {
createCrashLoopBreaker,
decideTakeover,
devBackendPattern,
isVerifiedRunner,
parseRunnerRecord,
} from "./dev-runner-guards.mjs";
Expand Down Expand Up @@ -92,3 +93,20 @@ describe("crash loop breaker", () => {
assert.isTrue(breaker.recordAbnormalExit(4_000).tripped);
});
});

describe("dev process patterns", () => {
const matches = (pattern, command) => new RegExp(pattern).test(command);
const electron =
"/work/a/apps/desktop/.electron-runtime/T3 Code (Dev).app/Contents/MacOS/Electron";

it("matches the orphaned backend of this checkout only", () => {
const pattern = devBackendPattern(desktopDir);
assert.isTrue(
matches(pattern, `${electron} /work/a/apps/server/dist/bin.mjs --bootstrap-fd 3`),
);
assert.isFalse(
matches(pattern, `${electron} /work/b/apps/server/dist/bin.mjs --bootstrap-fd 3`),
);
assert.isFalse(matches(pattern, "node /work/a/apps/server/dist/bin.mjs serve"));
});
});
8 changes: 7 additions & 1 deletion apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import * as NodeVM from "node:vm";

import { assert, describe, it } from "vite-plus/test";

import { devAppPattern, devBackendPattern } from "./dev-runner-guards.mjs";

import {
makeDevelopmentEnvironmentScript,
makeDevelopmentLauncherScript,
Expand Down Expand Up @@ -129,7 +131,9 @@ async function startupCleanupPattern(desktopDir) {
NodeChildProcess: {
execFileSync: () => undefined,
spawnSync: (command, args) => {
if (command === "pkill" && args.includes("-f")) pattern = args.at(-1);
// The dev-root cleanup signals first; later backend cleanup must find nothing to stop.
if (command === "pkill" && args.includes("-f")) pattern ??= args.at(-1);
return { status: 1 };
},
spawn: () => new NodeEvents.EventEmitter(),
},
Expand All @@ -139,6 +143,8 @@ async function startupCleanupPattern(desktopDir) {
desktopDir,
acquireRunnerRecord: async () => ({ claimed: true, stoppedPids: [] }),
releaseRunnerRecord: () => undefined,
devAppPattern,
devBackendPattern,
createCrashLoopBreaker: () => ({
recordAbnormalExit: () => ({ tripped: false }),
rearm: () => undefined,
Expand Down
Loading