Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (869) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (871) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (257: 45 daemon_client + 19 conn-manager + 22 app-commands + 129 renderer smoke + 16 i18n + 7 integration + 3 commands + 7 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
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 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)
Expand Down
23 changes: 18 additions & 5 deletions emrg/_stop_all.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,16 +19,23 @@

Steps (mirroring the old stop-emrg.cmd flow, all best-effort):

- daemon: ws protocol ``shutdown`` → ``~/.emrg/emrgd.pid`` → SIGTERM /
``taskkill /F /PID`` → 3s poll; port file removed once dead
- GUI: Windows ``taskkill /IM EMRG.exe`` graceful → unconditional
``/F``; POSIX ps-scan (``EMRG.app`` / ``EMRG-*.AppImage``)
- TUI: Windows CIM filter ``python.exe|pythonw.exe -m emrg`` (not
``emrg.server``); POSIX ps-scan
- daemon: ws protocol ``shutdown`` → ``~/.emrg/emrgd.pid`` → SIGTERM /
``taskkill /F /PID`` → 3s poll; port file removed once dead
- bundled git: Windows ``install\\git\\`` prefix kill (git/ssh/plink/bash
+ fallback prefix full-kill — port of stop-emrg.cmd step 4)
- verify: residual scan; any survivor → ``exit 1`` with a named list
(installer aborts and shows the log, R125 semantics)

Order is deliberate: **clients (GUI/TUI) first, daemon LAST** (host rant
2026-08-17T14:15:33). Both GUI and TUI auto-spawn the daemon when they
detect it missing — stopping the daemon first while clients are alive makes
them immediately re-spawn it, so the stop "stops nothing" and the installer
still hits locked files. With the daemon last, no client remains to bring
it back, and verify() sees the true final state.
"""

from __future__ import annotations
Expand DownExpand Up@@ -453,13 +460,19 @@ def verify() -> list[str]:
# ── Orchestration ───────────────────────────────────────────────

def stop_all() -> int:
"""Run every stop step, then verify. Returns 0 (clean) or 1 (residuals)."""
print("emrg stop: stopping daemon ...")
stop_daemon()
"""Run every stop step, then verify. Returns 0 (clean) or 1 (residuals).

Clients (GUI/TUI) are stopped FIRST and the daemon LAST (rant
2026-08-17T14:15:33): both clients auto-spawn the daemon when it
disappears, so stopping the daemon first would let a live client
immediately bring it back — leaving locked files for the installer.
"""
print("emrg stop: stopping GUI ...")
stop_gui()
print("emrg stop: stopping TUI clients ...")
stop_tui()
print("emrg stop: stopping daemon ...")
stop_daemon()
if is_win():
print("emrg stop: stopping bundled git under install\\git ...")
stop_bundled_git()
Expand Down
38 changes: 38 additions & 0 deletions tests/test_stop_all.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,6 +165,44 @@ def test_main_exits_with_code(self, monkeypatch):
assert exc.value.code == 1


class TestStopAllOrder:
"""stop_all() must stop clients FIRST and the daemon LAST (rant
2026-08-17T14:15:33): GUI/TUI auto-spawn the daemon when they notice it
missing, so stopping the daemon first would let a live client re-spawn
it — the installer then still hits locked files."""

def _record_order(self, monkeypatch):
order: list[str] = []

def _rec(name: str):
def _fn(*a, **k):
order.append(name)
return _fn

monkeypatch.setattr(_stop_all, "stop_gui", _rec("stop_gui"))
monkeypatch.setattr(_stop_all, "stop_tui", _rec("stop_tui"))
monkeypatch.setattr(_stop_all, "stop_daemon", _rec("stop_daemon"))
monkeypatch.setattr(_stop_all, "stop_bundled_git", _rec("stop_bundled_git"))
monkeypatch.setattr(_stop_all, "verify", lambda: [])
monkeypatch.setattr(_stop_all, "is_win", lambda: True)
return order

def test_clients_before_daemon(self, monkeypatch):
order = self._record_order(monkeypatch)
assert stop_all() == 0
assert order == ["stop_gui", "stop_tui", "stop_daemon", "stop_bundled_git"]
# daemon MUST come after both clients — a client alive when the
# daemon dies would re-spawn it (auto-spawn mechanisms in GUI/TUI)
assert order.index("stop_daemon") > order.index("stop_gui")
assert order.index("stop_daemon") > order.index("stop_tui")

def test_posix_skips_bundled_git(self, monkeypatch):
order = self._record_order(monkeypatch)
monkeypatch.setattr(_stop_all, "is_win", lambda: False)
assert stop_all() == 0
assert order == ["stop_gui", "stop_tui", "stop_daemon"]


class TestStopTuiPsTemplate:
"""Windows stop_tui() must render its PowerShell template without raising.

Expand Down
Loading