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 项全通过
193 lines
6.9 KiB
Python
193 lines
6.9 KiB
Python
# -*- 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())
|