# -*- coding: utf-8 -*- r'''安装包完整验收:装 → 验 → 卸 → 再验。 用户要求"所有东西做完记得先测试能不能用",所以每一步都实测。 ⚠ 环境注意: 我这个 shell **不是管理员**,而安装器会提升。 所以所有"清理"动作都要交给**安装器/卸载器**自己做, 不能靠 PowerShell 杀进程或删 Program Files(会拒绝访问)。 补丁历史(这轮修的四个真 bug): 1. --uninstall 里 Popen 不重定向 stdio -> capture_output 等管道 EOF -> "卡死" 2. --uninstall 里 input() 无 stdin 时永久阻塞 -> 控制面板卸载卡死 3. _kill_running 用 taskkill /IM ykxmz.exe 把自己也杀了 -> 后面的删自启/删数据全没执行 4. 安装器关不掉以管理员身份跑着的旧实例 -> "无法关闭应用程序" -> 安装回滚 -> 改成 PrepareToInstall 里自己 taskkill /F ''' from __future__ import annotations import json import subprocess import sys import time from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") SETUP = ROOT / "dist" / "ykxmz-setup-1.7.1.exe" APP = Path(r"C:\Program Files\ykxmz") DATA = Path(r"C:\ProgramData\ykxmz") EXE = APP / "ykxmz.exe" REG = (r"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" r"\{8F3C1D42-7A5E-4B91-9C6D-2E8F4A1B7C30}_is1") RUN_KEY = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run" CODE = "517284" passed = failed = 0 def check(name, ok, detail=""): global passed, failed if ok: passed += 1 print(" [PASS] " + name) else: failed += 1 print(" [FAIL] " + name + " " + str(detail)) def ps(command: str, timeout: int = 300) -> str: r = subprocess.run( ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], capture_output=True, text=True, timeout=timeout, encoding="utf-8", errors="replace") return (r.stdout or "").strip() def run_bat(lines: list, timeout: int = 600) -> tuple: """把命令写进 .bat 再跑。 为什么不直接 subprocess.run(["cmd", "/c", "..."]): 引号会被 Python 和 cmd 来回吃掉,实测 0.0 秒就返回 exit=1。 写成 .bat 文件最稳,而且和学校实际的批量部署方式一致。 """ bat = ROOT / "_build" / "_run.bat" bat.write_text("\r\n".join(["@echo off"] + lines), encoding="gbk", errors="replace") start = time.perf_counter() r = subprocess.run(["cmd", "/c", str(bat)], capture_output=True, text=True, timeout=timeout, encoding="gbk", errors="replace") return r.returncode, time.perf_counter() - start, (r.stdout or "") def install() -> tuple: return run_bat([ '"%s" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART ' '/LOG="%s" /DIR="C:\\Program Files\\ykxmz" ' '/CODE=%s /TASKS=autostart' % ( SETUP, ROOT / "_build" / "setup.log", CODE), ]) def uninstall() -> tuple: found = ps("Get-ChildItem '%s\\unins*.exe' -ErrorAction SilentlyContinue " "| Select-Object -First 1 -ExpandProperty FullName" % APP) if not found: return -1, 0.0, "" return run_bat([ '"%s" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART' % found.strip(), ]) def main() -> int: check("安装包在", SETUP.is_file(), str(SETUP)) if not SETUP.is_file(): return 1 # ---------------- 安装 ---------------- print() print(" 一、静默安装(配对码 %s)" % CODE) code, elapsed, _out = install() check("安装器退出码 0", code == 0, "exit=%s" % code) check("安装没卡死", elapsed < 120, "%.1f 秒" % elapsed) print(" %.1f 秒" % elapsed) time.sleep(8) print() print(" 二、文件与数据") check("ykxmz.exe 在", EXE.is_file()) check("_internal 在", (APP / "_internal").is_dir()) check("数据目录在", DATA.is_dir()) settings = DATA / "data" / "settings.json" check("settings.json 生成了", settings.is_file()) if settings.is_file(): saved = json.loads(settings.read_text(encoding="utf-8")) got = saved.get("pairing_code") check("配对码正确写入", got == CODE, "实际 %r" % got) acl = ps("(Get-Acl '%s').Access | Where-Object { $_.IdentityReference " "-like '*Users*' } | Select-Object -First 1 " "-ExpandProperty FileSystemRights" % (DATA / "data")) check("普通用户对数据目录可写", any(w in acl for w in ("Modify", "FullControl", "Write")), acl or "(读不到)") print() print(" 三、--status") status = ps("& '%s' --status" % EXE, timeout=120) check("--status 显示配对码", CODE in status, status[:160]) for line in status.splitlines()[:7]: print(" " + line.strip()) print() print(" 四、进程与端口") count = ps("(Get-Process ykxmz -ErrorAction SilentlyContinue | " "Measure-Object).Count") check("被控端在跑", count.strip().splitlines()[-1:] != ["0"], count) listen = ps("(Get-NetTCPConnection -State Listen -ErrorAction " "SilentlyContinue | Where-Object { $_.LocalPort -eq 8765 } | " "Measure-Object).Count") check("8765 在监听", listen.strip().splitlines()[-1:] != ["0"], listen) run_value = ps("(Get-ItemProperty '%s' -Name ykxmz -ErrorAction " "SilentlyContinue).ykxmz" % RUN_KEY) check("开机自启已设置", "ykxmz" in run_value.lower(), run_value or "(无)") # ---------------- 卸载 ---------------- print() print(" 五、静默卸载") code, elapsed, _out = uninstall() check("卸载器执行了", code != -1, "找不到卸载器") check("卸载没卡死", elapsed < 120, "%.1f 秒" % elapsed) print(" %.1f 秒" % elapsed) time.sleep(10) print() print(" 六、卸载是否干净") check("安装目录已删", not APP.is_dir(), "还剩: %s" % ", ".join(p.name for p in APP.iterdir()) if APP.is_dir() else "") check("数据目录已删", not DATA.is_dir()) check("注册表卸载项已清", REG not in ps("if (Test-Path '%s') " "{ 'exists' }" % REG)) left = ps("(Get-Process ykxmz -ErrorAction SilentlyContinue | " "Measure-Object).Count") check("进程已退出", left.strip().splitlines()[-1:] == ["0"], left) still = ps("(Get-ItemProperty '%s' -Name ykxmz -ErrorAction " "SilentlyContinue).ykxmz" % RUN_KEY) check("开机自启已清掉", not still, still or "") print() print("=" * 56) print(" 通过 %d 项,失败 %d 项" % (passed, failed)) return 0 if failed == 0 else 1 if __name__ == "__main__": sys.exit(main())