Files
ykxmz/_build/rewrite_run_section.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

129 lines
4.7 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'''重写安装包的"装完收尾"逻辑。
问题:CurStepChanged(ssPostInstall) 在这个 Inno 版本里**没被触发**
(日志显示回调进了,CurStep=1,但 `CurStep <> ssPostInstall`
判断为真;而 Ord(ssPostInstall) 又报类型不匹配)。
结果 --code 从来没执行过,配对码永远是空的。
解决:不再依赖 CurStepChanged。改用 Inno 的标准做法 ——
把收尾命令放进 [Run] 段,配对码用 {code:Function} 取。
[Run] 里可以写 {code:GetPairingCode},Inno 会调用同名 Pascal 函数取值。
再加 Check: 函数决定这一条要不要跑(码为空就跳过)。
这样装完必然执行,不依赖任何步骤枚举。
'''
from __future__ import annotations
import ast
import sys
from pathlib import Path
ISS = (Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
/ "installer" / "ykxmz.iss")
# ---------- 新的 [Run] 段 ----------
NEW_RUN = '''[Run]
; 收尾分三步,顺序很重要:
; 1. 写配对码(一次性命令,秒退)
; 2. 设开机自启(一次性命令)
; 3. 静默启动(常驻进程)
;
; 为什么不放在 CurStepChanged 里:
; 实测这个 Inno 版本的 ssPostInstall 回调**不会触发**
; (日志里回调进了、CurStep=1,但 CurStep <> ssPostInstall 成立),
; 放在那里 --code 永远不执行、配对码永远为空。
; [Run] 段是安装完成后必然执行的,靠得住。
;
; {code:GetPairingCode} 是 Inno 的写法:调用同名 Pascal 函数取值。
; Check 函数返回 False 时这一条跳过(比如用户没填配对码)。
; 1. 写配对码
Filename: "{app}\\{#MyAppExeName}"; Parameters: "--code {code:GetPairingCode}"; \\
Flags: runhidden; Check: HasPairingCode
; 2. 开机自启
Filename: "{app}\\{#MyAppExeName}"; Parameters: "--autostart on"; \\
Flags: runhidden; Tasks: autostart
; 3. 静默启动
; skipifsilent 是因为静默安装走下面那条 cmd 版本
; (直接起会挂在安装器的进程树上,见那条注释)
Filename: "{app}\\{#MyAppExeName}"; Parameters: "--silent"; \\
Description: "立即启动(后台运行,不显示窗口)"; \\
Flags: nowait postinstall skipifsilent; Check: not WizardSilent
; 静默安装时也要跑起来。
; 必须用 cmd /c start 起:直接 Exec 出来的进程是安装器的子进程,
; 而 Start-Process -Wait / start /wait / MSI 封装器都会等**整个进程树**,
; --silent 是常驻守护进程永远不退出,于是"安装脚本永远跑不完"
; (实测卡满 600 秒超时)。cmd /c start 会把它挂到别处,父进程一退就断干净。
Filename: "cmd.exe"; Parameters: "/c start """" ""{app}\\{#MyAppExeName}"" --silent"; \\
Flags: runhidden nowait; Check: WizardSilent
; 让用户看一眼装完须知
Filename: "{app}\\docs\\AFTER-INSTALL.txt"; \\
Description: "打开「安装后须知」"; \\
Flags: postinstall shellexec unchecked skipifsilent
'''
# ---------- 新的 [Code] 收尾函数 ----------
NEW_FUNCS = '''
{ ===== 给 [Run] 段用的取值 / 判断函数 ===== }
{ 配对码。空串时 Inno 不会展开,所以配 HasPairingCode 一起用。 }
function GetPairingCode(Param: String): String;
begin
Result := EffectiveCode();
Diag('GetPairingCode -> "' + Result + '"');
end;
{ 有配对码才跑那条 --code 命令 }
function HasPairingCode(): Boolean;
begin
Result := EffectiveCode() <> '';
Diag('HasPairingCode -> ' + IntToStr(Ord(Result)));
end;
{ 非静默时才跑"立即启动"那条 }
function NotWizardSilent(): Boolean;
begin
Result := not WizardSilent;
end;
'''
def main() -> int:
text = ISS.read_text(encoding="utf-8")
# 1) 换掉 [Run] 段(从 [Run] 到 [UninstallDelete] 之前)
start = text.find("[Run]")
end = text.find("[UninstallDelete]")
if start < 0 or end < 0 or end < start:
print(" [失败] 找不到 [Run] / [UninstallDelete]")
return 1
text = text[:start] + NEW_RUN + "\n" + text[end:]
print(" 已替换 [Run] 段")
# 2) 在 [Code] 末尾追加三个函数
if "function GetPairingCode" not in text:
text = text.rstrip() + "\n" + NEW_FUNCS
print(" 已追加 GetPairingCode / HasPairingCode / NotWizardSilent")
ISS.write_text(text, encoding="utf-8", newline="\n")
# 语法自检(Python 层只能查文件可读)
print()
print(" [Run] 段现在:")
run_start = text.find("[Run]")
run_end = text.find("[UninstallDelete]")
for line in text[run_start:run_end].splitlines():
if line.strip() and not line.strip().startswith(";"):
print(" " + line[:88])
return 0
if __name__ == "__main__":
sys.exit(main())