Windows: Ghost port bindings when server killed via SIGTERM (affects plugins like openralph) #7094

Open
opened 2026-02-16 18:06:10 -05:00 by yindo · 2 comments
Owner

Originally created by @JosXa on GitHub (Jan 21, 2026).

Originally assigned to: @thdxr on GitHub.

Summary

The opencode serve command lacks signal handlers for graceful shutdown, causing ghost port bindings on Windows when the process is terminated by external tools. This affects plugins like openralph and opencode-ralph that spawn OpenCode as a subprocess and send SIGTERM when exiting.

Problem

When these plugins terminate, they send SIGTERM to the OpenCode server process. However, opencode serve has no signal handlers to catch this and call server.stop(), so:

  1. The process dies immediately without closing the TCP listener
  2. On Windows, the port binding persists as a "ghost" - bound to a non-existent PID
  3. Restarting the server fails with Failed to start server on port XXXX
  4. The only workarounds are: wait 2-5 minutes, use a different port, or reboot

Observed Behavior

$ opencode serve --port 4190
Error: Failed to start server on port 4190

$ netstat -ano | find "4190"
TCP    127.0.0.1:4190    0.0.0.0:0    LISTENING    464972

$ tasklist /FI "PID eq 464972"
INFO: No tasks are running which match the specified criteria.

The port is bound to a PID that no longer exists - a ghost binding that Windows won't release.

Root Cause

File: packages/opencode/src/cli/cmd/serve.ts

export async function serveCmd(args: Arguments<ServeOptions>) {
  const server = await Server.listen({
    port: args.port,
    hostname: args.hostname,
  })
  
  logger.info(`Server started at http://${args.hostname}:${args.port}`)
  
  await new Promise(() => {})  // Waits forever
  
  server.stop()  // Never called
}

There are no handlers for SIGINT, SIGTERM, or SIGBREAK. When a plugin sends SIGTERM, the process exits immediately without calling server.stop().

Proposed Fix

Add signal handlers to ensure graceful shutdown:

export async function serveCmd(args: Arguments<ServeOptions>) {
  const server = await Server.listen({
    port: args.port,
    hostname: args.hostname,
  })
  
  logger.info(`Server started at http://${args.hostname}:${args.port}`)
  
  const shutdown = (signal: string) => {
    logger.info(`Received ${signal}, shutting down...`)
    server.stop()
    process.exit(0)
  }
  
  process.on('SIGINT', () => shutdown('SIGINT'))
  process.on('SIGTERM', () => shutdown('SIGTERM'))
  process.on('SIGBREAK', () => shutdown('SIGBREAK'))  // Windows Ctrl+Break
  
  await new Promise(() => {})
}

Additionally, enabling reusePort: true in Bun.serve() would allow immediate port reuse even after abnormal termination, providing defense-in-depth.

Environment

Reproduction Steps

  1. Use openralph or opencode-ralph to spawn opencode serve --port 4190
  2. Exit the plugin (which sends SIGTERM to opencode)
  3. Restart the plugin
  4. Observe: Failed to start server on port 4190

Impact

  • Severity: High for Windows users
  • User Experience: Very poor - requires system restart in worst case
  • Affects all plugins/tools that spawn OpenCode as a subprocess
