Skip to content

fix(win): tree-kill local PTYs so ports free on terminal stop - #10183

Closed
innocarpe wants to merge 0 commit into
stablyai:mainfrom
innocarpe:fix/windows-terminal-port-tree-kill
Closed

fix(win): tree-kill local PTYs so ports free on terminal stop#10183
innocarpe wants to merge 0 commit into
stablyai:mainfrom
innocarpe:fix/windows-terminal-port-tree-kill

Conversation

@innocarpe

Copy link
Copy Markdown
Contributor

Summary

  • On Windows, every local PTY stop runs taskkill /T /F on the ConPTY root before shell kill, so npm run dev / Vite children release listening ports.
  • Ports panel kill also tree-kills on Windows instead of single-PID process.kill.
  • POSIX keeps agent-only descendant snapshot kill; plain POSIX terminals still skip tree-kill so intentional nohup children survive.

Fixes #10150.

Why

Closing a terminal (or switching projects) on Windows only stopped the shell process. Child servers reparented and kept the port, so Project B appeared to serve Project A’s site.

Relationship to #10004 / #10100

Shares the Windows taskkill /T helper and killWithDescendantSweep branch. This PR applies tree-kill to all local Windows PTYs (not only agent sessions) and the Ports kill path, which is what frees dev-server ports. #10100 can be closed as partially superseded once this lands, or restacked to keep agent-specific POSIX notes.

Test plan

  • windows-process-tree-kill, pty-descendant-termination (incl. Windows sweep), workspace-port-ownership, local-pty-provider tests
  • pnpm typecheck:node
  • Manual on Windows: npm run dev in a worktree terminal → close tab → confirm port free / Project B binds cleanly; Ports UI Stop also frees port

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Windows-specific taskkill /T /F helper with timeout and PID validation. Extends descendant sweeping to invoke the Windows tree killer while preserving root cleanup and ownership checks. Updates local PTY shutdown to use asynchronous descendant sweeps, and changes workspace-port shutdown to use tree termination on Windows and SIGTERM on POSIX. Adds tests covering utility behavior, sweep sequencing, PTY shutdown coordination, and workspace-port platform behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary, rationale, and testing, but it omits required template sections like Screenshots, AI Review Report, Security Audit, and Notes. Add the missing template sections, or explicitly mark screenshots as 'No visual change' and include the AI review, security audit, and notes sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the Windows tree-kill fix for local PTYs and port release on terminal stop.
Linked Issues check ✅ Passed The changes implement the Windows port-release fix described in #10150 by tree-killing stopped PTYs and Ports panel processes, with tests covering the behavior.
Out of Scope Changes check ✅ Passed All code changes support the Windows PTY/port-termination fix and associated tests; no unrelated refactors or feature work are evident.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/ports/workspace-port-ownership.test.ts (1)

45-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Platform override isn't restored if an assertion fails mid-test.

Both tests restore process.platform only after the assertions; if any expect throws first, the override leaks into later tests since vi.restoreAllMocks() doesn't cover Object.defineProperty mutations.

♻️ Proposed fix: guarantee restoration via try/finally
   it('on Windows tree-kills the owning process so npm wrappers free the port', async () => {
     const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
     Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
-    scanWorkspacePortsMock.mockResolvedValue({...})
-    terminateWindowsProcessTreeMock.mockResolvedValue(undefined)
-
-    await expect(...).resolves.toEqual({ ok: true })
-    expect(terminateWindowsProcessTreeMock).toHaveBeenCalledWith(4242)
-
-    if (platformDescriptor) {
-      Object.defineProperty(process, 'platform', platformDescriptor)
-    }
+    try {
+      scanWorkspacePortsMock.mockResolvedValue({...})
+      terminateWindowsProcessTreeMock.mockResolvedValue(undefined)
+      await expect(...).resolves.toEqual({ ok: true })
+      expect(terminateWindowsProcessTreeMock).toHaveBeenCalledWith(4242)
+    } finally {
+      if (platformDescriptor) {
+        Object.defineProperty(process, 'platform', platformDescriptor)
+      }
+    }
   })

Apply the equivalent wrapping to the POSIX test (lines 65-85).


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d86942d-4b4c-43cf-984e-455117712dd0

📥 Commits

Reviewing files that changed from the base of the PR and between 8f40ddf and 4d052a8.

📒 Files selected for processing (8)
  • src/main/ports/workspace-port-ownership.test.ts
  • src/main/ports/workspace-port-ownership.ts
  • src/main/providers/local-pty-provider.test.ts
  • src/main/providers/local-pty-provider.ts
  • src/main/pty-descendant-termination.test.ts
  • src/main/pty-descendant-termination.ts
  • src/main/windows-process-tree-kill.test.ts
  • src/main/windows-process-tree-kill.ts

Comment thread src/main/ports/workspace-port-ownership.ts Outdated
innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 23, 2026
terminateWindowsProcessTree is best-effort and always resolves so PTY
teardown is never blocked. Ports UI needs a real outcome: after taskkill,
probe liveness with process.kill(pid, 0) and return failure if the PID
is still alive.

Addresses CodeRabbit review on stablyai#10183.
@innocarpe

Copy link
Copy Markdown
Contributor Author

Sync update (be844668a)

Address CodeRabbit: verify process is gone after Windows tree-kill before Ports kill reports success.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b5b6169-2d45-40f6-a18a-4687ccd7815f

📥 Commits

Reviewing files that changed from the base of the PR and between 4d052a8 and be84466.

