Skip to content

emrg: daemon single-instance via fixed-port bind exclusivity (rant 2026-08-19T08:05:21) - #861

Merged
argszero merged 3 commits into
masterfrom
feature/daemon-fixed-port
Aug 19, 2026
Merged

emrg: daemon single-instance via fixed-port bind exclusivity (rant 2026-08-19T08:05:21)#861
argszero merged 3 commits into
masterfrom
feature/daemon-fixed-port

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

Host rant 2026-08-19T08:05:21: the daemon's PID-file single-instance admission is unreliable — multiple daemons have coexisted (4 instances on 08-18; dual instances PID 3924+2592 on 08-19, caused by a stale os.kill(pid,0) liveness probe). Root cause: serve() binds a random port (port=0), so kernel port exclusivity can never engage, and emrgd.pid is a plain file whose content can be overwritten/deleted.

This PR makes the fixed-port bind the only single-instance admission (rant: "唯一机制,无需任何其他兜底"):

  • Daemon (emrg/server/daemon.py): serve() now pre-creates a listening socket on the fixed 127.0.0.1:56031 (EMRGD_PORT). A second bind fails with EADDRINUSE → logs emrgd already running ... fixed-port admission and exits itself. Pure kernel resource exclusivity: nothing to forge, no race window, auto-released on crash.
    • Windows: SO_EXCLUSIVEADDRUSE + SO_REUSEADDR together (fast TIME_WAIT restart and no port hijacking — SO_REUSEADDR alone would allow any socket to steal the port).
    • POSIX: SO_REUSEADDR only (fast restart; two listeners still impossible — SO_REUSEPORT deliberately never set).
    • emrgd.pid is demoted to diagnostics only — written after a successful bind, never used as an admission gate. The old process-name scan (_find_emrg_server_processes), port-file liveness probe, and O_EXCL pid-file admission are all removed (the rant explicitly replaces them: "升级后即唯一生效").
  • Clients (emrg/connect.py): EMRGD_PORT = 56031 constant. connect_to_server() reads only the auth token from emrgd.port and connects to the fixed port; is_server_running_sync() probes the fixed port directly (no file read — a missing/stale port file can no longer hide a live daemon, the exact dual-instance root cause). daemon_manager.py inherits both via delegation (no change needed).
  • stop_all (emrg/_stop_all.py, pure stdlib): _EMRGD_PORT = 56031 (kept in sync by comment); stop_daemon() sends the graceful WS shutdown to the fixed port with the token read from emrgd.port.
  • Tests: admission tests rewritten for the fixed-port bind (EADDRINUSE → exit without pid claim; free port → proceeds; non-EADDRINUSE → propagates; port file contains 56031). test_ws_e2e boots on a free patched port so tests never fight a real daemon. Agent.md count synced.

Acceptance (rant)

  1. Two daemons started simultaneously → only one survives; the other prints emrgd already running and exits. ✅ (bind exclusivity, verified with real sockets)
  2. Restart after crash/kill has no TIME_WAIT stall (Windows too). ✅ (SO_REUSEADDR + SO_EXCLUSIVEADDRUSE)
  3. No file operation can start a second daemon. ✅ (kernel-enforced, no file in the admission path)

Verification

  • Real-socket checks: first bind OK / second bind → EADDRINUSE; liveness probe True on live port / False on closed port.
  • serve() admission paths (mocked bind): EADDRINUSE → _running=False, no pid, websockets serve not reached; free port → pid diagnostic written, serve reached; EACCES → propagated.
  • Full e2e boot on a free port: ping/pong OK, wrong token rejected, port file = fixed port.
  • _stop_all.stop_daemon() against a stub WS server: token read from port file, graceful shutdown sent to the fixed port.
  • CI will validate the full suite (test + test-windows matrix).

@argszero

Copy link
Copy Markdown
OwnerAuthor

Update (post CI-failure discovery): the first CI run caught a Windows-only problem — SO_EXCLUSIVEADDRUSE and SO_REUSEADDR are mutually exclusive on Windows: setting the second setsockopt fails with WSAEINVAL (10022), which crashed serve() on the Windows matrix (OSError: [WinError 10022] in _create_fixed_port_socket).

The rant's intent (fast TIME_WAIT restart and no port hijacking) is preserved with a different mechanism:

  • Windows: SO_EXCLUSIVEADDRUSE only (the anti-hijack option — SO_REUSEADDR alone would allow any local socket to steal the port, violating acceptance emrg: fix dead 256-color branch in style_to_sgr, add test #3).
  • TIME_WAIT fast restart (acceptance emrg: add uninstall support and Windows/WSL install guidance #2): serve() now distinguishes a live daemon from a TIME_WAIT remnant by probing whether the port accepts connections (is_server_running_sync). A live daemon → log emrgd already running + exit. No listener behind the port → bounded retry of the bind (20 × 0.5s ≈ 10s, well under the 30-120s Windows TIME_WAIT stall) → recover or give up gracefully.
  • POSIX: unchanged (SO_REUSEADDR only — fast restart, no hijack possible since it never allows two listeners).

Acceptance mapping still holds: two daemons → one survives; crash-restart has no unbounded TIME_WAIT stall; no file operation can start a second daemon.

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle 1/3 (CI test + test-windows both SUCCESS on 13d8419; fixed-port bind admission, TIME_WAIT probe-retry, and the Windows SO_EXCLUSIVEADDRUSE correction all verified)

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle 2/3 (head 13d8419 unchanged since cycle-1 review, CI test + test-windows PASS on 32201924999, MERGEABLE)

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle 3/3 (head 13d8419 unchanged, CI test + test-windows PASS on 32201924999, MERGEABLE — 3 consecutive LGTMs from cycles 845/846/847)

@argszero
argszero merged commit 1aed66b into masterAug 19, 2026
2 checks passed
@argszero
argszero deleted the feature/daemon-fixed-port branch August 19, 2026 00:44
argszero added a commit that referenced this pull request Aug 19, 2026
Co-authored-by: EMRG Evolution <emrg@argszero.dev>
argszero added a commit that referenced this pull request Aug 21, 2026
…rt (fixed-port admission regression) (#906)
Since #861 (rant 2026-08-19T08:05:21) the daemon binds a FIXED loopback
port (56031) as its single-instance admission, and #884 (rant
2026-08-20T14:32:52) moved the auth credential to emrgd.token (port no
longer carried in any file). The GUI integration suite spawns its OWN
isolated daemon (HOME->tmp) — on any host where the real daemon is
already running (the normal dev-machine state), the isolated daemon
cannot bind 56031 (EADDRINUSE) and exits, so every test times out with
"daemon token file timeout" (7 failures on ). CI stays green
only because runners have no live daemon.
Fix: probe 127.0.0.1:56031 at module load; when a live daemon already
owns the fixed port, skip the whole suite with a clear message (same
mechanism as EMRG_SKIP_INTEGRATION, extended to the live-daemon state —
the isolated-daemon premise is impossible there). CI (daemon-free
runners) still runs the full suite.
Also sync Agent.md GUI test count: 260 -> 254 (renderer.smoke
131->126, i18n 16->15 drifted in #896-#905 without doc re-sync; the
doc-count guard only checks breakdown-sum consistency, not actual
collection).
Verified: pytest 984+1 green, GUI 247 pass / 0 fail (8 skipped on this
daemon-hosting host), import + --help OK.
Sign up for freeto 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

@argszero