mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
1. 卸载里 input() 在无 stdin 时永久阻塞
从控制面板卸载**永远卡住不结束**。安装包的 [UninstallRun]
用 runhidden 调用,没有 stdin,input() 就一直等。
改成只在 sys.stdin.isatty() 时才等回车。
2. Popen 没重定向子进程 stdio,导致 capture_output 等管道 EOF
_schedule_self_delete 起的 cmd 继承了父进程的 stdout/stderr 句柄,
任何用 capture_output 调 --uninstall 的地方都会一直等,
表现为"程序卡死"(实测 90 秒不返回,改后 0.2 秒)。
Popen 加 stdin/stdout/stderr=DEVNULL。
顺带把延迟从 timeout 换成 ping —— timeout 在 DETACHED_PROCESS
下没有控制台会立刻失败,等于没延迟直接删。
3. _kill_running 用 taskkill /IM ykxmz.exe 把**自己**杀了
--uninstall 本身就跑在 ykxmz.exe 里,这条命令一发,
后面的"删开机自启""删数据目录"全都没执行。
现象:卸载看着完成了,HKCU 的自启注册表项还留着。
改成 /FI "PID ne <self>" 排除自己。
4. 安装器关不掉以管理员身份运行的旧实例 -> 整个安装回滚
Inno 自带机制只会"请求"对方退出,遇上常驻的服务进程关不掉,
弹"无法自动关闭所有应用程序",静默模式下被压成"终止",
安装直接回滚 —— 用户看到的是"装了没反应/还是旧版本"。
改成 PrepareToInstall 里自己 taskkill /F,关掉 Inno 那套。
另外修了两个卸载残留:
* 安装目录留个空壳 -> [UninstallDelete] 加 dirifempty
* ProgramData\ykxmz 没删 -> 加进 [UninstallDelete]
还有一处 Inno 写法陷阱(踩了三次):
注释里写 [区段名] 或 {常量} 会让编译直接失败,
因为 [Code] 解析器连注释内容也处理。
加了 _build/check_iss.py 专门扫这个,并进全量套件。
实测结果:
静默安装 5 秒内退出,退出码 0,配对码正确写入,程序自动跑起来
静默卸载 0.7 秒退出(原来无限卡住)
全量回归 29 项全通过
卸载逻辑 11 项全通过
181 lines
6.8 KiB
Python
181 lines
6.8 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.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())
|