Files
ykxmz/_build/test_installer_e2e.py
xmz7891 df8056dd16 修四个安装/卸载卡死的 bug(都实测复现过)
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 项全通过
2026-10-07 01:19:11 +08:00

193 lines
6.9 KiB
Python
Raw Permalink 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'''1.7.1 安装包端到端验收。
用户要求:"所有东西做完记得先测试能不能用"
—— 所以不能只测源码,要**真的装一遍**。
流程:
1. 静默安装(带配对码)
2. 确认文件落在 Program Files
3. 确认数据目录权限对(普通用户能写)
4. 确认 --status 能读到配对码
5. 确认进程起来了、端口在听
6. 卸载(静默,避开确认弹窗)
⚠ 踩过的坑:
* 不带 /SUPPRESSMSGBOXES 卸载会弹确认框,脚本永远卡住
* /DIR= 的值里有空格,必须整体加引号,否则 Start-Process 会拆开
* Inno 的 UsePreviousAppDir 会记住上次目录,必须显式给 /DIR=
'''
from __future__ import annotations
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"
INSTALL_DIR = r"C:\Program Files\ykxmz"
DATA_DIR = r"C:\ProgramData\ykxmz\data"
CODE = "739251"
PORT = 8877
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 run_ps(command: str, timeout: int = 300) -> tuple:
result = subprocess.run(
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-Command", command],
capture_output=True, text=True, timeout=timeout,
encoding="utf-8", errors="replace")
return result.returncode, (result.stdout or "").strip()
def cleanup() -> None:
"""把上次可能留下的装机和注册表清掉,保证是全新安装。"""
print(" 零、清理上次残留")
run_ps(
"Get-Process ykxmz -ErrorAction SilentlyContinue | "
"Stop-Process -Force -ErrorAction SilentlyContinue; "
"Start-Sleep -Seconds 2; "
"Remove-Item 'C:\\Program Files\\ykxmz' -Recurse -Force "
"-ErrorAction SilentlyContinue; "
"Remove-Item 'C:\\ProgramData\\ykxmz' -Recurse -Force "
"-ErrorAction SilentlyContinue; "
"Remove-Item 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
"\\{8F3C1D42-7A5E-4B91-9C6D-2E8F4A1B7C30}_is1' -Recurse -Force "
"-ErrorAction SilentlyContinue; "
"Write-Output cleaned")
print(" 已清理")
def main() -> int:
check("安装包存在", SETUP.is_file(), str(SETUP))
if not SETUP.is_file():
return 1
print(" 安装包 %.2f MB" % (SETUP.stat().st_size / 1024 / 1024))
cleanup()
print()
print(" 一、静默安装(配对码 %s)" % CODE)
# /DIR= 的值有空格,整体加引号 —— 否则 Start-Process 会拆开
arg_list = (
'/VERYSILENT /SUPPRESSMSGBOXES /NORESTART '
'/DIR="C:\\Program Files\\ykxmz" '
'/CODE=%s /TASKS=autostart' % CODE
)
start = time.perf_counter()
proc = subprocess.run(
["powershell", "-NoProfile", "-Command",
"Start-Process -FilePath '%s' -ArgumentList '%s' -Wait -PassThru "
"| Select-Object -ExpandProperty ExitCode" % (SETUP, arg_list)],
capture_output=True, text=True, timeout=600,
encoding="utf-8", errors="replace")
elapsed = time.perf_counter() - start
exit_code = (proc.stdout or "").strip()
check("安装退出码 0", exit_code == "0", "exit=%r" % exit_code)
print(" 耗时 %.1f 秒" % elapsed)
check("安装没卡死(<120 秒)", elapsed < 120, "%.1f 秒" % elapsed)
time.sleep(6)
print()
print(" 二、文件落地")
exe = Path(INSTALL_DIR) / "ykxmz.exe"
check("ykxmz.exe 在", exe.is_file(), str(exe))
if exe.is_file():
print(" %.1f MB" % (exe.stat().st_size / 1024 / 1024))
check("_internal 目录在",
(Path(INSTALL_DIR) / "_internal").is_dir())
print()
print(" 三、数据目录与权限")
check("数据目录在", Path(DATA_DIR).is_dir(), DATA_DIR)
code_file = Path(DATA_DIR) / "settings.json"
check("settings.json 生成了", code_file.is_file())
if code_file.is_file():
import json
try:
saved = json.loads(code_file.read_text(encoding="utf-8"))
got = saved.get("pairing_code")
check("配对码写进去了", got == CODE, "实际 %r" % got)
except Exception as exc: # noqa: BLE001
check("settings.json 能解析", False, str(exc))
rc, acl = run_ps(
"(Get-Acl '%s').Access | Where-Object { $_.IdentityReference -like "
"'*Users*' } | Select-Object -First 1 -ExpandProperty "
"FileSystemRights" % DATA_DIR)
check("普通用户对数据目录有写权限",
"Modify" in acl or "FullControl" in acl or "Write" in acl,
acl or "(读不到 ACL)")
print()
print(" 四、--status 能读到配对码")
rc, out = run_ps("& '%s' --status" % exe, timeout=120)
check("--status 退出码 0", rc == 0, "exit=%s" % rc)
check("--status 显示配对码", CODE in out, out[:200])
for line in (out or "").splitlines()[:8]:
print(" " + line.strip())
print()
print(" 五、进程与端口")
rc, procs = run_ps(
"(Get-Process ykxmz -ErrorAction SilentlyContinue | "
"Measure-Object).Count")
try:
count = int((procs or "0").strip().splitlines()[-1])
except ValueError:
count = 0
check("ykxmz 进程在跑", count > 0, "%d 个" % count)
rc, ports = run_ps(
"(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | "
"Where-Object { $_.LocalPort -eq 8765 } | Measure-Object).Count")
try:
listening = int((ports or "0").strip().splitlines()[-1])
except ValueError:
listening = 0
check("8765 端口在监听", listening > 0, "%d 个" % listening)
print()
print(" 六、静默卸载")
rc, out = run_ps(
"Get-Process ykxmz -ErrorAction SilentlyContinue | "
"Stop-Process -Force -ErrorAction SilentlyContinue; "
"Start-Sleep -Seconds 2; "
"$u = Get-ChildItem 'C:\\Program Files\\ykxmz\\unins*.exe' "
"-ErrorAction SilentlyContinue | Select-Object -First 1; "
"if ($u) { Start-Process -FilePath $u.FullName "
"-ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' "
"-Wait -PassThru | Select-Object -ExpandProperty ExitCode } "
"else { Write-Output 'no-uninstaller' }",
timeout=600)
check("卸载器执行了", "no-uninstaller" not in out, out[:120])
time.sleep(6)
rc, left = run_ps(
"if (Test-Path 'C:\\Program Files\\ykxmz') { Write-Output 'exists' } "
"else { Write-Output 'gone' }")
check("安装目录已清掉", "gone" in left, left)
print()
print("=" * 54)
print(" 通过 %d 项,失败 %d 项" % (passed, failed))
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())