Skip to content

fix: kill entire process group on quit to prevent stale server processes#656

Open
leszko wants to merge 1 commit intomainfrom
rafal/fix-port-issue
Open

fix: kill entire process group on quit to prevent stale server processes#656
leszko wants to merge 1 commit intomainfrom
rafal/fix-port-issue

Conversation

@leszko
Copy link
Collaborator

@leszko leszko commented Mar 11, 2026

Summary

  • Spawn Python server with detached: true on Unix to create a dedicated process group
  • stopServer() now kills the entire process group (process.kill(-pid, 'SIGKILL')) instead of just sending SIGINT to the direct child (uv), ensuring the Python server is also terminated
  • Adds *.tsbuildinfo to .gitignore

Problem

Users were seeing "No available ports found after 5 attempts starting from 52178" because stopServer() only sent SIGINT to the uv process, leaving the Python server orphaned and holding the port across app restarts.

Test plan

  • Launch the Electron app, verify server starts normally
  • Quit the app (Cmd+Q on macOS, Alt+F4 on Windows), verify no stale Python processes remain (lsof -i :52178)
  • Relaunch the app, verify it starts without port errors
  • Test on Windows to verify taskkill /T /F still works correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved cross-platform process termination reliability to ensure complete cleanup of background processes on both Windows and Unix systems.
  • Chores

    • Updated build configuration to exclude TypeScript incremental build files from version control.

Previously, stopServer() only sent SIGINT to the direct child (uv), leaving
the Python server orphaned and holding the port. This caused "No available
ports found" errors on next launch. Now spawns with detached:true on Unix
and kills the entire process group with SIGKILL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Rafał Leszko <rafal@livepeer.org>
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Updated .gitignore to exclude TypeScript build info files. Refactored Python process spawning and termination logic to create detached process groups on Unix systems, enabling efficient tree-wide process cleanup via negative PID signals instead of per-process termination.

Changes

Cohort / File(s) Summary
Build Configuration
.gitignore
Added *.tsbuildinfo pattern to ignore TypeScript incremental build information files.
Process Management
app/src/services/pythonProcess.ts
Modified spawn configuration to create detached process groups on Unix. Refactored stopServer to use platform-specific termination: taskkill on Windows, process group SIGKILL on Unix. Simplified pid-centric flow with early return and immediate null assignment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A little bunny hops with glee,
Process trees now terminate with such ease,
On Unix or Windows, they all must obey,
Clean signals sent, bugs hop away!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the pull request: fixing the issue of stale server processes by killing the entire process group on quit.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rafal/fix-port-issue

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/services/pythonProcess.ts`:
- Around line 197-220: The stop() method currently sets this.intentionalStop =
true and clears this.serverProcess before verifying there is a PID and before
the old child actually emits its exit events, which can leave the flag stuck or
allow a stale close/error handler to clear a new child; move the lifecycle
changes so you first check if (!this.serverProcess?.pid) return, then perform
the platform-specific kill and only after the child has actually exited (or
after successful kill) set this.serverProcess = null and this.intentionalStop =
true as appropriate. Also update the child event handlers (child.on('close') and
child.on('error')) to only clear this.serverProcess if this.serverProcess ===
child and to reset this.intentionalStop (e.g., this.intentionalStop = false) in
close handling so the flag reflects the real lifecycle.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51193ce6-5b4c-47d4-851e-a4959ece972b

📥 Commits

Reviewing files that changed from the base of the PR and between 4cba0a9 and c14f38c.

📒 Files selected for processing (2)
  • .gitignore
  • app/src/services/pythonProcess.ts

Comment on lines 197 to +220
this.intentionalStop = true; // Mark as intentional to prevent respawn on exit code 42
if (this.serverProcess) {
logger.info('Stopping server...');
const pid = this.serverProcess.pid;

if (process.platform === 'win32' && pid) {
// On Windows, kill the entire process tree using taskkill
// This ensures child processes (like the Python server spawned by uv) are also terminated
try {
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
logger.info(`Killed process tree for PID ${pid}`);
} catch (err) {
logger.warn(`Failed to kill process tree: ${err}`);
// Fallback to regular kill
this.serverProcess.kill('SIGINT');
}
} else {
// On Unix-like systems, SIGINT should propagate to child processes
this.serverProcess.kill('SIGINT');
}

this.serverProcess = null;
if (!this.serverProcess?.pid) return;

logger.info('Stopping server...');
const pid = this.serverProcess.pid;
this.serverProcess = null;

if (process.platform === 'win32') {
// On Windows, kill the entire process tree using taskkill
try {
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
logger.info(`Killed process tree for PID ${pid}`);
} catch (err) {
logger.warn(`Failed to kill process tree: ${err}`);
}
} else {
// On Unix, kill the entire process group (negative PID) since we spawned with detached: true
try {
process.kill(-pid, 'SIGKILL');
logger.info(`Killed process group ${pid}`);
} catch (err) {
logger.warn(`Failed to kill process group: ${err}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move the lifecycle state changes after the PID check and actual child exit.

Setting intentionalStop before the PID guard leaves the flag stuck true on a no-op stop, and clearing serverProcess before the old child emits close lets that stale handler wipe out a replacement child reference. Both cases break restart/error handling.

💡 Proposed fix
 stopServer(): void {
-  this.intentionalStop = true;  // Mark as intentional to prevent respawn on exit code 42
-
-  if (!this.serverProcess?.pid) return;
+  const child = this.serverProcess;
+  if (!child?.pid) return;
 
+  this.intentionalStop = true;  // Mark as intentional to prevent respawn on exit code 42
   logger.info('Stopping server...');
-  const pid = this.serverProcess.pid;
-  this.serverProcess = null;
+  const pid = child.pid;

And in the existing handlers, only clear the field if the event belongs to the currently tracked child:

child.on('close', (code, signal) => {
  const wasIntentionalStop = this.intentionalStop;
  if (this.serverProcess === child) {
    this.serverProcess = null;
  }
  this.intentionalStop = false;
  // ...
});

child.on('error', (err) => {
  // ...
  if (this.serverProcess === child) {
    this.serverProcess = null;
  }
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/pythonProcess.ts` around lines 197 - 220, The stop() method
currently sets this.intentionalStop = true and clears this.serverProcess before
verifying there is a PID and before the old child actually emits its exit
events, which can leave the flag stuck or allow a stale close/error handler to
clear a new child; move the lifecycle changes so you first check if
(!this.serverProcess?.pid) return, then perform the platform-specific kill and
only after the child has actually exited (or after successful kill) set
this.serverProcess = null and this.intentionalStop = true as appropriate. Also
update the child event handlers (child.on('close') and child.on('error')) to
only clear this.serverProcess if this.serverProcess === child and to reset
this.intentionalStop (e.g., this.intentionalStop = false) in close handling so
the flag reflects the real lifecycle.

@github-actions
Copy link
Contributor

🚀 fal.ai Preview Deployment

App ID daydream/scope-pr-656--preview
WebSocket wss://fal.run/daydream/scope-pr-656--preview/ws
Commit c14f38c

Testing

Connect to this preview deployment by running this on your branch:

uv run build && SCOPE_CLOUD_APP_ID="daydream/scope-pr-656--preview/ws" uv run daydream-scope

🧪 E2E tests will run automatically against this deployment.

@github-actions
Copy link
Contributor

✅ E2E Tests passed

Status passed
fal App daydream/scope-pr-656--preview
Run View logs

Test Artifacts

Check the workflow run for screenshots.

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