Skip to content

fix(cli): handle SIGHUP to prevent orphaned processes on terminal close#12718

Open
shjeong92 wants to merge 1 commit intoanomalyco:devfrom
shjeong92:fix/handle-sighup-prevent-orphaned-processes
Open

fix(cli): handle SIGHUP to prevent orphaned processes on terminal close#12718
shjeong92 wants to merge 1 commit intoanomalyco:devfrom
shjeong92:fix/handle-sighup-prevent-orphaned-processes

Conversation

@shjeong92
Copy link

What does this PR do?

Fixes #10563, relates to #11225, #11527

When a terminal tab/window is closed, the OS sends SIGHUP to the process group. OpenCode has no SIGHUP handler, so the process ignores it and stays alive as an orphan under launchd (PPID=1), consuming memory indefinitely.

This adds SIGHUP and SIGTERM handlers to all long-running entry points:

  • index.ts: Safety net with 3s timeout fallback (covers all commands)
  • thread.ts: Graceful worker shutdown before exit (TUI mode)
  • worker.ts: Shutdown + ppid polling to detect parent death (safety net for SIGKILL)
  • attach.ts: Exit on signal (opencode attach mode)
  • serve.ts: Exit on signal (opencode serve mode)

The layered approach works like this: command-specific handlers do graceful cleanup and exit immediately, while the index.ts safety net force-exits after 3s if a command-specific handler hangs.

How did you verify your code works?

  • All 897 existing tests pass, 0 regressions
  • tsc --noEmit passes with no type errors
  • Added signal handling tests that spawn actual opencode serve, send SIGHUP/SIGTERM, and verify graceful exit (exit code 0 vs 129 without fix)
  • Manual test: start opencode, note PID, send kill -HUP <pid>, confirm process exits

@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

The following comment was made by an LLM, it may be inaccurate:

Based on my search, here are the related PRs that might be duplicates or closely related:

Potentially Related PRs

  1. fix: prevent orphaned worker process when killed #11603 - fix: prevent orphaned worker process when killed

  2. fix: force kill MCP server processes on dispose to prevent orphan processes #7424 - fix: force kill MCP server processes on dispose to prevent orphan processes

  3. fix(pty): use proper process tree cleanup on disposal #5876 - fix(pty): use proper process tree cleanup on disposal

Note: PR #12718 (the current PR) appears to be a comprehensive solution addressing the same orphaned process issue that previous PRs have attempted to fix. Check if #11603 and #7424 are closed/merged and whether this PR supersedes them or if there's overlapping work.

@shjeong92 shjeong92 force-pushed the fix/handle-sighup-prevent-orphaned-processes branch from 4591da7 to ecdb049 Compare February 8, 2026 16:27
@luisrudge
Copy link

yeah I was just hit with this issue as well.

You have 68 opencode processes running, most dating back to Jan 28 - Feb 1. This happens when you close terminal windows without explicitly exiting opencode (Ctrl+C or /exit) - the processes keep running in the background.
These are consuming:

  • ~10-15% total CPU (even while idle)
  • ~8GB+ RAM combined
    To clean them up, you can kill all except the current session:

Kill all opencode processes except the current one (PID 76275)

ps aux | grep opencode | grep -v grep | grep -v 76275 | awk '{print $2}' | xargs kill

@goniz
Copy link
Contributor

goniz commented Feb 11, 2026

Hmm I think that calling process.exit() explicitly skips some shutdown procedures which probably can cause corrupted writes (stdout/files etc)

Additionally it skips finally() blocks and other background work since it stops the event loop and exits synchronously.

Here is the Node docs which transfer to Bun as well: https://nodejs.org/api/process.html#processexitcode

@shjeong92
Copy link
Author

@goniz Thanks for the feedback! You're right — I've updated the signal handlers to avoid calling process.exit() directly. They now remove their own listener and re-raise the signal so the default OS handler runs, allowing finally blocks and pending I/O to complete normally.

The only place process.exit() remains is in the index.ts safety net, which fires after a 3s timeout as a last resort if graceful shutdown hangs.

@shjeong92 shjeong92 force-pushed the fix/handle-sighup-prevent-orphaned-processes branch 2 times, most recently from df894ca to f07b5c9 Compare February 15, 2026 00:50
Comment on lines 11 to 17
for (const signal of ["SIGHUP", "SIGTERM"] as const) {
const handler = () => {
process.off(signal, handler)
process.kill(process.pid, signal)
}
process.on(signal, handler)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Check if you can use the process.once() instead of doing this dance.
If not, extract to a utility function and document the pattern

Copy link
Author

@shjeong92 shjeong92 Feb 17, 2026

Choose a reason for hiding this comment

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

Good cal. switched all signal handlers to process.once() across the codebase (serve.ts, attach.ts, thread.ts, worker.ts, index.ts). No more manual off dance.

if (ppid > 1) {
const monitor = setInterval(() => {
try {
process.kill(ppid, 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this can lie due to race conditions. What if the parent dies and his pid gets recycled?

You can try checking if the processed moved to live under init as ppid / p group? Check me

Copy link
Author

@shjeong92 shjeong92 Feb 17, 2026

Choose a reason for hiding this comment

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

You're right. the stale PID + kill(pid, 0) approach is vulnerable to PID recycling. Replaced it with a live process.ppid check each interval: when the worker gets reparented to init (ppid === 1), we know the parent is gone. Also gated behind process.platform !== "win32" since ppid semantics don't apply there.

@shjeong92 shjeong92 force-pushed the fix/handle-sighup-prevent-orphaned-processes branch from 7b5e903 to ebeb3df Compare February 17, 2026 07:37
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.

Memory Leak: Orphaned processes when terminal closes without explicit exit

3 participants

Comments