Originally created by @JosXa on GitHub (Jan 21, 2026). Originally assigned to: @thdxr on GitHub. ## Summary The `opencode serve` command lacks signal handlers for graceful shutdown, causing ghost port bindings on Windows when the process is terminated by external tools. This affects plugins like [openralph](https://github.com/shuv1337/openralph) and [opencode-ralph](https://github.com/hona/opencode-ralph) that spawn OpenCode as a subprocess and send SIGTERM when exiting. ## Problem When these plugins terminate, they send SIGTERM to the OpenCode server process. However, `opencode serve` has no signal handlers to catch this and call `server.stop()`, so: 1. The process dies immediately without closing the TCP listener 2. On Windows, the port binding persists as a "ghost" - bound to a non-existent PID 3. Restarting the server fails with `Failed to start server on port XXXX` 4. The only workarounds are: wait 2-5 minutes, use a different port, or reboot ### Observed Behavior ``` $ opencode serve --port 4190 Error: Failed to start server on port 4190 $ netstat -ano | find "4190" TCP 127.0.0.1:4190 0.0.0.0:0 LISTENING 464972 $ tasklist /FI "PID eq 464972" INFO: No tasks are running which match the specified criteria. ``` The port is bound to a PID that no longer exists - a ghost binding that Windows won't release. ## Root Cause **File**: `packages/opencode/src/cli/cmd/serve.ts` ```typescript export async function serveCmd(args: Arguments<ServeOptions>) { const server = await Server.listen({ port: args.port, hostname: args.hostname, }) logger.info(`Server started at http://${args.hostname}:${args.port}`) await new Promise(() => {}) // Waits forever server.stop() // Never called } ``` There are no handlers for SIGINT, SIGTERM, or SIGBREAK. When a plugin sends SIGTERM, the process exits immediately without calling `server.stop()`. ## Proposed Fix Add signal handlers to ensure graceful shutdown: ```typescript export async function serveCmd(args: Arguments<ServeOptions>) { const server = await Server.listen({ port: args.port, hostname: args.hostname, }) logger.info(`Server started at http://${args.hostname}:${args.port}`) const shutdown = (signal: string) => { logger.info(`Received ${signal}, shutting down...`) server.stop() process.exit(0) } process.on('SIGINT', () => shutdown('SIGINT')) process.on('SIGTERM', () => shutdown('SIGTERM')) process.on('SIGBREAK', () => shutdown('SIGBREAK')) // Windows Ctrl+Break await new Promise(() => {}) } ``` Additionally, enabling `reusePort: true` in `Bun.serve()` would allow immediate port reuse even after abnormal termination, providing defense-in-depth. ## Environment - **OS**: Windows 11 - **OpenCode Version**: 1.1.29 - **Affected Plugins**: - [shuv1337/openralph](https://github.com/shuv1337/openralph) - [hona/opencode-ralph](https://github.com/hona/opencode-ralph) ## Reproduction Steps 1. Use openralph or opencode-ralph to spawn `opencode serve --port 4190` 2. Exit the plugin (which sends SIGTERM to opencode) 3. Restart the plugin 4. Observe: `Failed to start server on port 4190` ## Impact - **Severity**: High for Windows users - **User Experience**: Very poor - requires system restart in worst case - Affects all plugins/tools that spawn OpenCode as a subprocess
yindo added the windows label 2026-02-16 18:06:10 -05:00
Author
Owner

@JosXa commented on GitHub (Jan 21, 2026):

yo opencode, can you propose a fix in a PR?

@JosXa commented on GitHub (Jan 21, 2026): yo opencode, can you propose a fix in a PR?
Author
Owner

@JosXa commented on GitHub (Jan 21, 2026):

Summary

I've implemented the fix for the ghost port bindings issue on Windows.

Changes

  • packages/opencode/src/cli/cmd/serve.ts - Added signal handlers for SIGINT, SIGTERM, and SIGBREAK (Windows Ctrl+Break) that gracefully call server.stop() before exiting
  • packages/opencode/src/server/server.ts - Added reusePort: true to Bun.serve() for defense-in-depth, allowing immediate port reuse even after abnormal termination

Pull Request

Created PR #9866: fix: add signal handlers for graceful shutdown in serve command

@JosXa commented on GitHub (Jan 21, 2026): ## Summary I've implemented the fix for the ghost port bindings issue on Windows. ## Changes - **`packages/opencode/src/cli/cmd/serve.ts`** - Added signal handlers for `SIGINT`, `SIGTERM`, and `SIGBREAK` (Windows Ctrl+Break) that gracefully call `server.stop()` before exiting - **`packages/opencode/src/server/server.ts`** - Added `reusePort: true` to `Bun.serve()` for defense-in-depth, allowing immediate port reuse even after abnormal termination ## Pull Request Created PR #9866: [fix: add signal handlers for graceful shutdown in serve command](https://github.com/anomalyco/opencode/pull/9866) <!-- opencode-agent:{"v":1,"replyTo":3780194465,"timestamp":"2026-01-21T18:13:08.444Z"} -->
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#7094