修四个安装/卸载卡死的 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 项全通过
This commit is contained in:
xmz7891
2026-10-07 01:19:11 +08:00
parent e29b741ddf
commit df8056dd16
13 changed files with 1242 additions and 58 deletions
+96
View File
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
r'''验证卸载卡死的假设:--uninstall 里的自删逻辑和 Inno 卸载器抢目录。
步骤:
1. 彻底清干净
2. 静默安装
3. 单独跑 ykxmz.exe --uninstall,**计时**(不经过 Inno 卸载器)
如果它自己很快返回,说明卡的是 Inno 那一步(抢目录)
4. 看它有没有把安装目录删掉
'''
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 = Path(r"C:\Program Files\ykxmz")
CODE = "739251"
def ps(command: str, timeout: int = 300) -> tuple:
r = subprocess.run(
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
"-Command", command],
capture_output=True, text=True, timeout=timeout,
encoding="utf-8", errors="replace")
return r.returncode, (r.stdout or "").strip()
def main() -> int:
print(" 一、彻底清理")
ps("Get-Process ykxmz,unins000,_unins.tmp -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 ok")
ps("Get-Process ykxmz -ErrorAction SilentlyContinue | "
"Stop-Process -Force -ErrorAction SilentlyContinue")
time.sleep(3)
print(" 二、静默安装")
start = time.perf_counter()
ps("Start-Process -FilePath '%s' -ArgumentList "
"'/VERYSILENT /SUPPRESSMSGBOXES /NORESTART "
"/DIR=\"C:\\Program Files\\ykxmz\" /CODE=%s' -Wait" % (SETUP, CODE),
timeout=600)
print(" 安装耗时 %.1f 秒" % (time.perf_counter() - start))
time.sleep(5)
exe = INSTALL_DIR / "ykxmz.exe"
print(" ykxmz.exe 在: %s" % exe.is_file())
print()
print(" 三、单独跑 --uninstall 并计时(不经过 Inno 卸载器)")
print(" 这一步是关键:如果它很快返回,卡的就是 Inno 那边")
start = time.perf_counter()
try:
r = subprocess.run(
[str(exe), "--uninstall"],
capture_output=True, text=True, timeout=90,
encoding="utf-8", errors="replace")
elapsed = time.perf_counter() - start
print(" ✓ %.1f 秒返回,exit=%s" % (elapsed, r.returncode))
for line in (r.stdout or "").strip().splitlines():
print(" " + line.strip())
except subprocess.TimeoutExpired:
print(" ✗ 90 秒没返回 —— **它自己就会卡**")
print(" 假设不成立,问题在 --uninstall 本身")
print()
print(" 四、看安装目录现在什么样")
if INSTALL_DIR.is_dir():
files = list(INSTALL_DIR.iterdir())
print(" 目录还在,%d 个条目:" % len(files))
for f in files[:10]:
print(" %s" % f.name)
else:
print(" 目录已被自删逻辑删掉")
print()
print(" 五、进程状态")
rc, out = ps("(Get-Process ykxmz,unins000,_unins.tmp "
"-ErrorAction SilentlyContinue | Measure-Object).Count")
print(" 残留进程: %s" % (out.strip().splitlines()[-1] if out else "?"))
return 0
if __name__ == "__main__":
sys.exit(main())