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` (914) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (916) — 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
19 changes: 15 additions & 4 deletions emrg/_stop_all.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,17 @@ def _no_window() -> dict:

# ── Process matching ────────────────────────────────────────────

# Windows python interpreter image names (TUI `python.exe` / daemon
# `pythonw.exe`, plus versioned launchers `python3.exe` / `python3.13.exe` /
# `pythonw3.13.exe` / `python3.13w.exe` — bin/emrgd.cmd's fallback chain ends
# at `python-dist\python3.13.exe`, #576). Loose on purpose: the `-m emrg`
# command-line filter is the strong discriminator; the name only pre-filters
# the process list (degraded installs where the pid file is missing/stale are
# exactly the DeleteFile-code-5 scenario #826 targets — a versioned launcher
# must not slip past the scan and keep locking install\ files).
_WIN_PY_NAME_RE = r"^python.*\.exe$"


def match_cmdline(cmd: str) -> bool:
"""True if a command line belongs to an emrg process.

Expand DownExpand Up@@ -184,10 +195,10 @@ def _scan_windows_python_emrg(own_pid: int) -> list[int]:
ps_cmd = (
"Get-CimInstance Win32_Process | "
"Where-Object {{ $_.ProcessId -ne {own} -and "
"$_.Name -match '^python(\\.exe|w\\.exe)?$' -and "
"$_.Name -match '{name_re}' -and "
"$_.CommandLine -match '-m emrg' }} | "
"ForEach-Object {{ Write-Output $_.ProcessId }}"
).format(own=own_pid)
).format(own=own_pid, name_re=_WIN_PY_NAME_RE)
try:
out = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
Expand DownExpand Up@@ -412,11 +423,11 @@ def stop_tui() -> None:
ps_cmd = (
"Get-CimInstance Win32_Process | "
"Where-Object {{ $_.ProcessId -ne {own} -and "
"$_.Name -match '^python(\\.exe|w\\.exe)?$' -and "
"$_.Name -match '{name_re}' -and "
"$_.CommandLine -match '-m emrg' -and "
"$_.CommandLine -notmatch 'emrg\\.server' }} | "
"ForEach-Object {{ Stop-Process -Id $_.ProcessId -Force }}"
).format(own=own)
).format(own=own, name_re=_WIN_PY_NAME_RE)
subprocess.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, **_no_window(),
Expand Down
16 changes: 10 additions & 6 deletions tests/test_installer_stop.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,13 +42,15 @@ def test_stop_all_py_covers_daemon_gui_tui_git_verify():
assert '"taskkill", "/IM", "EMRG.exe"' in content
assert '"taskkill", "/F", "/IM", "EMRG.exe"' in content
assert content.index('"/IM", "EMRG.exe"') < content.index('"/F", "/IM", "EMRG.exe"')
# TUI:CIM 命令行过滤 python.exe|pythonw.exe,排除 emrg.server
assert r"python(\\.exe|w\\.exe)?" in content
# TUI:CIM 命令行过滤 python 解释器镜像(python.exe|pythonw.exe|python3.13.exe
# 等版本化启动器,_WIN_PY_NAME_RE 宽松匹配),排除 emrg.server
assert "_WIN_PY_NAME_RE" in content
assert r'^python.*\.exe$' in content
assert r"-notmatch 'emrg\\.server'" in content
# 调用方自身 PID 排除(`emrg stop` CLI 本身匹配 -m emrg 过滤,会自杀于
# stop_bundled_git + verify 之前 — pm25coder #811 review finding 1)
assert r"$_.ProcessId -ne {own}" in content
assert ".format(own=own)" in content
assert ".format(own=own, name_re=_WIN_PY_NAME_RE)" in content
# bundled git:install\\git\\ 前缀(系统 Git 永不命中)
assert r'\"$env:USERPROFILE\\.emrg\\install\\git\\*\"' in content
assert "stop_bundled_git" in content
Expand All@@ -74,10 +76,12 @@ def test_stop_all_py_cmdline_scan_fallback():
兜底(cmdline 是唯一可靠身份),daemon 步与 verify 步都接入。
"""
content = _read("emrg/_stop_all.py")
# 扫描辅助:python.exe|pythonw.exe + CommandLine 匹配 -m emrg(含 emrg.server),
# 排除自身;不排除 emrg.server(那是 stop_tui 的盲区)
# 扫描辅助:python 解释器镜像(_WIN_PY_NAME_RE 宽松匹配版本化启动器)
# + CommandLine 匹配 -m emrg(含 emrg.server),排除自身;不排除
# emrg.server(那是 stop_tui 的盲区)
assert "def _scan_windows_python_emrg" in content
assert r"python(\\.exe|w\\.exe)?" in content
assert "_WIN_PY_NAME_RE" in content
assert r'^python.*\.exe$' in content
assert r"-match '-m emrg'" in content
assert "Write-Output $_.ProcessId" in content
assert "emrg\\.server" not in content # 绝不能 -notmatch emrg.server
Expand Down
50 changes: 50 additions & 0 deletions tests/test_stop_all.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -302,6 +302,49 @@ def test_renders_template_without_valueerror(self, monkeypatch):
)
assert _stop_all._scan_windows_python_emrg(os.getpid()) == []

def test_name_re_matches_versioned_python_launchers(self):
"""The Windows name pre-filter must match versioned launchers too.

#826 follow-up (pm25coder finding): bin/emrgd.cmd's daemon fallback
chain ends at ``python-dist\\python3.13.exe`` (#576), but the original
``^python(\\.exe|w\\.exe)?$`` pattern missed every versioned name —
a degraded install would run the daemon under a name both stop_daemon()
and verify() ignore, reproducing DeleteFile code 5 with verify clean.
The pattern is loose (the ``-m emrg`` cmdline filter is the strong
discriminator) but must still reject non-python images.
"""
import re

pat = re.compile(_stop_all._WIN_PY_NAME_RE)
for name in (
"python.exe",
"pythonw.exe",
"python3.exe",
"python3w.exe",
"python3.13.exe",
"pythonw3.13.exe",
"python3.13w.exe",
):
assert pat.match(name), f"versioned launcher not matched: {name}"
for name in ("py.exe", "node.exe", "git.exe", "python3.dll"):
assert not pat.match(name), f"non-python image wrongly matched: {name}"

def test_ps_template_embeds_name_re(self, monkeypatch):
"""The rendered PowerShell must actually use the widened pattern."""
calls: list = []
monkeypatch.setattr(_stop_all, "is_win", lambda: True)

def fake_run(cmd, **kw):
calls.append(cmd)
return type("CP", (), {"stdout": ""})()

monkeypatch.setattr(_stop_all.subprocess, "run", fake_run)
_stop_all._scan_windows_python_emrg(1)
ps = calls[0][-1]
assert f"$_.Name -match '{_stop_all._WIN_PY_NAME_RE}'" in ps
# the old narrow pattern must be gone
assert "^python(\\.exe|w\\.exe)?$" not in ps

def test_posix_returns_empty(self, monkeypatch):
monkeypatch.setattr(_stop_all, "is_win", lambda: False)
assert _stop_all._scan_windows_python_emrg(1) == []
Expand DownExpand Up@@ -363,6 +406,13 @@ def test_no_python_residual_when_clean(self, monkeypatch):
_stop_all.subprocess, "run",
lambda cmd, **kw: type("CP", (), {"stdout": ""}),
)
# hermeticity (#738): check_install_writable() probes the REAL
# ~/.emrg/install (hardcoded expanduser path) — on a machine where EMRG
# is installed+running this would report real locked files and the
# "clean" assertion would fail. Isolate it (verified pre-existing on
# master dba96a7, #829's lock-probe addition).
monkeypatch.setattr(_stop_all, "check_install_writable", lambda: [])
monkeypatch.setattr(_stop_all, "_windows_lock_owners", lambda kill, stdout=None: [])
assert _stop_all._verify_windows() == []


Expand Down
Loading