Skip to content

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

@JosXa

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions