From c738de7c7a3539e0464a4ba1712cc05a737bb56c Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Thu, 13 Aug 2026 10:04:45 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20stop-emrg.cmd=20v2=20=E2=80=94=20host-v?= =?UTF-8?q?erified=20fix=20for=20installer=20exit=201=20(rants=2009:56:47/?= =?UTF-8?q?10:00:33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Agent.md | 2 +- bin/stop-emrg.cmd | 124 ++++++++++++++++------------------- tests/test_installer_stop.py | 29 +++++++- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/Agent.md b/Agent.md index daf0a0b8..f31019aa 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` (760) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (761) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (229: 44 daemon_client + 19 conn-manager + 22 app-commands + 107 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — 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/bin/stop-emrg.cmd b/bin/stop-emrg.cmd index f1838fdc..2e1f4c81 100644 --- a/bin/stop-emrg.cmd +++ b/bin/stop-emrg.cmd @@ -1,94 +1,80 @@ @echo off -REM stop-emrg.cmd -- gracefully stop EMRG GUI/TUI/daemon before the installer -REM overwrites ~\.emrg\install files (rant 2026-08-10T08:50:44: Inno Setup got -REM stuck at "stopping existing processes" because the windowless pythonw daemon holds file locks -REM and Inno CloseApplications cannot see it). -REM -REM Order matters (mirrors bin/emrg-uninstall steps 1a/1b): -REM 1. GUI (EMRG.exe): graceful WM_CLOSE first (taskkill without /F), then -REM UNCONDITIONAL /F after the ~5s grace window (host 01:27:07Z: long-lived -REM GUI deferred WM_CLOSE past 5s -- /F must not be gated on a survivor check) -REM 2. TUI (python.exe -m emrg): command-line filter (wmic, PowerShell -REM fallback), excludes the daemon (pythonw.exe -m emrg.server) -REM 3. daemon: `emrg server stop` protocol shutdown via the OLD install's CLI -REM (present since #364 -- version-safe), emrgd.pid poll (<=10s), then -REM taskkill /F /PID fallback -REM 4. bundled git: INLINE guilt-by-association force-kill (single file, -REM rant 2026-08-12T14:00:05 -- stop-git.ps1 merged into this script). -REM Host 2026-08-11T19:47:44 FINAL decision OVERRIDES #689: install success -REM has priority -- if sh/vim hold msys-2.0.dll and block the git-tree kill, -REM they are killed too; only the install\git\ prefix is touched, system Git -REM in Program Files is never affected. Step 4 runs PowerShell inline (same -REM \" escaping as the TUI/verify snippets below, proven on v0.2.25-v0.2.27); -REM every pass re-queries Get-CimInstance (no stale snapshot) and residual -REM processes are reported truthfully via exit 1 (rant 2026-08-12T12:30:41). -REM -REM Returns 0 when nothing EMRG-related survives; 1 if a process could not be -REM stopped (installer aborts with a clear message instead of hanging). -REM Safe on clean install: no old install dir -> everything is skipped -> 0. +chcp 65001 >nul +REM stop-emrg.cmd -- fixed v2: no nested-parenthesis %VAR% expansion bugs +REM (v1 failed: "10 was unexpected" + "no emrg.cmd" -- %VAR% inside ( ) blocks +REM expands at parse time. Use labels + !VAR! delayed expansion instead.) setlocal enabledelayedexpansion set "EMRG_DIR=%USERPROFILE%\.emrg" set "INSTALL=%EMRG_DIR%\install" set "EXIT_CODE=0" +echo [stop-emrg] ============ begin ============ +echo [stop-emrg] EMRG_DIR=%EMRG_DIR% +echo [stop-emrg] INSTALL=%INSTALL% -REM --- 1. GUI: graceful WM_CLOSE, then unconditional /F after the grace window --- -REM (host report 2026-08-10T01:27:07Z: a long-lived ~15h GUI session deferred -REM WM_CLOSE past the 5s window -- two full script runs did not terminate it -REM while a direct `taskkill /IM EMRG.exe` did. => the /F fallback must be -REM UNCONDITIONAL after the wait so an old GUI session can never hold the -REM installer hostage. When the GUI is not running, the graceful taskkill -REM returns errorlevel 1 (skip the wait) and the /F below is a fast no-op.) +echo [1] check GUI (EMRG.exe)... taskkill /IM EMRG.exe >nul 2>&1 if not errorlevel 1 ( - REM give the GUI up to ~5s to exit cleanly (ping = portable sleep) + echo [1] graceful found, wait 5s... ping -n 6 127.0.0.1 >nul 2>&1 ) +echo [1] force-kill GUI (taskkill /F)... taskkill /F /IM EMRG.exe >nul 2>&1 +echo [1] GUI done (exit=%errorlevel%) -REM --- 2. TUI: python.exe -m emrg (daemon is pythonw.exe -m emrg.server, excluded) --- +echo [2] check TUI (python -m emrg)... where wmic >nul 2>&1 -if not errorlevel 1 ( - REM %% = literal % in batch files (wmic LIKE wildcard) - wmic process where "name='python.exe' and commandline like '%%-m emrg%%' and commandline not like '%%emrg.server%%'" call terminate >nul 2>&1 -) else ( - powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" | Where-Object { $_.CommandLine -match '-m emrg' -and $_.CommandLine -notmatch 'emrg\.server' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }" >nul 2>&1 -) +if not errorlevel 1 goto :tui_wmic +echo [2] kill TUI via PowerShell... +powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" | Where-Object { $_.CommandLine -match '-m emrg' -and $_.CommandLine -notmatch 'emrg\.server' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }" >nul 2>&1 +echo [2] PowerShell done (exit=%errorlevel%) +goto :tui_done +:tui_wmic +echo [2] kill TUI via wmic... +wmic process where "name='python.exe' and commandline like '%%-m emrg%%' and commandline not like '%%emrg.server%%'" call terminate >nul 2>&1 +echo [2] wmic done (exit=%errorlevel%) +:tui_done -REM --- 3. daemon: protocol shutdown via old install's CLI, pid poll, /F fallback --- -if exist "%INSTALL%\bin\emrg.cmd" ( - call "%INSTALL%\bin\emrg.cmd" server stop -) -if not exist "%EMRG_DIR%\emrgd.pid" goto :verify +echo [3] check daemon... +if exist "%INSTALL%\bin\emrg.cmd" goto :daemon_stop +echo [3] no emrg.cmd at %INSTALL%\bin -- skip protocol stop +goto :daemon_pid +:daemon_stop +echo [3] call emrg server stop... +call "%INSTALL%\bin\emrg.cmd" server stop +echo [3] emrg server stop done (exit=%errorlevel%) +:daemon_pid +if exist "%EMRG_DIR%\emrgd.pid" goto :daemon_pid_wait +echo [3] no emrgd.pid (daemon not running) -- skip, continue to step 4 +goto :step4 +:daemon_pid_wait set /a TRIES=0 :wait_pid -if not exist "%EMRG_DIR%\emrgd.pid" goto :verify +if not exist "%EMRG_DIR%\emrgd.pid" goto :pid_gone set /a TRIES+=1 -if %TRIES% geq 10 goto :kill_pid +if !TRIES! geq 10 goto :kill_pid ping -n 2 127.0.0.1 >nul 2>&1 goto :wait_pid :kill_pid set "DPID=" for /f "usebackq delims=" %%p in ("%EMRG_DIR%\emrgd.pid") do set "DPID=%%p" -if defined DPID taskkill /F /PID %DPID% >nul 2>&1 +if defined DPID ( + echo [3] force-kill daemon PID !DPID!... + taskkill /F /PID !DPID! >nul 2>&1 + echo [3] daemon kill done (exit=%errorlevel%) +) +:pid_gone +echo [3] daemon pid gone -REM --- 4. bundled git: inline guilt-by-association force-kill (single file) --- -REM (host 2026-08-11T19:47:44 FINAL decision OVERRIDES #689's snapshot-only -REM approach: install success has priority -- if sh/vim hold msys-2.0.dll under -REM install\git\ and block the git-tree kill, they are killed too; only the -REM install\git\ prefix is touched, system Git (Program Files) is never matched. -REM Rant 2026-08-12T14:00:05: stop-git.ps1 deleted, logic inlined below. -REM Each pass re-queries Get-CimInstance (no stale snapshot); the survivor -REM check uses the latest snapshot; residual -> exit 1 (truthful failure #701). -REM The \" escaping matches the TUI/verify inline snippets proven on real -REM Windows in v0.2.25-v0.2.27.) -powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='SilentlyContinue'; $prefix=\"$env:USERPROFILE\.emrg\install\git\*\"; Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like $prefix -and $_.Name -in @('git.exe','ssh.exe','plink.exe','bash.exe') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; Start-Sleep -Milliseconds 300; Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like $prefix } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; Start-Sleep -Milliseconds 300; $left = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like $prefix }); if ($left.Count -gt 0) { $left | ForEach-Object { Write-Host (\"still running: {0} (pid {1})\" -f $_.Name, $_.ProcessId) }; exit 1 }; exit 0" >nul 2>&1 +:step4 +echo [4] check+kill bundled git (install\git)... +powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='SilentlyContinue'; $prefix=\"$env:USERPROFILE\.emrg\install\git\*\"; Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like $prefix -and $_.Name -in @('git.exe','ssh.exe','plink.exe','bash.exe') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; Start-Sleep -Milliseconds 300; Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like $prefix } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }; Start-Sleep -Milliseconds 300; $left = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like $prefix }); if ($left.Count -gt 0) { $left | ForEach-Object { Write-Host (\"still running: {0} (pid {1})\" -f $_.Name, $_.ProcessId) }; exit 1 }; exit 0" +echo [4] git kill done (exit=%errorlevel%) if errorlevel 1 set "EXIT_CODE=1" - :verify +echo [verify] check residual GUI... tasklist /FI "IMAGENAME eq EMRG.exe" 2>nul | findstr /i "EMRG.exe" >nul && set "EXIT_CODE=1" -REM %-variables inside a parenthesized block expand at parse time (after set "DPID=" they hold the old/empty value) -> must use -REM enabledelayedexpansion !DPID! (runtime expansion); if defined itself is a runtime check. +echo [verify] GUI residual (EXIT_CODE=%EXIT_CODE%) if exist "%EMRG_DIR%\emrgd.pid" ( set "DPID=" for /f "usebackq delims=" %%p in ("%EMRG_DIR%\emrgd.pid") do set "DPID=%%p" @@ -96,10 +82,10 @@ if exist "%EMRG_DIR%\emrgd.pid" ( tasklist /FI "PID eq !DPID!" 2>nul | findstr /i "!DPID!" >nul && set "EXIT_CODE=1" ) ) -REM Bundled-git survival check (plain prefix; guilt-by-association semantics): -REM any process still under install\git\ -> exit 1 (installer aborts). Same -REM condition as step 4's inline PowerShell; kept as belt-and-braces in case the -REM PowerShell invocation itself fails. -powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like \"$env:USERPROFILE\.emrg\install\git\*\" }; if ($p) { exit 1 }" >nul 2>&1 +echo [verify] check residual git... +powershell -NoProfile -Command "$p = Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like \"$env:USERPROFILE\.emrg\install\git\*\" }; if ($p) { $p | ForEach-Object { Write-Host (\" [verify] residual: {0} (pid {1})\" -f $_.Name, $_.ProcessId) }; exit 1 }" +echo [verify] git residual (exit=%errorlevel%) if errorlevel 1 set "EXIT_CODE=1" + +echo [stop-emrg] ============ end (EXIT_CODE=%EXIT_CODE%) ============ endlocal & exit /b %EXIT_CODE% diff --git a/tests/test_installer_stop.py b/tests/test_installer_stop.py index e1e1efa6..cc431ed3 100644 --- a/tests/test_installer_stop.py +++ b/tests/test_installer_stop.py @@ -46,7 +46,7 @@ def test_stop_emrg_cmd_covers_gui_tui_daemon_and_inline_step4(): assert content.index("taskkill /IM EMRG.exe") < daemon_line # step 4(rant 2026-08-12T14:00:05 合并单文件):内联 PowerShell 连坐强杀。 # 判别:-Command 内联(无 -File 独立脚本),\" 转义与 TUI/verify 段同模式。 - step4_idx = content.index("REM --- 4. bundled git: inline guilt-by-association") + step4_idx = content.index("\n:step4\n") # v2 label(宿主 2026-08-13 真机验证版:标签结构替代注释头) verify_idx = content.index("\n:verify\n") # 标签定义处(\n 前缀排除前面 goto :verify 行) assert daemon_line < step4_idx < verify_idx assert "-ExecutionPolicy Bypass -Command" in content @@ -87,7 +87,7 @@ def test_stop_git_merged_single_file(): # rant 2026-08-12T14:00:05 验收:bin/ 下无 stop-git.ps1;grep stop-git 仅历史注释 assert not (REPO_ROOT / "bin" / "stop-git.ps1").exists() cmd = (REPO_ROOT / "bin" / "stop-emrg.cmd").read_text(encoding="utf-8") - step4 = cmd[cmd.index("REM --- 4. bundled git:"):cmd.index("\n:verify\n")] + step4 = cmd[cmd.index("\n:step4\n"):cmd.index("\n:verify\n")] # pass 2:连坐强杀——前缀内全部残留(sh/vim 一并杀,宿主 2026-08-11T19:47:44 拍板) assert "Start-Sleep -Milliseconds 300" in step4 # 每次枚举重新 Get-CimInstance(不用旧快照)——pass1/pass2/$left 共 3 次 @@ -103,6 +103,31 @@ def test_stop_git_merged_single_file(): assert 'cp "$ROOT/bin/stop-git.ps1"' not in br +def test_stop_emrg_v2_step4_always_runs(): + """R127 / rant 2026-08-13T09:56:47 + 10:00:33 — host-verified v2 (EXIT_CODE=0): + step 4 (kill bundled git) must ALWAYS run before :verify. The old + `if not exist emrgd.pid goto :verify` skipped step 4 when the daemon was + down → orphaned evolution-spawned git/sh/vim processes locked install\\git + → verify reported them → exit 1 → installer aborted.""" + content = (REPO_ROOT / "bin" / "stop-emrg.cmd").read_text(encoding="utf-8") + # step 4 unconditional: daemon section falls through via goto :step4, never to :verify + daemon_end = content.index("goto :step4") + verify_idx = content.index("\n:verify\n") + step4_idx = content.index("\n:step4\n") + assert daemon_end < step4_idx < verify_idx + # no direct daemon→verify jump exists (only the :verify label, no early goto :verify) + # (v1 had `if not exist "%EMRG_DIR%\\emrgd.pid" goto :verify` — must be gone) + assert 'goto :verify' not in content + # paren-block %VAR% parse-time expansion fixed: !TRIES! delayed expansion for the loop guard + assert "if !TRIES! geq 10 goto :kill_pid" in content + # label structure replaces nested parens for TUI (wmic/PowerShell) + daemon stop + assert ":tui_wmic" in content and ":tui_done" in content + assert ":daemon_stop" in content and ":daemon_pid" in content + # host diagnostics preserved (每步 echo [N] check/kill/result) + assert "echo [4] check+kill bundled git" in content + assert "echo [verify] git residual" in content + + def test_emrgd_cmd_has_stop_branch(): content = (REPO_ROOT / "bin" / "emrgd.cmd").read_text(encoding="utf-8") assert 'if /I not "%~1"=="stop" goto :start' in content