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` (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 路径不受影响)
Expand Down
124 changes: 55 additions & 69 deletions bin/stop-emrg.cmd
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,91 @@
@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"
if defined DPID (
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%
29 changes: 27 additions & 2 deletions tests/test_installer_stop.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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 次
Expand All@@ -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
Expand Down
Loading