Skip to content

fix(desktop): prevent Electron dev process leaks on restart - #164

Merged
juliusmarminge merged 4 commits into
mainfrom
t3code/fix-desktop-dev-process-leak
Mar 4, 2026
Merged

fix(desktop): prevent Electron dev process leaks on restart#164
juliusmarminge merged 4 commits into
mainfrom
t3code/fix-desktop-dev-process-leak

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Mar 4, 2026

Copy link
Copy Markdown
Member

Summary

  • replace electronmon usage with explicit Electron child-process lifecycle management in dev-electron.mjs
  • add debounced restart orchestration tied to targeted file watchers for desktop and server build outputs
  • implement graceful shutdown with forced-kill fallback (SIGTERM then SIGKILL) and child-tree cleanup to avoid orphaned Electron processes
  • add stale dev-process cleanup on startup using a scoped --t3code-dev-root marker
  • preserve startup gating via wait-on for renderer port and required build artifacts

Testing

  • Not run (not provided in this diff context)
  • Manual sanity checks expected:
    • start desktop dev script and verify Electron launches once after required files/port are ready
    • modify dist-electron/main.js, dist-electron/preload.js, and ../server/dist/index.mjs; verify a single debounced restart per change burst
    • stop the script with SIGINT/SIGTERM; verify no orphan Electron processes remain
    • restart the script repeatedly; verify stale prior dev Electron instances are cleaned up

Note

Medium Risk
Touches the desktop dev runner’s process lifecycle and uses pkill-based cleanup, which could behave differently across platforms or accidentally terminate the wrong processes if the marker argument changes. No production/runtime app logic is modified, but dev workflows could regress (restart/shutdown reliability).

Overview
Replaces electronmon with an explicit Electron process supervisor in apps/desktop/scripts/dev-electron.mjs to stop dev-mode process leaks.

The dev script now launches Electron via spawn, watches specific build outputs (dist-electron/main.js, dist-electron/preload.js, ../server/dist/index.mjs), and performs debounced, serialized restarts (stop then start) when those files change.

Adds graceful shutdown + forced-kill fallback (SIGTERM then SIGKILL) and Unix-only child-process tree cleanup, plus a startup sweep that kills stale Electron dev instances via a scoped --t3code-dev-root=... marker. Removes electronmon from apps/desktop/package.json and updates bun.lock accordingly.

Written by Cursor Bugbot for commit 1633fc6. This will update automatically on new commits. Configure here.

Note

Replace electronmon with a direct Electron launcher in dev-electron.mjs to prevent desktop dev process leaks on restart

Rewrites the dev runner to spawn Electron directly, add debounced (120ms) restarts on file changes, gracefully stop processes with SIGTERM then SIGKILL, and clean up stale Electron dev processes; removes the electronmon dependency and updates the lockfile.

📍Where to Start

Start at the script entry flow in dev-electron.mjs, focusing on startWatchers, startApp, stopApp, and scheduleRestart.

Macroscope summarized 1633fc6.

@coderabbitai

