opencode icon indicating copy to clipboard operation
opencode copied to clipboard

git bash.exe processes on Windows get orphaned

Open nikitakot opened this issue 1 month ago • 5 comments

Description

Processes which are launched on Windows via Git bash.exe get orphaned.

OpenCode version

1.0.152

Steps to reproduce

  1. start opencode
  2. ask it to run npx serve
  3. interrupt the execution
  4. the bash.exe will still be running and will still be occupying the default serve 3000 localhost port

Screenshot and/or share link

Image

Operating System

Windows 11, pwsh, wezterm

Terminal

wezterm

nikitakot avatar Dec 14 '25 15:12 nikitakot

This issue might be a duplicate of existing issues. Please check:

  • #4384: Process sometimes doesn't close after response is finished (Windows-specific, involves process cleanup issues)
  • #3763: [Windows] interactive call breaking tui (Windows Bash-related process issues)

Feel free to ignore if none of these address your specific case.

github-actions[bot] avatar Dec 14 '25 15:12 github-actions[bot]

Root cause analysis

This is the same underlying issue as #3057 but Windows-specific. The process cleanup code has inconsistent implementations:

Shell cleanup (shell/shell.ts) — Robust

process.kill(-pid, "SIGTERM")      // Kill process group
await Bun.sleep(200)
if (!opts?.exited?.()) {
  process.kill(-pid, "SIGKILL")    // Escalate if still alive
}

LSP cleanup (lsp/client.ts) — Weak

input.server.process.kill()        // Just SIGTERM, no group, no escalation

Why orphans happen

  1. No process group kill (-pid) — Child processes spawned by the parent (e.g., tsserver from typescript-language-server, or subshells from bash.exe) are not targeted
  2. No SIGKILL escalation — If SIGTERM is ignored, the process survives
  3. No exit confirmation — Code assumes .kill() works but never verifies

Windows-specific note

On Windows, the shell module has a fallback path using taskkill:

if (process.platform === "win32") {
  const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"])
  // ...
}

The /t flag kills the process tree. This pattern should also be applied to LSP cleanup on Windows.

Related: #3057 (general process cleanup discussion with suggested fix)

anntnzrb avatar Dec 21 '25 00:12 anntnzrb

Opened PRs to address this:

  • #5875 - Fix LSP process cleanup (uses Shell.killTree() + detached: true)
  • #5876 - Fix PTY process cleanup (adds killPtyTree() with taskkill /t on Windows)

Both now properly kill process trees on Windows using taskkill /pid <pid> /f /t.

anntnzrb avatar Dec 21 '25 00:12 anntnzrb

@anntnzrb I dont think the lsps are having an orphan issue? I think it’s just the bash tool

rekram1-node avatar Dec 21 '25 01:12 rekram1-node

@rekram1-node You're right that the user-reported symptom (scripts running after Ctrl+C) is specifically about the bash tool - that's a separate abort race condition where void kill() is fire-and-forget.

However, the LSP fix addresses a different scenario I encountered: when using git worktrees with parallel subagents, each worktree spawns its own set of LSPs. After the session ends, orphaned tsserver processes remained because:

  1. LSP servers weren't spawned with detached: true (needed for process group kill)
  2. shutdown() used plain process.kill() instead of Shell.killTree()

So the LSP PR (#5875) addresses LSP cleanup on session disposal, while the bash issue (#3057) is about abort during execution. They're related but different code paths.

Happy to close the LSP PR if you don't think it's needed though!

anntnzrb avatar Dec 21 '25 01:12 anntnzrb

maybe im misunderstanding but Because we dont spawn lsps with detached true i dont think we need special kill logic

or am i misunderstanding u?

Ur diagnosis seemed ai generated so im trying to make sure we are both actually understanding source of issue instead of relying on its response

rekram1-node avatar Dec 21 '25 02:12 rekram1-node

Fair point - let me clarify with a concrete example of what I observed:

  1. I was running opencode in multiple git worktrees simultaneously (parallel subagents)
  2. Each worktree instance spawned typescript-language-server which spawns tsserver as a child
  3. After closing opencode, I ran ps aux | grep tsserver and saw 7+ orphaned tsserver processes still running

The current shutdown() calls process.kill() which sends SIGTERM to typescript-language-server, but its child tsserver survives because:

  • Without detached: true, the child isn't in a killable process group
  • Without Shell.killTree(), there's no SIGKILL escalation if SIGTERM is ignored

My PR adds both:

  1. detached: true when spawning → makes the process a group leader
  2. Shell.killTree() on shutdown → kills the group with -pid and escalates to SIGKILL

You're right that without detached: true, the group kill won't work - that's why the PR adds both together.

anntnzrb avatar Dec 21 '25 02:12 anntnzrb

Okay cool, yeah these days if I read a big AI diagnosis I really need to be careful because most of the time it's slop, you're response here sounds a lot more sane hahaa so thank you!

I'll take a look at those

rekram1-node avatar Dec 21 '25 02:12 rekram1-node