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 number Diff line number Diff line change
Expand Up @@ -93,7 +93,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` (670) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (674) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (107: 29 daemon_client + 22 app-commands + 31 renderer smoke + 15 i18n + 7 integration + 3 commands) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` + 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
2 changes: 1 addition & 1 deletion README.cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ EMRG 不只是追赶——它自己追上来。
git clone https://github.com/argszero/emrg.git
cd emrg
uv sync # 安装依赖
uv run pytest tests/ -v # 跑测试(当前 670 项)
uv run pytest tests/ -v # 跑测试(当前 674 项)
uv run python -m emrg # 启动 TUI
# CI 含 actionlint workflow 门禁(#444):workflow 解析错误在 PR 即失败

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ EMRG doesn't just keep up — it catches up on its own.
git clone https://github.com/argszero/emrg.git
cd emrg
uv sync # install deps
uv run pytest tests/ -v # run tests (currently 670 items)
uv run pytest tests/ -v # run tests (currently 674 items)
uv run python -m emrg # launch TUI
# CI includes actionlint workflow gate (#444): workflow parse errors fail PR CI

Expand Down
8 changes: 8 additions & 0 deletions bin/emrgd.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ REM (RotatingFileHandler),StreamHandler 仅附加,无控制台不影响日
set PYEXE=%DIR%\python-dist\pythonw.exe
if not exist "%PYEXE%" set PYEXE=%DIR%\python-dist\python.exe
if not exist "%PYEXE%" set PYEXE=%DIR%\python-dist\python3.13.exe
REM R124: `emrgd.cmd stop` — 优雅关闭 daemon(rant 2026-08-10T08:50:44 安装器预停止):
REM 复用 CLI `emrg server stop`(协议 shutdown → ping-pid SIGTERM 兜底,见
REM emrg/__main__.py _stop_daemon)。pythonw 无控制台,print 丢弃无害,shutdown 无需 stdout。
REM 用标签而非括号块,保证 %errorlevel% 在 python 退出后才展开。
if /I not "%~1"=="stop" goto :start
"%PYEXE%" -m emrg server stop
exit /b %errorlevel%
:start
"%PYEXE%" -m emrg.server %*
70 changes: 70 additions & 0 deletions bin/stop-emrg.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@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 "停止已有进程" 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),
REM /F fallback after ~5s if still alive
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
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.
setlocal enabledelayedexpansion
set "EMRG_DIR=%USERPROFILE%\.emrg"
set "INSTALL=%EMRG_DIR%\install"

REM --- 1. GUI: graceful WM_CLOSE, then /F fallback ---
taskkill /IM EMRG.exe >nul 2>&1
if not errorlevel 1 (
REM give the GUI up to ~5s to exit cleanly (ping = portable sleep)
ping -n 6 127.0.0.1 >nul 2>&1
)
tasklist /FI "IMAGENAME eq EMRG.exe" 2>nul | findstr /i "EMRG.exe" >nul
if not errorlevel 1 taskkill /F /IM EMRG.exe >nul 2>&1

REM --- 2. TUI: python.exe -m emrg (daemon is pythonw.exe -m emrg.server, excluded) ---
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
)

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
set /a TRIES=0
:wait_pid
if not exist "%EMRG_DIR%\emrgd.pid" goto :verify
set /a TRIES+=1
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

:verify
set "EXIT_CODE=0"
tasklist /FI "IMAGENAME eq EMRG.exe" 2>nul | findstr /i "EMRG.exe" >nul && set "EXIT_CODE=1"
REM 括号块内 %-变量 在块解析时展开(set "DPID=" 之后取到旧值/空值)→ 必须用
REM enabledelayedexpansion 的 !DPID!(运行时展开);if defined 本身是运行时判定。
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"
)
)
endlocal & exit /b %EXIT_CODE%
1 change: 1 addition & 0 deletions packaging/build-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ cp -R "$PY_ROOT/." "$DIST/bin/python-dist/"
cp "$ROOT/bin/emrgd" emrgd
cp "$ROOT/bin/emrg.cmd" emrg.cmd 2>/dev/null || true
cp "$ROOT/bin/emrgd.cmd" emrgd.cmd 2>/dev/null || true
cp "$ROOT/bin/stop-emrg.cmd" stop-emrg.cmd 2>/dev/null || true
cp "$ROOT/bin/emrg-uninstall" emrg-uninstall
chmod +x emrg emrgd emrg-uninstall
)
Expand Down
30 changes: 30 additions & 0 deletions packaging/make-installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ Compression=lzma2
SolidCompression=yes
[Files]
Source: "$STAGE_WIN/payload\\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion
; R124: dontcopy — 供 [Code] PrepareToInstall 在覆盖文件前 ExtractTemporaryFile 取出并
; 运行 bin\stop-emrg.cmd(升级安装前优雅关闭 GUI/TUI/daemon,rant 2026-08-10T08:50:44:
; pythonw daemon 锁文件导致卡在"停止已有进程")。正常安装时该文件仍由上方通配符
; 装入 {app}\bin\stop-emrg.cmd。
Source: "$STAGE_WIN/payload\\bin\\stop-emrg.cmd"; DestDir: "{tmp}"; Flags: dontcopy
[Icons]
Name: "{userprograms}\\EMRG"; Filename: "{app}\\emrg-gui\\EMRG\\EMRG.exe"; IconFilename: "{app}\\emrg-gui\\EMRG\\EMRG.exe"
[UninstallRun]
Expand Down Expand Up @@ -357,6 +362,31 @@ begin
if CurUninstallStep = usPostUninstall then
RemoveBinDirFromPath;
end;

