From bc332c3eb485269e53fdff0c591a81418f6bdde3 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 27 Aug 2026 16:11:23 +0800 Subject: [PATCH 1/2] =?UTF-8?q?emrg:=20connect=20=E2=80=94=20never=20delet?= =?UTF-8?q?e=20a=20healthy=20daemon's=20token=20in=20cleanup=5Fserver=20(g?= =?UTF-8?q?uard=20on=20fixed-port=20probe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- emrg/connect.py | 18 +++++++++++++++++- tests/test_connect.py | 22 +++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/emrg/connect.py b/emrg/connect.py index 4b6a52db..01126eae 100644 --- a/emrg/connect.py +++ b/emrg/connect.py @@ -102,7 +102,23 @@ async def connect_to_server(): def cleanup_server() -> None: - """Remove the daemon auth token file on shutdown.""" + """Remove the daemon auth token file on shutdown. + + ⚠️ Guards against deleting a HEALTHY daemon's token (rant 2026-08-27T14:48:50): + a mis-triggered spawn cleanup (start_daemon / _start_daemon_background run this + unconditionally) must never remove the token of a daemon that is actually + listening on the fixed port — the fixed port is the single-instance ground + truth, and a transient TCP probe miss while a daemon is alive caused the token + to be deleted and the doomed spawn (EADDRINUSE suicide) never rewrote it. + + Only delete when the fixed-port probe confirms NO daemon is accepting + connections. A fresh daemon start rewrites the token atomically + (_assert_token_file / atomic_write_bytes), so a stale token from a crashed + daemon is harmless on the next successful start. + """ + if is_server_running_sync(): + logger.debug("daemon still listening on the fixed port — keeping token file") + return port_path = Path(get_server_path()) if port_path.exists(): port_path.unlink() diff --git a/tests/test_connect.py b/tests/test_connect.py index c686d15c..2d75e1e9 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -30,8 +30,9 @@ def test_is_exception(self): class TestCleanupServer: def test_removes_token_file(self, monkeypatch, tmp_path): - """Removes the token file if present.""" + """Removes the token file if present (no daemon listening → deletion ok).""" monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) + monkeypatch.setattr("emrg.connect.is_server_running_sync", lambda: False) port_file = tmp_path / f"{CONNECT_ID}.token" port_file.write_text("token", encoding="utf-8") @@ -42,12 +43,14 @@ def test_removes_token_file(self, monkeypatch, tmp_path): def test_noop_when_absent(self, monkeypatch, tmp_path): """Does nothing when the token file doesn't exist.""" monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) + monkeypatch.setattr("emrg.connect.is_server_running_sync", lambda: False) cleanup_server() # must not raise def test_leaves_other_files(self, monkeypatch, tmp_path): """Only removes the token file, not other config files.""" monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) + monkeypatch.setattr("emrg.connect.is_server_running_sync", lambda: False) other = tmp_path / "config.toml" other.write_text("x", encoding="utf-8") port_file = tmp_path / f"{CONNECT_ID}.token" @@ -58,6 +61,23 @@ def test_leaves_other_files(self, monkeypatch, tmp_path): assert other.exists() assert not port_file.exists() + def test_keeps_token_when_daemon_listening(self, monkeypatch, tmp_path): + """rant 2026-08-27T14:48:50 — never delete a HEALTHY daemon's token. + + A mis-triggered spawn cleanup (start_daemon / _start_daemon_background run + cleanup_server() unconditionally) must not remove the token of a daemon + that is actually listening on the fixed port. The doomed spawn (EADDRINUSE + suicide) would never rewrite it → token-missing window for new connections. + """ + monkeypatch.setattr("emrg.connect.config_dir", lambda: tmp_path) + monkeypatch.setattr("emrg.connect.is_server_running_sync", lambda: True) + port_file = tmp_path / f"{CONNECT_ID}.token" + port_file.write_text("token", encoding="utf-8") + + cleanup_server() + + assert port_file.exists(), "token must survive when a daemon is listening" + class TestIsServerRunningSync: """Probes the FIXED daemon port (rant 2026-08-19T08:05:21) — no token-file From e83ec09d582875f5c5f048007cc7333062b87016 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 27 Aug 2026 19:59:49 +0800 Subject: [PATCH 2/2] =?UTF-8?q?emrg:=20connect=20=E2=80=94=20never=20delet?= =?UTF-8?q?e=20a=20healthy=20daemon's=20token=20in=20cleanup=5Fserver=20(g?= =?UTF-8?q?uard=20on=20fixed-port=20probe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased onto master (Agent.md Python 1126→1127 after #1046 merged). --- Agent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Agent.md b/Agent.md index 0e47ec56..f4e10431 100644 --- a/Agent.md +++ b/Agent.md @@ -119,7 +119,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg ``` -Python: `uv run pytest tests/ -v` (1126) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (1127) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (93: 45 daemon_client + 20 conn-manager + 8 integration + 6 build-config + 7 gui-state + 3 preload-api + 4 boot-contract) — syntax: `node --check main.js preload.js daemon_client.js` Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (445: 5 snapshot-store + 9 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 11 markdown + 15 transcript + 7 TranscriptView + 15 history + 22 composer + 14 Composer + 12 sidebar + 17 Sidebar + 9 fileTree + 9 FileTree + 16 resultPanel + 8 ResultPanel + 29 workspaceView + 8 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 9 openSession + 6 WelcomeDialog + 8 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 14 daemonBridge + 7 DaemonBridgeProvider + 24 Shell + 15 DialogHost + 19 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown) + `npm run build` → `renderer/dist/` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)