import * as NodeChildProcess from "node:child_process";import * as NodeFS from "node:fs";import * as NodeOS from "node:os";import * as NodePath from "node:path"; import { desktopDir, resolveDevProtocolClient, resolveElectronLaunchCommand,} from "./electron-launcher.mjs";import { waitForResources } from "./wait-for-resources.mjs"; const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();if (!devServerUrl) { throw new Error("VITE_DEV_SERVER_URL is required for desktop development.");} const devServer = new URL(devServerUrl);const port = Number.parseInt(devServer.port, 10);if (!Number.isInteger(port) || port <= 0) { throw new Error(`VITE_DEV_SERVER_URL must include an explicit port: ${devServerUrl}`);} const requiredFiles = [ "dist-electron/main.cjs", "dist-electron/electron/WindowsForegroundFocusWorker.cjs", "dist-electron/preload.cjs", "dist-electron/snapShot/GlobalShiftShortcutWorker.cjs", "dist-electron/snapShot/RegionSnapShotWorker.cjs", "dist-electron/snapShot/SnapShotAccessibilityWorker.cjs", "../server/dist/bin.mjs",];const watchedDirectories = [ { directory: "dist-electron", files: new Set(["main.cjs", "preload.cjs"]) }, { directory: "dist-electron/electron", files: new Set(["WindowsForegroundFocusWorker.cjs"]), }, { directory: "dist-electron/snapShot", files: new Set([ "GlobalShiftShortcutWorker.cjs", "RegionSnapShotWorker.cjs", "SnapShotAccessibilityWorker.cjs", ]), }, { directory: "../server/dist", files: new Set(["bin.mjs"]) },];const forcedShutdownTimeoutMs = 1_500;const restartDebounceMs = 120;const childTreeGracePeriodMs = 1_200;const remoteDebuggingPort = process.env.T3CODE_DESKTOP_REMOTE_DEBUGGING_PORT?.trim();// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone dev script has no Effect runtime.const hostPlatform = NodeOS.platform(); NodeChildProcess.execFileSync( process.execPath, [NodePath.join(desktopDir, "scripts/build-browser-secret.mjs")], { stdio: "inherit" },); await waitForResources({ baseDir: desktopDir, files: requiredFiles, tcpHost: devServer.hostname, tcpPort: port,}); const childEnv = { ...process.env };delete childEnv.ELECTRON_RUN_AS_NODE;const devProtocolClient = resolveDevProtocolClient();if (devProtocolClient) { childEnv.T3CODE_DESKTOP_APP_USER_MODEL_ID = devProtocolClient.appBundleId; childEnv.T3CODE_DESKTOP_PROTOCOL_REGISTRATION_MANAGED = "1";} let shuttingDown = false;let restartTimer = null;let currentApp = null;let restartQueue = Promise.resolve();const expectedExits = new WeakSet();const watchers = []; function killChildTreeByPid(pid, signal) { if (hostPlatform === "win32" || typeof pid !== "number") { return; } NodeChildProcess.spawnSync("pkill", [`-${signal}`, "-P", String(pid)], { stdio: "ignore" });} function cleanupStaleDevApps() { if (hostPlatform === "win32") { return; } NodeChildProcess.spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${desktopDir}`], { stdio: "ignore", });} function startApp() { if (shuttingDown || currentApp !== null) { return; } const electronArgs = remoteDebuggingPort ? [`--remote-debugging-port=${remoteDebuggingPort}`] : []; const launchArgs = devProtocolClient ? electronArgs : [...electronArgs, `--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"]; const electronCommand = resolveElectronLaunchCommand(launchArgs); const app = NodeChildProcess.spawn(electronCommand.electronPath, electronCommand.args, { cwd: desktopDir, env: childEnv, stdio: "inherit", }); currentApp = app; app.once("error", () => { if (currentApp === app) { currentApp = null; } if (!shuttingDown) { scheduleRestart(); } }); app.once("exit", (code, signal) => { if (currentApp === app) { currentApp = null; } const exitedAbnormally = signal !== null || code !== 0; if (!shuttingDown && !expectedExits.has(app) && exitedAbnormally) { scheduleRestart(); } });} async function stopApp() { const app = currentApp; if (!app) { return; } currentApp = null; expectedExits.add(app); await new Promise((resolve) => { let settled = false; const finish = () => { if (settled) { return; } settled = true; resolve(); }; app.once("exit", finish); app.kill("SIGTERM"); killChildTreeByPid(app.pid, "TERM"); cleanupStaleDevApps(); setTimeout(() => { if (settled) { return; } app.kill("SIGKILL"); killChildTreeByPid(app.pid, "KILL"); cleanupStaleDevApps(); finish(); }, forcedShutdownTimeoutMs).unref(); });} function scheduleRestart() { if (shuttingDown) { return; } if (restartTimer) { clearTimeout(restartTimer); } restartTimer = setTimeout(() => { restartTimer = null; restartQueue = restartQueue .catch(() => undefined) .then(async () => { await stopApp(); if (!shuttingDown) { startApp(); } }); }, restartDebounceMs);} function startWatchers() { for (const { directory, files } of watchedDirectories) { const watcher = NodeFS.watch( NodePath.join(desktopDir, directory), { persistent: true }, (_eventType, filename) => { if (typeof filename !== "string" || !files.has(filename)) { return; } scheduleRestart(); }, ); watchers.push(watcher); }} function killChildTree(signal) { if (hostPlatform === "win32") { return; } // Kill direct children as a final fallback in case normal shutdown leaves stragglers. NodeChildProcess.spawnSync("pkill", [`-${signal}`, "-P", String(process.pid)], { stdio: "ignore", });} async function shutdown(exitCode) { if (shuttingDown) return; shuttingDown = true; if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; } for (const watcher of watchers) { watcher.close(); } await stopApp(); killChildTree("TERM"); await new Promise((resolve) => { setTimeout(resolve, childTreeGracePeriodMs); }); killChildTree("KILL"); process.exit(exitCode);} startWatchers();cleanupStaleDevApps();startApp(); process.once("SIGINT", () => { void shutdown(130);});process.once("SIGTERM", () => { void shutdown(143);});process.once("SIGHUP", () => { void shutdown(129);});
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import {
desktopDir,
resolveDevProtocolClient,
resolveElectronLaunchCommand,
} from "./electron-launcher.mjs";
import { waitForResources } from "./wait-for-resources.mjs";
const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();
if (!devServerUrl) {
throw new Error("VITE_DEV_SERVER_URL is required for desktop development.");
}
const devServer = new URL(devServerUrl);
const port = Number.parseInt(devServer.port, 10);
if (!Number.isInteger(port) || port <= 0) {
throw new Error(`VITE_DEV_SERVER_URL must include an explicit port: ${devServerUrl}`);
}
const requiredFiles = [
"dist-electron/main.cjs",
"dist-electron/electron/WindowsForegroundFocusWorker.cjs",
"dist-electron/preload.cjs",
"dist-electron/snapShot/GlobalShiftShortcutWorker.cjs",
"dist-electron/snapShot/RegionSnapShotWorker.cjs",
"dist-electron/snapShot/SnapShotAccessibilityWorker.cjs",
"../server/dist/bin.mjs",
];
const watchedDirectories = [
{ directory: "dist-electron", files: new Set(["main.cjs", "preload.cjs"]) },
{
directory: "dist-electron/electron",
files: new Set(["WindowsForegroundFocusWorker.cjs"]),
},
{
directory: "dist-electron/snapShot",
files: new Set([
"GlobalShiftShortcutWorker.cjs",
"RegionSnapShotWorker.cjs",
"SnapShotAccessibilityWorker.cjs",
]),
},
{ directory: "../server/dist", files: new Set(["bin.mjs"]) },
];
const forcedShutdownTimeoutMs = 1_500;
const restartDebounceMs = 120;
const childTreeGracePeriodMs = 1_200;
const remoteDebuggingPort = process.env.T3CODE_DESKTOP_REMOTE_DEBUGGING_PORT?.trim();
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone dev script has no Effect runtime.
const hostPlatform = NodeOS.platform();
NodeChildProcess.execFileSync(
process.execPath,
[NodePath.join(desktopDir, "scripts/build-browser-secret.mjs")],
{ stdio: "inherit" },
);
await waitForResources({
baseDir: desktopDir,
files: requiredFiles,
tcpHost: devServer.hostname,
tcpPort: port,
});
const childEnv = { ...process.env };
delete childEnv.ELECTRON_RUN_AS_NODE;
const devProtocolClient = resolveDevProtocolClient();
if (devProtocolClient) {
childEnv.T3CODE_DESKTOP_APP_USER_MODEL_ID = devProtocolClient.appBundleId;
childEnv.T3CODE_DESKTOP_PROTOCOL_REGISTRATION_MANAGED = "1";
}
let shuttingDown = false;
let restartTimer = null;
let currentApp = null;
let restartQueue = Promise.resolve();
const expectedExits = new WeakSet();
const watchers = [];
function killChildTreeByPid(pid, signal) {
if (hostPlatform === "win32" || typeof pid !== "number") {
return;
}
NodeChildProcess.spawnSync("pkill", [`-${signal}`, "-P", String(pid)], { stdio: "ignore" });
}
function cleanupStaleDevApps() {
if (hostPlatform === "win32") {
return;
}
NodeChildProcess.spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${desktopDir}`], {
stdio: "ignore",
});
}
function startApp() {
if (shuttingDown || currentApp !== null) {
return;
}
const electronArgs = remoteDebuggingPort
? [`--remote-debugging-port=${remoteDebuggingPort}`]
: [];
const launchArgs = devProtocolClient
? electronArgs
: [...electronArgs, `--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"];
const electronCommand = resolveElectronLaunchCommand(launchArgs);
const app = NodeChildProcess.spawn(electronCommand.electronPath, electronCommand.args, {
cwd: desktopDir,
env: childEnv,
stdio: "inherit",
});
currentApp = app;
app.once("error", () => {
if (currentApp === app) {
currentApp = null;
}
if (!shuttingDown) {
scheduleRestart();
}
});
app.once("exit", (code, signal) => {
if (currentApp === app) {
currentApp = null;
}
const exitedAbnormally = signal !== null || code !== 0;
if (!shuttingDown && !expectedExits.has(app) && exitedAbnormally) {
scheduleRestart();
}
});
}
async function stopApp() {
const app = currentApp;
if (!app) {
return;
}
currentApp = null;
expectedExits.add(app);
await new Promise((resolve) => {
let settled = false;
const finish = () => {
if (settled) {
return;
}
settled = true;
resolve();
};
app.once("exit", finish);
app.kill("SIGTERM");
killChildTreeByPid(app.pid, "TERM");
cleanupStaleDevApps();
setTimeout(() => {
if (settled) {
return;
}
app.kill("SIGKILL");
killChildTreeByPid(app.pid, "KILL");
cleanupStaleDevApps();
finish();
}, forcedShutdownTimeoutMs).unref();
});
}
function scheduleRestart() {
if (shuttingDown) {
return;
}
if (restartTimer) {
clearTimeout(restartTimer);
}
restartTimer = setTimeout(() => {
restartTimer = null;
restartQueue = restartQueue
.catch(() => undefined)
.then(async () => {
await stopApp();
if (!shuttingDown) {
startApp();
}
});
}, restartDebounceMs);
}
function startWatchers() {
for (const { directory, files } of watchedDirectories) {
const watcher = NodeFS.watch(
NodePath.join(desktopDir, directory),
{ persistent: true },
(_eventType, filename) => {
if (typeof filename !== "string" || !files.has(filename)) {
return;
}
scheduleRestart();
},
);
watchers.push(watcher);
}
}
function killChildTree(signal) {
if (hostPlatform === "win32") {
return;
}
// Kill direct children as a final fallback in case normal shutdown leaves stragglers.
NodeChildProcess.spawnSync("pkill", [`-${signal}`, "-P", String(process.pid)], {
stdio: "ignore",
});
}
async function shutdown(exitCode) {
if (shuttingDown) return;
shuttingDown = true;
if (restartTimer) {
clearTimeout(restartTimer);
restartTimer = null;
}
for (const watcher of watchers) {
watcher.close();
}
await stopApp();
killChildTree("TERM");
await new Promise((resolve) => {
setTimeout(resolve, childTreeGracePeriodMs);
});
killChildTree("KILL");
process.exit(exitCode);
}
startWatchers();
cleanupStaleDevApps();
startApp();
process.once("SIGINT", () => {
void shutdown(130);
});
process.once("SIGTERM", () => {
void shutdown(143);
});
process.once("SIGHUP", () => {
void shutdown(129);
});