From d2e2e3b6691fb71a09aaf85d872f308bc7f2a5ce Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Wed, 5 Aug 2026 15:29:15 +0800 Subject: [PATCH 1/3] emrg: remove duplicate Inno const block (WM_SETTINGCHANGE/HWND_BROADCAST/SMTO_ABORTIFHUNG are IS 6.3+ builtin) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #397 修复 heredoc 转义后,iscc 首次成功解析 .iss,暴露下一层错误: Duplicate identifier 'HWND_BROADCAST'(Inno Setup 6.3+ 已预置这些 Windows 常量)。 删除 [Code] const 块,保留 R120 注释说明。windows-2025 runner 内置 IS 6.4.x。 --- packaging/make-installer.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packaging/make-installer.sh b/packaging/make-installer.sh index 84f6d100..a8d10cae 100755 --- a/packaging/make-installer.sh +++ b/packaging/make-installer.sh @@ -187,10 +187,9 @@ Name: "{userprograms}\\EMRG"; Filename: "{app}\\emrg-gui\\EMRG\\EMRG.exe"; IconF [UninstallRun] Filename: "{app}\\bin\\python.exe"; Parameters: "{app}\\bin\\emrg-uninstall"; Flags: runhidden [Code] -const - WM_SETTINGCHANGE = 26; { \$001A } - HWND_BROADCAST = 65535; { \$FFFF } - SMTO_ABORTIFHUNG = 2; { \$0002 } +{ R120: WM_SETTINGCHANGE / HWND_BROADCAST / SMTO_ABORTIFHUNG 为 Inno Setup 6.3+ 内置常量, + 不可在 const 段重复定义(Duplicate identifier 'HWND_BROADCAST',v0.2.2 CI 二次失败)。 + 旧代码的显式 const 定义在 IS 6.3 之前是必需的,现版本已预置。 } function SendMessageTimeout(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM; fuFlags: UINT; uTimeout: UINT; var lpdwResult: DWORD): BOOL; From 2e8ec0dd4543750e2bd85980aac996cd5b8d8aa8 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Wed, 5 Aug 2026 15:37:09 +0800 Subject: [PATCH 2/3] emrg: keep WM_SETTINGCHANGE/SMTO_ABORTIFHUNG const, only drop HWND_BROADCAST (iscc builtin) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正 #398 的不完整修复:issrc 源码确认 iscc 预定义常量 (Compiler.ScriptFunc.pas RegisterConst,185 个)仅含 HWND_BROADCAST, 无任何 WM_*/SMTO_* 常量。删除整个 const 块会导致 Unknown identifier 'WM_SETTINGCHANGE'。现在 const 块保留 WM_SETTINGCHANGE = 26 与 SMTO_ABORTIFHUNG = 2(带 $ 转义), 仅移除 HWND_BROADCAST 定义。 --- packaging/make-installer.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packaging/make-installer.sh b/packaging/make-installer.sh index a8d10cae..b72b1cdb 100755 --- a/packaging/make-installer.sh +++ b/packaging/make-installer.sh @@ -187,9 +187,12 @@ Name: "{userprograms}\\EMRG"; Filename: "{app}\\emrg-gui\\EMRG\\EMRG.exe"; IconF [UninstallRun] Filename: "{app}\\bin\\python.exe"; Parameters: "{app}\\bin\\emrg-uninstall"; Flags: runhidden [Code] -{ R120: WM_SETTINGCHANGE / HWND_BROADCAST / SMTO_ABORTIFHUNG 为 Inno Setup 6.3+ 内置常量, - 不可在 const 段重复定义(Duplicate identifier 'HWND_BROADCAST',v0.2.2 CI 二次失败)。 - 旧代码的显式 const 定义在 IS 6.3 之前是必需的,现版本已预置。 } +{ R120: HWND_BROADCAST 为 iscc 预定义常量(Compiler.ScriptFunc.pas RegisterConst), + 显式定义会报 Duplicate identifier 'HWND_BROADCAST'(v0.2.2 CI 二次失败)。 + WM_SETTINGCHANGE / SMTO_ABORTIFHUNG 未预置,需保留 const 定义。 } +const + WM_SETTINGCHANGE = 26; { \$001A } + SMTO_ABORTIFHUNG = 2; { \$0002 } function SendMessageTimeout(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM; fuFlags: UINT; uTimeout: UINT; var lpdwResult: DWORD): BOOL; From 6cc8d1875c555a5c8b2518dd2d40bc5b73f2bad8 Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Wed, 5 Aug 2026 15:41:08 +0800 Subject: [PATCH 3/3] =?UTF-8?q?emrg:=20thorough=20Windows=20uninstall=20?= =?UTF-8?q?=E2=80=94=20kill=20GUI=20first,=20full=20whitelist,=20explicit?= =?UTF-8?q?=20failures,=20[UninstallDelete]=20(rant=202026-08-05T15:35:17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(rant 证据链): 1. GUI 进程(EMRG.exe)在 ws 断开后自动 startDaemon 重建 daemon → 卸载过程 中 emrgd.pid/port/log/projects.yml 被重写;运行中 daemon 锁住 install/。 2. WHITELIST 缺 emrgd.log/emrg-gui.log/gui-window.json/skills → 被当用户数据保留。 3. rmtree(ignore_errors=True) + except OSError: pass → 删除失败静默吞掉。 4. .iss 无 [UninstallDelete] → install/ 删除失败无兜底。 5. self_verify 只查 6 个固定文件 → 残留不告警、退出码 0。 修复: - Step 1a 新增 stop_gui(Windows taskkill /IM EMRG.exe /F;POSIX pkill -f EMRG) - Step 1b daemon 停止后 Windows 轮询 tasklist 确认退出(≤5s) - WHITELIST 补全 4 项;graveyard 快照 members 增加 skills - delete_whitelisted 返回 (removed, failed),失败写入 report 的 4_delete_whitelisted.failed,残留时 WARNING + 退出码 1 - self_verify 全量扫描 ~/.emrg 顶层项 - Step 7 finalize:卸载凭证(report + graveyard 快照)移至家目录,rmtree ~/.emrg - .iss 增加 [UninstallDelete] Type: filesandordirs; Name: "{app}" (emrg-uninstall 退出后强制删除 install/) 验证:466 passed + import OK + 隔离 HOME 实测(12 paths 删除、~/.emrg 清除、 凭证移家目录、二次运行幂等 EXIT=0)。 --- bin/emrg-uninstall | 157 +++++++++++++++++++++++++++++------- packaging/make-installer.sh | 4 + 2 files changed, 131 insertions(+), 30 deletions(-) diff --git a/bin/emrg-uninstall b/bin/emrg-uninstall index 2d4dd6e0..55905717 100755 --- a/bin/emrg-uninstall +++ b/bin/emrg-uninstall @@ -2,15 +2,21 @@ """EMRG unified uninstaller — ran by the platform uninstaller (rant #12 §8). Steps (idempotent, safe to re-run): - 1. Stop the daemon (protocol shutdown when websockets importable, else - SIGTERM from pid file / Windows taskkill fallback). + 1a. Stop GUI processes (EMRG.exe / pkill EMRG) so it cannot respawn daemon + while we delete runtime files (R121). + 1b. Stop the daemon (protocol shutdown when websockets importable, else + SIGTERM from pid file / Windows taskkill fallback, with exit poll). 2. Termination report -> ~/.emrg/logs/uninstall-report-.json 3. Graveyard snapshot -> ~/.emrg/graveyard/emrg-data-.tar.gz 4. Delete known EMRG files in ~/.emrg (R101 whitelist; anything outside the - whitelist is kept and listed in the report). + whitelist is kept and listed in the report). Failures are collected, not + swallowed (R121). 5. Clean environment traces (PATH shell-rc anchor blocks, launcher symlinks, Start-menu shortcuts are handled by the native uninstaller). - 6. Self-verify: print what remains and the cleanup list. + 6. Self-verify: full scan of ~/.emrg top-level items; any runtime residue is + reported with a non-zero exit code (R121). + 7. Finalize: move uninstall records (report + graveyard snapshot) to the home + directory, then remove ~/.emrg entirely (R121). R92: install/ itself is NOT deleted by this script — the interpreter lives in install/bin, so a running interpreter would lock it on Windows (NTFS). The @@ -44,10 +50,14 @@ INSTALL_DIR = EMRG_DIR / "install" # R101 whitelist — known EMRG files. Anything else in ~/.emrg is user data # and is preserved (listed in the report instead of deleted). +# R121: 补全运行时文件 — emrgd.log / emrg-gui.log / gui-window.json 为 daemon/GUI +# 运行日志与窗口状态;skills/ 是 daemon 启动时 mkdir 的运行时骨架(非用户数据), +# 用户自定义 skills 先入 graveyard 快照再删除(rant 2026-08-05T15:35:17)。 WHITELIST = [ "install", "versions", "config.toml", "sessions", "memory", "logs", "projects.yml", "tasks.yml", "rants.jsonl", "saturation", "emrgd.sock", "emrgd.pid", "emrgd.port", "install-info.json", + "emrgd.log", "emrg-gui.log", "gui-window.json", "skills", ] # Shell rc anchor for PATH cleanup (R19). @@ -59,8 +69,37 @@ def now_iso() -> str: return datetime.now(timezone.utc).isoformat(timespec="seconds") +def stop_gui() -> dict: + """Step 1a — stop GUI processes BEFORE daemon, so the GUI cannot respawn + the daemon (daemon_client.js auto-startDaemon on ws disconnect) while we + are deleting runtime files (rant 2026-08-05T15:35:17).""" + result = {"method": "none", "ok": False} + try: + if os.name == "nt": + r = subprocess.run( + ["taskkill", "/IM", "EMRG.exe", "/F"], + capture_output=True, timeout=10, + ) + if r.returncode == 0: + result = {"method": "taskkill-EMRG.exe", "ok": True} + print(" [1] GUI stopped (taskkill /IM EMRG.exe /F)") + else: + # POSIX: EMRG Electron GUI (app path contains 'EMRG'). Matches the + # packaged app only — not this script (lowercase 'emrg-uninstall'). + r = subprocess.run( + ["pkill", "-f", "EMRG"], + capture_output=True, timeout=10, + ) + if r.returncode == 0: + result = {"method": "pkill-EMRG", "ok": True} + print(" [1] GUI stopped (pkill -f EMRG)") + except (OSError, subprocess.SubprocessError): + pass + return result + + def stop_daemon() -> dict: - """Step 1 — stop the daemon. Returns {method, ok}.""" + """Step 1b — stop the daemon. Returns {method, ok}.""" result = {"method": "none", "ok": False} # Try protocol shutdown when websockets is importable (PYTHONPATH has lib/). try: @@ -128,6 +167,15 @@ def stop_daemon() -> dict: ["taskkill", "/PID", str(pid), "/F"], capture_output=True, timeout=10, ) + # R121: 轮询确认进程退出(≤5s),避免 pid 文件被并发重建 + for _ in range(35): + chk = subprocess.run( + ["tasklist", "/FI", f"PID eq {pid}"], + capture_output=True, text=True, timeout=5, + ) + if "No tasks" in chk.stdout: + break + time.sleep(0.15) else: os.kill(pid, signal.SIGTERM) for _ in range(20): @@ -167,7 +215,8 @@ def graveyard_snapshot() -> Path | None: ts = time.strftime("%Y%m%d-%H%M%S") dest = GRAVEYARD_DIR / f"emrg-data-{ts}.tar.gz" members = [] - for name in ("memory", "sessions", "logs", "rants.jsonl", "projects.yml", "tasks.yml"): + # R121: skills 一并快照(备份用户自定义 skills 后再删) + for name in ("memory", "sessions", "logs", "rants.jsonl", "projects.yml", "tasks.yml", "skills"): p = EMRG_DIR / name if p.exists(): members.append(p) @@ -181,24 +230,29 @@ def graveyard_snapshot() -> Path | None: return dest -def delete_whitelisted() -> list: - """Step 4 — delete whitelisted EMRG files. Returns list of removed paths.""" - removed = [] +def delete_whitelisted() -> tuple[list, list]: + """Step 4 — delete whitelisted EMRG files. + + Returns (removed, failed). R121: 删除失败不再静默吞掉 — 失败路径 + 收集进 failed,由 main() 写入 report 的 4_delete_whitelisted.failed + 并以非零退出码告警。 + """ + removed, failed = [], [] for name in WHITELIST: p = EMRG_DIR / name if name == "install": # R92: install/ is deleted by the platform uninstaller, not here. continue - if p.is_dir() and not p.is_symlink(): - shutil.rmtree(p, ignore_errors=True) - removed.append(str(p)) - elif p.exists() or p.is_symlink(): - try: + try: + if p.is_dir() and not p.is_symlink(): + shutil.rmtree(p) + removed.append(str(p)) + elif p.exists() or p.is_symlink(): p.unlink() removed.append(str(p)) - except OSError: - pass - return removed + except OSError as e: + failed.append({"path": str(p), "error": str(e)}) + return removed, failed def clean_environment() -> list: @@ -234,14 +288,43 @@ def clean_environment() -> list: def self_verify() -> list: - """Step 6 — self-verify. Returns list of remaining EMRG artifacts.""" + """Step 6 — self-verify. R121: 全量扫描 ~/.emrg 顶层项,任何运行时残留 + (除 logs/graveyard 卸载产物外)都视为残留。""" remaining = [] - for f in ("emrgd.port", "emrgd.pid", "config.toml", "projects.yml", "tasks.yml", "rants.jsonl"): - if (EMRG_DIR / f).exists(): - remaining.append(str(EMRG_DIR / f)) + if EMRG_DIR.exists(): + for p in sorted(EMRG_DIR.iterdir()): + if p.name in ("logs", "graveyard"): + continue # 卸载产物目录(report / snapshot),finalize 阶段处理 + remaining.append(str(p)) return remaining +def finalize_artifacts(report_path: Path, snap: Path | None) -> list: + """Final — move uninstall artifacts (report + graveyard snapshot) to the + home directory (kept as uninstall records, outside ~/.emrg), then remove + ~/.emrg entirely (R121: 卸载彻底,~/.emrg 不留运行时残留). + + Windows: install/ may stay locked by the running interpreter — the Inno + [UninstallDelete] step removes {app} after this script exits (R92). + """ + moved = [] + for p in (report_path, snap): + if p is None or not p.exists(): + continue + dest = Path.home() / p.name + i = 1 + while dest.exists(): + dest = Path.home() / f"{p.stem}-{i}{p.suffix}" + i += 1 + try: + shutil.move(str(p), str(dest)) + moved.append(str(dest)) + except OSError: + pass + shutil.rmtree(EMRG_DIR, ignore_errors=True) + return moved + + def main() -> int: print("EMRG uninstaller") print("=================") @@ -252,12 +335,14 @@ def main() -> int: "steps": {}, } - report["steps"]["1_stop_daemon"] = stop_daemon() + report["steps"]["1a_stop_gui"] = stop_gui() + report["steps"]["1b_stop_daemon"] = stop_daemon() - report["steps"]["3_graveyard_snapshot"] = {"path": str(snap) if (snap := graveyard_snapshot()) else None} + snap = graveyard_snapshot() + report["steps"]["3_graveyard_snapshot"] = {"path": str(snap) if snap else None} - removed = delete_whitelisted() - report["steps"]["4_delete_whitelisted"] = {"removed": removed} + removed, failed = delete_whitelisted() + report["steps"]["4_delete_whitelisted"] = {"removed": removed, "failed": failed} cleaned = clean_environment() report["steps"]["5_clean_environment"] = {"cleaned": cleaned} @@ -269,16 +354,28 @@ def main() -> int: report_path = write_report(report) report["steps"]["2_termination_report"] = {"path": str(report_path)} + # Final: move uninstall records to home, then remove ~/.emrg (R121). + moved = finalize_artifacts(report_path, snap) + report["steps"]["7_finalize"] = {"moved_to_home": moved} + print("\nCleanup summary:") print(f" removed: {len(removed)} paths") print(f" env cleaned: {len(cleaned)} items") + if failed: + print(f" WARNING: {len(failed)} delete failures:") + for item in failed: + print(f" - {item['path']}: {item['error']}") + if moved: + print(" uninstall records kept:") + for m in moved: + print(f" - {m}") if remaining: - print(f" remaining (expected): {len(remaining)} artifacts") + print(f" WARNING: {len(remaining)} artifacts remain under ~/.emrg:") + for r in remaining: + print(f" - {r}") else: - print(" remaining: none") - print("\nNOTE: ~/.emrg/install/ is left for the platform uninstaller to remove (R92).") - print("User files outside the whitelist were preserved and are not listed.") - return 0 + print(" ~/.emrg fully removed (or empty)") + return 1 if (failed or remaining) else 0 if __name__ == "__main__": diff --git a/packaging/make-installer.sh b/packaging/make-installer.sh index b72b1cdb..15006b62 100755 --- a/packaging/make-installer.sh +++ b/packaging/make-installer.sh @@ -186,6 +186,10 @@ Source: "$STAGE_WIN/payload\\*"; DestDir: "{app}"; Flags: recursesubdirs createa Name: "{userprograms}\\EMRG"; Filename: "{app}\\emrg-gui\\EMRG\\EMRG.exe"; IconFilename: "{app}\\emrg-gui\\EMRG\\EMRG.exe" [UninstallRun] Filename: "{app}\\bin\\python.exe"; Parameters: "{app}\\bin\\emrg-uninstall"; Flags: runhidden +[UninstallDelete] +; R121: emrg-uninstall 脚本退出后(python.exe 已退出,无文件锁),强制删除 +; {app}(install/)— 兜底卸载彻底(rant 2026-08-05T15:35:17) +Type: filesandordirs; Name: "{app}" [Code] { R120: HWND_BROADCAST 为 iscc 预定义常量(Compiler.ScriptFunc.pas RegisterConst), 显式定义会报 Duplicate identifier 'HWND_BROADCAST'(v0.2.2 CI 二次失败)。