// R124: 升级安装前优雅关闭运行中的 EMRG 进程(rant 2026-08-10T08:50:44)——
// Inno CloseApplications 看不到无窗口的 pythonw daemon(emrgd.cmd → pythonw.exe
// -m emrg.server 常驻锁文件),覆盖 ~/.emrg\install 时卡在"停止已有进程"。
// PrepareToInstall 在安装开始前运行 bin\stop-emrg.cmd:taskkill EMRG.exe
// 优雅→/F 兜底、wmic/PowerShell 命令行过滤 TUI、emrg server stop 协议关闭
// daemon + emrgd.pid 轮询兜底(顺序 GUI→TUI→daemon)。干净安装(无旧 install)
// 脚本自行跳过。返回非空字符串 = 中止安装并显示该消息(宁可中止也不卡死)。
// {cmd} = cmd.exe(Inno 预定义常量,批处理文件须经 cmd 启动)。
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
StopScript: string;
begin
Result := '';
ExtractTemporaryFile('stop-emrg.cmd');
StopScript := ExpandConstant('{tmp}\\stop-emrg.cmd');
if Exec(ExpandConstant('{cmd}'), '/c "' + StopScript + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
if ResultCode <> 0 then
Result := 'EMRG could not stop all running processes (stop-emrg.cmd exit code ' + IntToStr(ResultCode) + '). Please close EMRG (GUI/TUI) and retry the install.';
end
else
Result := 'EMRG could not run the process-stop helper (stop-emrg.cmd). Please close EMRG (GUI/TUI) and retry the install.';
end;
EOF
# Windows 路径转义(iscc 需要 Windows 路径,但在 bash/msys 下用当前路径)
echo " iscc version: $("$ISCC" /? 2>&1 | head -1)"
Expand Down
65 changes: 65 additions & 0 deletions tests/test_installer_stop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Windows 安装器预停止接线回归测试(rant 2026-08-10T08:50:44)。

安装器(Inno Setup,make-installer.sh 生成 emrg.iss)覆盖 ~/.emrg/install 前必须
先优雅关闭 GUI/TUI/daemon,否则无窗口的 pythonw daemon 独占锁文件 → 卡在
"停止已有进程"(宿主只能重启系统)。本测试纯文本断言(不执行 iscc/cmd ——
macOS/CI 无 Windows),钉死四处接线:
1. bin/stop-emrg.cmd 存在且覆盖三步:GUI 优雅关闭+/F 兜底、TUI 命令行过滤、
daemon 协议关闭 + emrgd.pid 轮询兜底(顺序 GUI → daemon)
2. bin/emrgd.cmd 含 stop 分支(复用 `emrg server stop`)
3. make-installer.sh 的 .iss 模板含 [Files] dontcopy + [Code] PrepareToInstall
4. build-runtime.sh 把 stop-emrg.cmd 复制进 runtime bin/
"""

from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent


def test_stop_emrg_cmd_covers_gui_tui_daemon_in_order():
content = (REPO_ROOT / "bin" / "stop-emrg.cmd").read_text(encoding="utf-8")
# GUI:无 /F 优雅 WM_CLOSE 优先,/F 兜底
assert "taskkill /IM EMRG.exe" in content
assert "taskkill /F /IM EMRG.exe" in content
assert content.index("taskkill /IM EMRG.exe") < content.index("taskkill /F /IM EMRG.exe")
# TUI:命令行过滤(wmic LIKE 通配符须 %% 转义)
assert "wmic" in content and "commandline like" in content
assert "powershell" in content # wmic 缺失(Win11 24H2+)时的回退
# daemon:协议关闭 + pid 轮询兜底(用实际执行行而非注释里的字面量)
assert 'call "%INSTALL%\\bin\\emrg.cmd" server stop' in content
assert "emrgd.pid" in content
# 顺序:GUI 在 daemon 之前(GUI 不能复活 daemon)
daemon_line = content.index('call "%INSTALL%\\bin\\emrg.cmd" server stop')
assert content.index("taskkill /IM EMRG.exe") < daemon_line
# 干净安装安全:无旧 install 目录时跳过
assert 'set "INSTALL=%EMRG_DIR%\\install"' in content
# 括号块内 pid 判定必须用延迟展开(!DPID!)——%DPID% 在块解析时展开,
# set "DPID=" 后取到旧值/空值 → if defined 恒假 → daemon 存活误报干净
# (rant 2026-08-10T08:50:44,cmd.exe 经典括号块展开坑)
assert "setlocal enabledelayedexpansion" in content
# 从标签定义处(而非前面 wait 循环的 goto :verify)截取校验块
verify_block = content[content.index(":verify\nset \"EXIT_CODE=0\""):]
assert "PID eq !DPID!" in verify_block
# 非延迟展开 %DPID% 不得出现在括号块内(块解析时展开=恒旧值)
assert "%DPID%" not in verify_block


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
assert "-m emrg server stop" in content
assert "exit /b %errorlevel%" in content


def test_make_installer_iss_has_prepare_to_install():
content = (REPO_ROOT / "packaging" / "make-installer.sh").read_text(encoding="utf-8")
assert "dontcopy" in content
assert "stop-emrg.cmd" in content
assert "PrepareToInstall" in content
assert "ExtractTemporaryFile('stop-emrg.cmd')" in content
assert "SW_HIDE" in content # 批处理执行不弹控制台窗口(#592 纪律)


def test_build_runtime_copies_stop_emrg_cmd():
content = (REPO_ROOT / "packaging" / "build-runtime.sh").read_text(encoding="utf-8")
assert 'cp "$ROOT/bin/stop-emrg.cmd" stop-emrg.cmd' in content
Loading