History

Prevent desktop dev Electron process leaks during restarts

- replace `electronmon` with explicit Electron spawn/stop lifecycle management - debounce file-watch restarts for main/preload/server build outputs - add stale-process cleanup and forced child-tree shutdown fallback

Julius Marminge committed 8 months ago

Commit 07aaf371 parent 65a80881 file · +168 −24Browse files

Unified · Split

apps/desktop/scripts/dev-electron.mjs+168−24
@@ -1,43 +1,179 @@
1−import { spawnSync } from "node:child_process";
2−
3−import electronmon from "electronmon";
1+import { spawn, spawnSync } from "node:child_process";
2+import { watch } from "node:fs";
3+import { join } from "node:path";
44 import waitOn from "wait-on";
55
66 import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
77
88 const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5733);
99 const devServerUrl = `http://localhost:${port}`;
10+const requiredFiles = ["dist-electron/main.js", "dist-electron/preload.js", "../server/dist/index.mjs"];
11+const watchedDirectories = [
12+ { directory: "dist-electron", files: new Set(["main.js", "preload.js"]) },
13+ { directory: "../server/dist", files: new Set(["index.mjs"]) },
14+];
15+const forcedShutdownTimeoutMs = 1_500;
16+const restartDebounceMs = 120;
17+const workspaceDir = join(desktopDir, "..", "..");
1018
1119 await waitOn({
12− resources: [
13− `tcp:${port}`,
14− "file:dist-electron/main.js",
15− "file:dist-electron/preload.js",
16− "file:../server/dist/index.mjs",
17− ],
20+ resources: [`tcp:${port}`, ...requiredFiles.map((filePath) => `file:${filePath}`)],
1821 });
1922
2023 const childEnv = { ...process.env };
2124 delete childEnv.ELECTRON_RUN_AS_NODE;
2225
23−const monitor = await electronmon({
24− cwd: desktopDir,
25− args: ["dist-electron/main.js"],
26− env: {
27− ...childEnv,
28− VITE_DEV_SERVER_URL: devServerUrl,
29− },
30− electronPath: resolveElectronPath(),
31−});
32−
3326 let shuttingDown = false;
27+let restartTimer = null;
28+let currentApp = null;
29+let restartQueue = Promise.resolve();
30+const expectedExits = new WeakSet();
31+const watchers = [];
32+
33+function killChildTreeByPid(pid, signal) {
34+ if (process.platform === "win32" || typeof pid !== "number") {
35+ return;
36+ }
37+
38+ spawnSync("pkill", [`-${signal}`, "-P", String(pid)], { stdio: "ignore" });
39+}
40+
41+function cleanupStaleDevApps() {
42+ if (process.platform === "win32") {
43+ return;
44+ }
45+
46+ const patterns = [
47+ `${workspaceDir}/node_modules/.bun/electronmon@.+/node_modules/electronmon/src/hook.js dist-electron/main.js`,
48+ ];
49+
50+ for (const pattern of patterns) {
51+ spawnSync("pkill", ["-f", pattern], { stdio: "ignore" });
52+ }
53+
54+ spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${desktopDir}`], { stdio: "ignore" });
55+}
56+
57+function startApp() {
58+ if (shuttingDown || currentApp !== null) {
59+ return;
60+ }
61+
62+ const app = spawn(
63+ resolveElectronPath(),
64+ [`--t3code-dev-root=${desktopDir}`, "dist-electron/main.js"],
65+ {
66+ cwd: desktopDir,
67+ env: {
68+ ...childEnv,
69+ VITE_DEV_SERVER_URL: devServerUrl,
70+ },
71+ stdio: "inherit",
72+ },
73+ );
74+
75+ currentApp = app;
76+
77+ app.once("error", () => {
78+ if (currentApp === app) {
79+ currentApp = null;
80+ }
81+
82+ if (!shuttingDown) {
83+ scheduleRestart();
84+ }
85+ });
86+
87+ app.once("exit", () => {
88+ if (currentApp === app) {
89+ currentApp = null;
90+ }
91+
92+ if (!shuttingDown && !expectedExits.has(app)) {
93+ scheduleRestart();
94+ }
95+ });
96+}
97+
98+async function stopApp() {
99+ const app = currentApp;
100+ if (!app) {
101+ return;
102+ }
103+
104+ currentApp = null;
105+ expectedExits.add(app);
106+
107+ await new Promise((resolve) => {
108+ let settled = false;
109+
110+ const finish = () => {
111+ if (settled) {
112+ return;
113+ }
114+
115+ settled = true;
116+ resolve();
117+ };
118+
119+ app.once("exit", finish);
120+ app.kill("SIGTERM");
121+ killChildTreeByPid(app.pid, "TERM");
122+
123+ setTimeout(() => {
124+ if (settled) {
125+ return;
126+ }
127+
128+ app.kill("SIGKILL");
129+ killChildTreeByPid(app.pid, "KILL");
130+ finish();
131+ }, forcedShutdownTimeoutMs).unref();
132+ });
133+}
134+
135+function scheduleRestart() {
136+ if (shuttingDown) {
137+ return;
138+ }
139+
140+ if (restartTimer) {
141+ clearTimeout(restartTimer);
142+ }
143+
144+ restartTimer = setTimeout(() => {
145+ restartTimer = null;
146+ restartQueue = restartQueue
147+ .catch(() => undefined)
148+ .then(async () => {
149+ await stopApp();
150+ if (!shuttingDown) {
151+ startApp();
152+ }
153+ });
154+ }, restartDebounceMs);
155+}
156+
157+function startWatchers() {
158+ for (const { directory, files } of watchedDirectories) {
159+ const watcher = watch(join(desktopDir, directory), { persistent: true }, (_eventType, filename) => {
160+ if (typeof filename !== "string" || !files.has(filename)) {
161+ return;
162+ }
163+
164+ scheduleRestart();
165+ });
166+
167+ watchers.push(watcher);
168+ }
169+}
34170
35171 function killChildTree(signal) {
36172 if (process.platform === "win32") {
37173 return;
38174 }
39175
40− // electronmon may leave Electron orphaned when parent shutdown races; kill direct children as fallback.
176+ // Kill direct children as a final fallback in case normal shutdown leaves stragglers.
41177 spawnSync("pkill", [`-${signal}`, "-P", String(process.pid)], { stdio: "ignore" });
42178 }
43179
@@ -45,12 +181,16 @@ async function shutdown(exitCode) { · 1 unmodified line
45181 if (shuttingDown) return;
46182 shuttingDown = true;
47183
48− try {
49− await Promise.race([monitor.destroy(), new Promise((resolve) => setTimeout(resolve, 1_500))]);
50− } catch {
51− // Best effort only; fallback process kill below handles stubborn children.
184+ if (restartTimer) {
185+ clearTimeout(restartTimer);
186+ restartTimer = null;
52187 }
53188
189+ for (const watcher of watchers) {
190+ watcher.close();
191+ }
192+
193+ await stopApp();
54194 killChildTree("TERM");
55195 setTimeout(() => {
56196 killChildTree("KILL");
@@ -59,6 +199,10 @@ async function shutdown(exitCode) { · 2 unmodified lines
59199 process.exit(exitCode);
60200 }
61201
202+startWatchers();
203+cleanupStaleDevApps();
204+startApp();
205+
62206 process.once("SIGINT", () => {
63207 void shutdown(130);
64208 });

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t