Skip to content

fix(web): fall back to a free port when the configured one is taken - #65

Open
lenucksi wants to merge 2 commits into
shekohex:mainfrom
lenucksi:fix/pty-server-port-standalone
Open

lenucksi wants to merge 2 commits into
shekohex:mainfrom
lenucksi:fix/pty-server-port-standalone

Conversation

@lenucksi

Copy link
Copy Markdown
Contributor

Problem

The plugin crashes when its configured web-server port (default 4200) is already in use:

  • PTYServer.startWebServer calls Bun.serve({ port, ... }) with no EADDRINUSE handling.
  • The V2 setup autostarts the server with a bare await getOrCreateServer(...) — a busy port rejects PTYServer.createServer, which makes plugin setup throw, i.e. the plugin dies at load.
  • There is neither a fallback to the next free port nor a check for an already-running opencode-pty server on that port.

Reproduced: with one opencode instance already serving on 4200, starting a second instance with the default config crashes its plugin.

Fix

  • Port fallback: keep port: 0/unset semantics (OS-assigned, never conflicts). For an explicit port, try it and on EADDRINUSE walk port+1 … port+10, with port 0 as the final fallback. Every attempt logs which port is busy; the final port is logged too. server.url/getWsUrl are unchanged, so the dynamic port flows into the web UI and /pty-show-server-url correctly.
  • EADDRINUSE detection: checks the Bun error code, with a message-pattern fallback for differently-shaped environments.
  • Existing-server probe (diagnostic only): before binding an explicit port, a short /health probe detects an already-running opencode-pty server and logs that clearly instead of crashing. The bind loop stays the TOCTOU-safe authority. Cross-process "server reuse" is intentionally not attempted: the PTY manager is in-process per plugin instance, so adopting a foreign server would show a foreign session list.
  • Crash-proof V2 setup: the autostart is wrapped in try/catch — on failure it warns and continues; the PTY tools stay fully functional in-process, and later on-demand server invocations get the fallback automatically.

Tests

15 new tests in test/port-fallback.test.ts:

  • configured port taken → resolves, binds the next free port, /health OK on the new port
  • configured port + all 10 fallbacks blocked → OS-assigned port, still healthy
  • port: 0 → random OS-assigned port
  • V2 Plugin.setup with autostart: true and a busy port → resolves, warning logged, getActiveServer() populated
  • pure helpers (resolution, candidate generation incl. 65535 cap, EADDRINUSE detection)

Verification

  • bun run format / lint / typecheck / build:prod — clean
  • bun test — 97 pass; the only failures are the same 4 that also fail on main in this environment (2 package tests need npm, which is absent here; 2 are timing-sensitive and pass in CI)
  • Live check on this host: second instance with PTY_WEB_PORT=4200 while 4200 was held → bound to 4201, healthy, original server untouched, log shows the "already runs / next port" warnings

Standalone PR (based on main, 2 commits) — intentionally not part of #61.

…ash-proof

The plugin crashed when its configured web-server port (default 4200) was
taken: PTYServer.startWebServer called Bun.serve without EADDRINUSE handling
and src/v2/index.ts setup awaited getOrCreateServer without a try/catch, so
a busy port rejected createServer and took the whole plugin down.

Port binding now keeps its semantics (undefined/0 -> OS-assigned ephemeral
port, never a conflict) and for an explicit port retries port+1..port+N
(N=10) before falling back to port 0 as a last resort, logging which ports
were busy and the finally bound port. EADDRINUSE is detected robustly from
Bun's error shape (own code property 'EADDRINUSE', verified at runtime, with
a message-based fallback). Before binding an explicit port, a quick /health
probe on 127.0.0.1 reports whether another opencode-pty instance already
serves it (purely diagnostic; the bind loop stays the TOCTOU-safe authority).
The foreign server is deliberately not reused: the PTY manager is in-process
per plugin instance, so adopting a foreign server would show a foreign
session list.

v2 setup wraps autostart in try/catch and continues on failure: tools keep
working in-process and the server can still come up later, because
getOrCreateServer retries with the new port fallback on the next on-demand
command invocation.

Adds test/port-fallback.test.ts covering the pure helpers (resolveWebPort,
portCandidates, isEaddrinuse), double-bind fallback with health verification,
exhausted-fallback (all 11 ports blocked -> OS-assigned), port 0 semantics,
and v2 setup resilience with a busy configured port.
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.

1 participant