From 14034bdd345ddcfc87017abead77fde77e773706 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Tue, 18 Aug 2026 07:43:17 +0800 Subject: [PATCH 1/2] emrg: stop_all match versioned python launchers in Windows scans --- Agent.md | 2 +- emrg/_stop_all.py | 19 +++++++++++--- tests/test_installer_stop.py | 16 +++++++----- tests/test_stop_all.py | 50 ++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/Agent.md b/Agent.md index 338f993c..e884ad24 100644 --- a/Agent.md +++ b/Agent.md @@ -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 路径不受影响) diff --git a/emrg/_stop_all.py b/emrg/_stop_all.py index 60c39bfd..d471dff5 100644 --- a/emrg/_stop_all.py +++ b/emrg/_stop_all.py @@ -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. @@ -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], @@ -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(), diff --git a/tests/test_installer_stop.py b/tests/test_installer_stop.py index d5931c08..f13b8e8e 100644 --- a/tests/test_installer_stop.py +++ b/tests/test_installer_stop.py @@ -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 @@ -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 diff --git a/tests/test_stop_all.py b/tests/test_stop_all.py index e6d11664..dac50e06 100644 --- a/tests/test_stop_all.py +++ b/tests/test_stop_all.py @@ -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) == [] @@ -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() == [] From 5f0c0f117fc9d0cf20da6b946a019039c6aafff0 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Tue, 18 Aug 2026 07:50:04 +0800 Subject: [PATCH 2/2] emrg: fix invalid escape sequences in test docstring (cosmetic) --- tests/test_stop_all.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_stop_all.py b/tests/test_stop_all.py index dac50e06..cc98344b 100644 --- a/tests/test_stop_all.py +++ b/tests/test_stop_all.py @@ -306,8 +306,8 @@ 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 — + 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