修四个安装/卸载卡死的 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
+25 -5
View File
@@ -79,6 +79,10 @@ def parse_args(argv=None):
help="查看中转连接状态")
parser.add_argument("--uninstall", action="store_true",
help="卸载:停进程、删开机自启、删数据目录")
parser.add_argument("--keep-dir", action="store_true",
help="配合 --uninstall:不安排自删程序目录。"
"安装包的卸载器调用时用这个 —— "
"Inno 自己会删 {app},两边一起删会抢文件")
return parser.parse_args(argv)
@@ -98,17 +102,33 @@ def cli_autostart(action: str) -> int:
return 0
def cli_uninstall() -> int:
"""卸载。会开一个控制台让用户看到过程。"""
def cli_uninstall(keep_dir: bool = False) -> int:
"""卸载。有控制台就开一个让用户看到过程。
keep_dir=True 时不安排自删程序目录(安装包卸载器调用时用)。
⚠ 别无条件调 input()。
安装包的 [UninstallRun] 是用 runhidden + waituntilterminated 调的,
那种情况下**没有 stdin**,input() 会永远阻塞 ——
表现就是"从控制面板卸载永远卡住不结束"。
(2026-10 实测:卸载器无限等待,根因就是这一行。)
所以只在真的有交互式终端时才等回车。
"""
attach_console()
from lanremote.uninstall import run as do_uninstall
try:
print(do_uninstall())
print(do_uninstall(keep_dir=keep_dir))
except Exception as exc: # noqa: BLE001
print(f"卸载出错:{exc}")
return 1
print()
input("按回车关闭…")
# 只有真有人在看的时候才等回车
if sys.stdin is not None and sys.stdin.isatty():
try:
input("按回车关闭…")
except (EOFError, KeyboardInterrupt):
pass
return 0
@@ -661,7 +681,7 @@ def main(argv=None) -> int:
if args.relay_status:
return cli_relay_status()
if args.uninstall:
return cli_uninstall()
return cli_uninstall(keep_dir=bool(args.keep_dir))
store = Store(data_dir())