Skip to content

Stale backend holding port blocks restart -> dashboard 502 (start doesn't reap own unhealthy listener; lsof-gated reap no-ops on Windows) #108

Description

@smoochy

Summary

A wedged claude-smart backend that still holds its port (8071) in LISTEN but no longer answers /health permanently blocks restart. Every subsequent backend-service.sh start fails to bind (WinError 10048 / EADDRINUSE) and the dashboard returns 502 on /preferences (and other data routes) because its proxy target is dead. start never self-heals this, even though the holder is our own stale process.

Distinct from #107: #107 is start no-op'ing under IDE entrypoints; this is start giving up when the port is held by a dead-but-listening reflexio backend.

Reproduction / evidence

Wedged old backend (PID answering TCP, not /health):

$ Get-NetTCPConnection -LocalPort 8071
State        OwningProcess
Listen       12144
CloseWait    12144
Established  12144
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8071/health
000

Fresh start then loses the bind race:

[backend ] INFO: Application startup complete.
[backend ] ERROR: [Errno 10048] error while attempting to bind on address ('0.0.0.0', 8071): ...
[backend ] exited with code 1

backend-service.sh status reports not running (health probe fails), yet start refuses to act because port_occupied is true. Manual Stop-Process -Id 12144 + start recovers immediately.

Root cause

Three compounding gaps in plugin/scripts/backend-service.sh:

  1. start never reaps a stale own-backend. The start) case does:

    if is_our_backend_running; then emit_ok; exit 0; fi
    if port_occupied; then
      # logs "held by another process; skipping start"
      emit_ok; exit 0
    fi

    is_our_backend_running is false (no /health), so it falls into port_occupied and gives up. reap_port_listeners only runs inside full_stop (the stop/restart path), never here. A held-but-unhealthy port that matches our own *reflexio*/*uvicorn* cmdline should be reaped and retried, not skipped.

  2. reap_port_listeners depends on lsof (command -v lsof || return 0). On Windows git-bash lsof is absent, so reap silently no-ops — on the exact platform where this occurred. Needs a Windows fallback (netstat -ano / Get-NetTCPConnection -> taskkill /F).

  3. Empty/stale PID file means full_stop's kill_group "$(cat "$PID_FILE")" has nothing to kill, so even an explicit restart can't clear the wedged process on Windows (reap is the only other path, and it's lsof-gated per refactor(cite): rank+fingerprint citation ids + uninstall subcommand #2).

Suggested fix

  • In the start) case, when port_occupied but ! backend_healthy and the holder cmdline matches our reflexio/uvicorn patterns, reap it and retry the bind (reuse reap_port_listeners), instead of emit_ok; exit 0. Keep the current "foreign holder -> skip" behavior for non-matching cmdlines.
  • Give reap_port_listeners / port_holder a Windows fallback when lsof is missing (netstat -ano to map port->PID, tasklist to match the command, taskkill /F /PID).

Environment

  • claude-smart v0.2.48
  • Claude Code VS Code extension, Windows 11 (git-bash)
  • uv / Python 3.12 present

Related: #107 (same file, _lib.sh/backend-service.sh service-startup robustness). Happy to open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions