From d0a9979cbdeb9043303d097c01ed446ddb933fa7 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Mon, 17 Aug 2026 12:34:20 +0800 Subject: [PATCH] emrg: escape PowerShell braces in stop_tui ps_cmd (Windows emrg stop crash) --- Agent.md | 2 +- emrg/_stop_all.py | 9 ++++++--- tests/test_stop_all.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Agent.md b/Agent.md index 4c1d7a09..01037191 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` (858) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (859) — 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 98589e79..02e999e6 100644 --- a/emrg/_stop_all.py +++ b/emrg/_stop_all.py @@ -353,13 +353,16 @@ def stop_tui() -> None: (same contract as the POSIX branch's ``own_pid`` exclusion).""" if is_win(): own = os.getpid() + # Literal PowerShell script-block braces must be escaped as {{ }} — + # otherwise str.format() treats them as replacement fields and raises + # ValueError: unexpected '{' in field name at runtime on Windows. ps_cmd = ( "Get-CimInstance Win32_Process | " - "Where-Object { $_.ProcessId -ne {own} -and " + "Where-Object {{ $_.ProcessId -ne {own} -and " "$_.Name -match '^python(\\.exe|w\\.exe)?$' -and " "$_.CommandLine -match '-m emrg' -and " - "$_.CommandLine -notmatch 'emrg\\.server' } | " - "ForEach-Object { Stop-Process -Id $_.ProcessId -Force }" + "$_.CommandLine -notmatch 'emrg\\.server' }} | " + "ForEach-Object {{ Stop-Process -Id $_.ProcessId -Force }}" ).format(own=own) subprocess.run( ["powershell", "-NoProfile", "-Command", ps_cmd], diff --git a/tests/test_stop_all.py b/tests/test_stop_all.py index c1d3f14c..0fcd4153 100644 --- a/tests/test_stop_all.py +++ b/tests/test_stop_all.py @@ -165,6 +165,40 @@ def test_main_exits_with_code(self, monkeypatch): assert exc.value.code == 1 +class TestStopTuiPsTemplate: + """Windows stop_tui() must render its PowerShell template without raising. + + Regression: the template's literal script-block braces (``Where-Object {`` + / ``ForEach-Object {``) were fed to str.format() unescaped, raising + ValueError: unexpected '{' in field name at runtime on Windows — crashing + `emrg stop` before stop_bundled_git + verify ever ran. The existing + TestStopAllExitCode monkeypatches stop_tui entirely, so CI never rendered + the template; this test pins the render path itself. + """ + + def test_stop_tui_renders_ps_template_win(self, monkeypatch): + import os + + calls: list = [] + monkeypatch.setattr(_stop_all, "is_win", lambda: True) + monkeypatch.setattr( + _stop_all.subprocess, "run", + lambda cmd, **kw: calls.append(cmd) or type("CP", (), {})(), + ) + + _stop_all.stop_tui() # must not raise ValueError + + assert len(calls) == 1 + cmd = calls[0] + assert cmd[0] == "powershell" + ps = cmd[-1] + # invoking-PID exclusion substituted into the template + assert f"-ne {os.getpid()}" in ps + # literal PowerShell script-block braces survived the format() call + assert "Where-Object { $_.ProcessId" in ps + assert "ForEach-Object { Stop-Process" in ps + + class TestMainDelegatesToStopAll: def test_emrg_stop_cli_exits_nonzero(self): """`emrg stop` must sys.exit with the stop_all() code (installer gate)."""