mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
修四个安装/卸载卡死的 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:
+113
-38
@@ -55,6 +55,20 @@ PrivilegesRequired=admin
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
|
||||
; 安装/升级前由 PrepareToInstall 自己强杀正在运行的被控端。
|
||||
;
|
||||
; 为什么不用 Inno 自带的关程序机制(默认开着):
|
||||
; 它只会礼貌地"请求"对方退出,遇上以管理员身份启动的常驻进程
|
||||
; 关不掉,然后弹出"安装程序无法自动关闭所有应用程序",
|
||||
; 静默模式下这个消息框被压成"终止",**安装直接回滚** ——
|
||||
; 用户看到的现象是"装了没反应/还是旧版本"。
|
||||
; (2026-10 实测踩到:旧实例在跑,新版本装不上,配对码也没写进去。)
|
||||
;
|
||||
; 我们自己在 PrepareToInstall 里 taskkill /F,干净利落。
|
||||
; 关掉 Inno 自带的那套,免得它先弹框把安装拦下来。
|
||||
CloseApplications=no
|
||||
RestartApplications=no
|
||||
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=ykxmz-setup-{#MyAppVersion}
|
||||
SetupIconFile=..\windows\app.ico
|
||||
@@ -107,27 +121,62 @@ Name: "{commonappdata}\ykxmz\data"; Permissions: users-modify
|
||||
Type: filesandordirs; Name: "{app}\data"
|
||||
|
||||
[Run]
|
||||
; 装完立即静默启动(标准 postinstall 勾选框,默认勾上)
|
||||
; 收尾三步,顺序很重要:先写配对码,再设开机自启,最后才启动。
|
||||
;
|
||||
; ⚠ 为什么放在这里,而不是 [Code] 的 CurStepChanged(ssPostInstall):
|
||||
; 实测这个 Inno 版本的 ssPostInstall 回调**不会触发**
|
||||
; (日志显示回调进来了、CurStep=1,但 CurStep <> ssPostInstall 成立;
|
||||
; 而 Ord(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. 启动。
|
||||
; 非静默:标准 postinstall 勾选框,用户可以取消。
|
||||
; 静默: /VERYSILENT 下 postinstall 那条被 skipifsilent 跳过,所以单独来一条。
|
||||
;
|
||||
; ⚠ 都用 nowait —— --silent 是常驻守护进程,等它退出等于永远不返回。
|
||||
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
|
||||
; (PowerShell 的 Start-Process -Wait 就是)。
|
||||
; 学校批量部署请用 cmd:
|
||||
; cmd /c ykxmz-setup-1.7.1.exe /VERYSILENT /CODE=123456
|
||||
; cmd 只等安装器本身,不等孙子进程,不会卡。
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||||
Description: "立即启动(后台运行,不显示窗口)"; \
|
||||
Flags: nowait postinstall skipifsilent
|
||||
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||||
Flags: nowait runhidden; Check: WizardSilent
|
||||
|
||||
; 让用户看一眼装完须知
|
||||
Filename: "{app}\docs\AFTER-INSTALL.txt"; \
|
||||
Description: "打开「安装后须知」"; \
|
||||
Flags: postinstall shellexec unchecked skipifsilent
|
||||
|
||||
[UninstallRun]
|
||||
; 卸载前先让被控端自己收尾:停进程、删开机自启、清数据目录。
|
||||
; 这样"卸载后什么都不留"才是真的干净。
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--uninstall"; \
|
||||
Flags: runhidden waituntilterminated; RunOnceId: "StopAgent"
|
||||
|
||||
[UninstallDelete]
|
||||
; 被控端会自己删数据目录,这里兜底再清一遍安装目录里的残留
|
||||
; 被控端会自己删数据目录,这里兜底再清一遍安装目录里的残留。
|
||||
; {commonappdata}\ykxmz 也要清 —— 安装时 [Dirs] 建了它,
|
||||
; 卸载时**不会自动删**([Dirs] 建的目录不归 [UninstallDelete] 管),
|
||||
; 不写会留个空目录在那儿。
|
||||
Type: filesandordirs; Name: "{app}\data"
|
||||
Type: filesandordirs; Name: "{app}\docs"
|
||||
Type: filesandordirs; Name: "{app}\_internal"
|
||||
Type: filesandordirs; Name: "{commonappdata}\ykxmz"
|
||||
|
||||
; Inno 默认只在目录空时删它,而卸载器自己的临时文件
|
||||
; (_unins.tmp 之类)经常让目录"非空",于是留下一个空壳。
|
||||
; 显式写 dirifempty 兜底。
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
[Code]
|
||||
var
|
||||
@@ -208,48 +257,74 @@ begin
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
{ 把向导里收集的东西交给被控端。
|
||||
注意这里**不能带 --silent** —— 那是常驻守护进程,永远不返回,
|
||||
用 ewWaitUntilTerminated 等它会把安装程序挂死(第一版就栽在这)。
|
||||
写配置和启动服务分成两次调用:
|
||||
--code / --autostart 一次性命令,秒退
|
||||
--silent 常驻,交给 [Run] 段用 nowait 启动 }
|
||||
{ 收尾命令现在放在 [Run] 段(见那里的注释)。
|
||||
原来放在 CurStepChanged(ssPostInstall) 里 —— 实测这个 Inno 版本的
|
||||
ssPostInstall **根本不会触发**,所以 --code 从来没执行过,
|
||||
配对码永远是空的。留这个函数只是为了以后有人想改回来时能看到这段历史。 }
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
ResultCode: Integer;
|
||||
Params: String;
|
||||
Code: String;
|
||||
begin
|
||||
if CurStep <> ssPostInstall then
|
||||
Exit;
|
||||
{ 故意什么都不做。真要恢复旧写法,先确认 ssPostInstall 会触发。 }
|
||||
end;
|
||||
|
||||
Code := EffectiveCode();
|
||||
if Code <> '' then
|
||||
{ ===== 给 [Run] 段用的取值 / 判断函数 ===== }
|
||||
|
||||
{ 配对码。配 HasPairingCode 一起用 —— 空串时那条命令会被跳过。 }
|
||||
function GetPairingCode(Param: String): String;
|
||||
begin
|
||||
Result := EffectiveCode();
|
||||
end;
|
||||
|
||||
{ 有配对码才跑那条 --code 命令 }
|
||||
function HasPairingCode(): Boolean;
|
||||
begin
|
||||
Result := EffectiveCode() <> '';
|
||||
end;
|
||||
|
||||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
|
||||
|
||||
必须在这里做,**不能**改到 UninstallRun 区段去:
|
||||
UninstallRun 是在文件都删完之后才跑的,
|
||||
那时候"停进程"已经没意义了(文件早没了)。
|
||||
|
||||
必须带 --keep-dir:
|
||||
InitializeUninstall 在卸载器开始删文件**之前**执行。
|
||||
不带 --keep-dir 的话被控端会安排一个后台 rmdir 删安装目录,
|
||||
而卸载器紧接着也在删同一个目录 —— 两边抢文件会**永久卡住**。
|
||||
(2026-10 实测:从控制面板卸载无限等待。两个原因叠加:
|
||||
这一行没带 --keep-dir;以及被控端里 input() 在无 stdin 时阻塞。)
|
||||
}
|
||||
{ 安装开始前强杀正在运行的被控端。
|
||||
|
||||
必须自己杀,不能指望 Inno 自带的关程序机制 ——
|
||||
它关不掉以管理员身份启动的常驻进程,然后弹框,静默模式下
|
||||
被压成"终止",导致整个安装回滚。
|
||||
|
||||
taskkill /F /IM ykxmz.exe 只会杀别的实例;
|
||||
这个进程本身是 setup 的辅助进程,名字不是 ykxmz.exe,不受影响。
|
||||
|
||||
返回空串 = 没问题,可以继续安装。 }
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
NeedsRestart := False;
|
||||
|
||||
if Exec('taskkill.exe', '/F /IM ykxmz.exe', '',
|
||||
SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||||
begin
|
||||
Params := '--code ' + Code;
|
||||
if not Exec(ExpandConstant('{app}\{#MyAppExeName}'), Params,
|
||||
ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated,
|
||||
ResultCode) then
|
||||
begin
|
||||
if not WizardSilent then
|
||||
MsgBox('配对码写入失败(错误码 ' + IntToStr(ResultCode) + ')。' + #13#10 +
|
||||
'装完之后双击程序图标,重新设一次也行。',
|
||||
mbInformation, MB_OK);
|
||||
end;
|
||||
{ 0 = 杀掉了;128 = 本来就没有在跑。都算正常。 }
|
||||
end;
|
||||
|
||||
{ 开机自启:单独调一次,也是一次性命令 }
|
||||
if WizardIsTaskSelected('autostart') then
|
||||
Exec(ExpandConstant('{app}\{#MyAppExeName}'), '--autostart on',
|
||||
ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
{ 给它一点时间释放文件句柄,否则覆盖 exe 还是会"拒绝访问" }
|
||||
Sleep(1200);
|
||||
end;
|
||||
|
||||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。 }
|
||||
function InitializeUninstall(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := True;
|
||||
Exec(ExpandConstant('{app}\{#MyAppExeName}'), '--uninstall',
|
||||
Exec(ExpandConstant('{app}\{#MyAppExeName}'), '--uninstall --keep-dir',
|
||||
ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user