Files
ykxmz/_build/test_installer_cycle.py
T
2026-10-07 15:44:49 +08:00

184 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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.8.2.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")
# ⚠ PowerShell 的注册表 provider 路径**必须带冒号**(HKCU:\...)。
# 写成 HKCU\... 不是合法路径,Get-ItemProperty 静默返回空,
# 于是"开机自启"这一项永远误报失败。(这个坑我自己踩了一次。)
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())