mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 21:50:44 +00:00
184 lines
7.0 KiB
Python
184 lines
7.0 KiB
Python
# -*- 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())
|