📒 Files selected for processing (2)
  • src/main/ports/workspace-port-ownership.test.ts
  • src/main/ports/workspace-port-ownership.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/ports/workspace-port-ownership.test.ts

Comment thread src/main/ports/workspace-port-ownership.ts Outdated
innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 23, 2026
EPERM from process.kill(pid, 0) means the process is still alive without
signal rights; do not report Ports kill success in that case.

Addresses CodeRabbit follow-up on stablyai#10183.
@innocarpe

Copy link
Copy Markdown
Contributor Author

Sync update (f8985b553)

Address CodeRabbit: only ESRCH counts as process-gone after Windows tree-kill; EPERM is failure.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e3350071-89bd-4229-9368-e4eaa5831074

📥 Commits

Reviewing files that changed from the base of the PR and between be84466 and f8985b5.

📒 Files selected for processing (2)
  • src/main/ports/workspace-port-ownership.test.ts
  • src/main/ports/workspace-port-ownership.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/ports/workspace-port-ownership.ts

Comment on lines +92 to +114
it('on Windows reports failure for EPERM liveness probes (still alive)', async () => {
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
scanWorkspacePortsMock.mockResolvedValue({
platform: 'win32',
scannedAt: Date.now(),
ports: [workspacePort(4242, 5173)]
})
terminateWindowsProcessTreeMock.mockResolvedValue(undefined)
vi.spyOn(process, 'kill').mockImplementation(() => {
const err = new Error('kill EPERM') as Error & { code?: string }
err.code = 'EPERM'
throw err
})

await expect(
killWorkspacePort(worktrees, { pid: 4242, port: 5173, repoId: 'repo' })
).resolves.toEqual({ ok: false, reason: 'kill EPERM' })

if (platformDescriptor) {
Object.defineProperty(process, 'platform', platformDescriptor)
}
})

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore process.platform in a finally block.

If the expectation fails, execution never reaches lines 111-113, leaving the global platform set to win32 and contaminating later tests.

Proposed fix
 const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
-Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
-scanWorkspacePortsMock.mockResolvedValue({
-  platform: 'win32',
-  scannedAt: Date.now(),
-  ports: [workspacePort(4242, 5173)]
-})
-terminateWindowsProcessTreeMock.mockResolvedValue(undefined)
-vi.spyOn(process, 'kill').mockImplementation(() => {
-  const err = new Error('kill EPERM') as Error & { code?: string }
-  err.code = 'EPERM'
-  throw err
-})
-
-await expect(
-  killWorkspacePort(worktrees, { pid: 4242, port: 5173, repoId: 'repo' })
-).resolves.toEqual({ ok: false, reason: 'kill EPERM' })
-
-if (platformDescriptor) {
-  Object.defineProperty(process, 'platform', platformDescriptor)
+try {
+  Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
+  // configure mocks and run the assertion
+} finally {
+  if (platformDescriptor) {
+    Object.defineProperty(process, 'platform', platformDescriptor)
+  }
 }

@innocarpe

Copy link
Copy Markdown
Contributor Author

Accidentally closed when a bad rebase helper force-pushed an empty tip. Restored and continued as #10346.

innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 24, 2026
terminateWindowsProcessTree is best-effort and always resolves so PTY
teardown is never blocked. Ports UI needs a real outcome: after taskkill,
probe liveness with process.kill(pid, 0) and return failure if the PID
is still alive.

Addresses CodeRabbit review on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 24, 2026
EPERM from process.kill(pid, 0) means the process is still alive without
signal rights; do not report Ports kill success in that case.

Addresses CodeRabbit follow-up on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 25, 2026
terminateWindowsProcessTree is best-effort and always resolves so PTY
teardown is never blocked. Ports UI needs a real outcome: after taskkill,
probe liveness with process.kill(pid, 0) and return failure if the PID
is still alive.

Addresses CodeRabbit review on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 25, 2026
EPERM from process.kill(pid, 0) means the process is still alive without
signal rights; do not report Ports kill success in that case.

Addresses CodeRabbit follow-up on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 27, 2026
terminateWindowsProcessTree is best-effort and always resolves so PTY
teardown is never blocked. Ports UI needs a real outcome: after taskkill,
probe liveness with process.kill(pid, 0) and return failure if the PID
is still alive.

Addresses CodeRabbit review on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Jul 27, 2026
EPERM from process.kill(pid, 0) means the process is still alive without
signal rights; do not report Ports kill success in that case.

Addresses CodeRabbit follow-up on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Aug 6, 2026
terminateWindowsProcessTree is best-effort and always resolves so PTY
teardown is never blocked. Ports UI needs a real outcome: after taskkill,
probe liveness with process.kill(pid, 0) and return failure if the PID
is still alive.

Addresses CodeRabbit review on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Aug 6, 2026
EPERM from process.kill(pid, 0) means the process is still alive without
signal rights; do not report Ports kill success in that case.

Addresses CodeRabbit follow-up on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Aug 6, 2026
terminateWindowsProcessTree is best-effort and always resolves so PTY
teardown is never blocked. Ports UI needs a real outcome: after taskkill,
probe liveness with process.kill(pid, 0) and return failure if the PID
is still alive.

Addresses CodeRabbit review on stablyai#10183.
innocarpe added a commit to innocarpe/orca that referenced this pull request Aug 6, 2026
EPERM from process.kill(pid, 0) means the process is still alive without
signal rights; do not report Ports kill success in that case.

Addresses CodeRabbit follow-up on stablyai#10183.
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.

[Bug]: Port is not released after stopping/switching project, causing new project to route to previous site

1 participant