Skip to content

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

Closed

Description

@JosXa

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

Activity

  1. JosXa commented on Jan 21, 2026

    @JosXa
    ContributorAuthor

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

  2. JosXa commented on Jan 21, 2026

    @JosXa
    ContributorAuthor

    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

  3. github-actions commented on Mar 28, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions