-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
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:
- The process dies immediately without closing the TCP listener
- On Windows, the port binding persists as a "ghost" - bound to a non-existent PID
- Restarting the server fails with
Failed to start server on port XXXX - 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
- OS: Windows 11
- OpenCode Version: 1.1.29
- Affected Plugins:
Reproduction Steps
- Use openralph or opencode-ralph to spawn
opencode serve --port 4190 - Exit the plugin (which sends SIGTERM to opencode)
- Restart the plugin
- 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