coderabbitai Bot commented Mar 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 54949a20-e00f-40e7-9f25-a806aad864e8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch t3code/fix-desktop-dev-process-leak

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/desktop/scripts/dev-electron.mjs Outdated
Comment on lines +46 to +47
spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${desktopDir}`], { stdio: "ignore" });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium scripts/dev-electron.mjs:46

pkill -f interprets the pattern as an extended regular expression, but desktopDir is inserted verbatim without escaping regex metacharacters. When the path contains . or other regex symbols (common in paths like /home/user/.config/ or dotted directory names), the pattern matches unintended processes. For example, a path /home/user/.dev/app where . matches any character would also match /home/user/Xdev/app. Consider escaping desktopDir for regex use (e.g., replacing . with \.) before passing it to pkill -f.

+  const escapedDesktopDir = desktopDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
-  spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${desktopDir}`], { stdio: "ignore" });
+  spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${escapedDesktopDir}`], { stdio: "ignore" });
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/desktop/scripts/dev-electron.mjs around lines 46-47:

`pkill -f` interprets the pattern as an extended regular expression, but `desktopDir` is inserted verbatim without escaping regex metacharacters. When the path contains `.` or other regex symbols (common in paths like `/home/user/.config/` or dotted directory names), the pattern matches unintended processes. For example, a path `/home/user/.dev/app` where `.` matches any character would also match `/home/user/Xdev/app`. Consider escaping `desktopDir` for regex use (e.g., replacing `.` with `\.`) before passing it to `pkill -f`.

Evidence trail:
1. apps/desktop/scripts/dev-electron.mjs line 46: `spawnSync("pkill", ["-f", "--", `--t3code-dev-root=${desktopDir}`], { stdio: "ignore" });`
2. apps/desktop/scripts/electron-launcher.mjs line 22: `export const desktopDir = resolve(__dirname, "..");`
3. Ubuntu pkill manpage (https://manpages.ubuntu.com/manpages/xenial/man1/pkill.1.html) OPERANDS section: "pattern - Specifies an Extended Regular Expression for matching against the process names or command lines."

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Forced-kill resolves stopApp before process exits
    • Removed the immediate finish() call after SIGKILL so the promise now resolves only when the exit event fires, ensuring the old process has actually exited before a new one is spawned.

Create PR

Or push these changes by commenting:

@cursor push 5728686213
Preview (5728686213)
diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs
--- a/apps/desktop/scripts/dev-electron.mjs
+++ b/apps/desktop/scripts/dev-electron.mjs
@@ -119,7 +119,6 @@
 
       app.kill("SIGKILL");
       killChildTreeByPid(app.pid, "KILL");
-      finish();
     }, forcedShutdownTimeoutMs).unref();
   });
 }

app.kill("SIGKILL");
killChildTreeByPid(app.pid, "KILL");
finish();
}, forcedShutdownTimeoutMs).unref();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forced-kill resolves stopApp before process exits

Low Severity

In the forced-kill timeout path of stopApp(), finish() is called immediately after app.kill("SIGKILL") — before the process has actually exited. This causes stopApp() to return and startApp() to spawn a new Electron instance while the old one is still alive, creating a brief window where two Electron processes run concurrently. This directly contradicts the PR's stated goal of preventing concurrent Electron dev processes. The exit-handler guards (expectedExits, currentApp identity checks) correctly handle the eventual exit of the old process, but the overlap itself is avoidable by waiting for the exit event even after SIGKILL.

Fix in Cursor Fix in Web

- 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
- drop `electronmon` from desktop dependencies
- stop killing stale processes by matching electronmon hook paths
- keep cleanup scoped to `--t3code-dev-root` processes only
- add `childTreeGracePeriodMs` constant for shutdown timing
- await grace period after `TERM` before sending `KILL` to avoid dev process leaks
@juliusmarminge
juliusmarminge force-pushed the t3code/fix-desktop-dev-process-leak branch from 26b775b to 2edd94d Compare March 4, 2026 17:47
- add `electron-updater` dependency for `apps/desktop`
- bump workspace package versions from `0.0.0-alpha.13` to `0.0.0-alpha.16`
- update transitive lockfile entries accordingly

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Shutdown unconditionally blocks for grace period delay
    • Changed the SIGKILL grace period timer to use .unref() and removed the await, so process.exit() runs immediately instead of blocking for 1.2 seconds.
  • ✅ Fixed: Startup order may kill newly spawned app process
    • Moved cleanupStaleDevApps() to run before startWatchers() and startApp(), ensuring stale processes are cleaned up before any new activity begins.

Create PR

Or push these changes by commenting:

@cursor push 3ee9f83046
Preview (3ee9f83046)
diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs
--- a/apps/desktop/scripts/dev-electron.mjs
+++ b/apps/desktop/scripts/dev-electron.mjs
@@ -184,16 +184,13 @@
 
   await stopApp();
   killChildTree("TERM");
-  await new Promise((resolve) => {
-    setTimeout(resolve, childTreeGracePeriodMs);
-  });
-  killChildTree("KILL");
+  setTimeout(() => killChildTree("KILL"), childTreeGracePeriodMs).unref();
 
   process.exit(exitCode);
 }
 
+cleanupStaleDevApps();
 startWatchers();
-cleanupStaleDevApps();
 startApp();
 
 process.once("SIGINT", () => {

await new Promise((resolve) => {
setTimeout(resolve, childTreeGracePeriodMs);
});
killChildTree("KILL");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shutdown unconditionally blocks for grace period delay

Medium Severity

The shutdown function unconditionally awaits a childTreeGracePeriodMs (1.2 second) timer between sending SIGTERM and SIGKILL to the child tree. The old code used .unref() on the timer and called process.exit() immediately, so shutdown was near-instant in the common case. Now every Ctrl+C during development will always take at least 1.2 seconds (plus up to 1.5 seconds in stopApp), even when no straggler processes exist.

Fix in Cursor Fix in Web


startWatchers();
cleanupStaleDevApps();
startApp();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Startup order may kill newly spawned app process

Medium Severity

cleanupStaleDevApps is called after startWatchers but just before startApp. Since cleanupStaleDevApps uses pkill -f matching the --t3code-dev-root argument, and startApp spawns a process with that same argument, if cleanupStaleDevApps were ever reordered after startApp (or called again), it would kill the freshly spawned instance. More critically, on a very fast system, if a prior stale process hasn't fully exited when startApp runs, pkill and the new spawn could race. Swapping the call order to run cleanup first (before watchers too) would make the intent clearer and safer.

Fix in Cursor Fix in Web

@juliusmarminge
juliusmarminge merged commit 7241400 into main Mar 4, 2026
5 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/fix-desktop-dev-process-leak branch March 4, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant