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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions