fix(win): tree-kill local PTYs so ports free on terminal stop - #10183
fix(win): tree-kill local PTYs so ports free on terminal stop#10183innocarpe wants to merge 0 commit into
Conversation
📝 WalkthroughWalkthroughAdds a Windows-specific 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/ports/workspace-port-ownership.test.ts (1)
45-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPlatform override isn't restored if an assertion fails mid-test.
Both tests restore
process.platformonly after the assertions; if anyexpectthrows first, the override leaks into later tests sincevi.restoreAllMocks()doesn't coverObject.definePropertymutations.♻️ 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
📒 Files selected for processing (8)
src/main/ports/workspace-port-ownership.test.tssrc/main/ports/workspace-port-ownership.tssrc/main/providers/local-pty-provider.test.tssrc/main/providers/local-pty-provider.tssrc/main/pty-descendant-termination.test.tssrc/main/pty-descendant-termination.tssrc/main/windows-process-tree-kill.test.tssrc/main/windows-process-tree-kill.ts
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.
Sync update (
|
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/main/ports/workspace-port-ownership.test.tssrc/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
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.
Sync update (
|
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/main/ports/workspace-port-ownership.test.tssrc/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
| 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) | ||
| } | ||
| }) |
There was a problem hiding this comment.
🩺 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)
+ }
}f8985b5 to
981653f
Compare
|
Accidentally closed when a bad rebase helper force-pushed an empty tip. Restored and continued as #10346. |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Summary
taskkill /T /Fon the ConPTY root before shell kill, sonpm run dev/ Vite children release listening ports.process.kill.nohupchildren 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 /Thelper andkillWithDescendantSweepbranch. 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-providertestspnpm typecheck:nodenpm run devin a worktree terminal → close tab → confirm port free / Project B binds cleanly; Ports UI Stop also frees port