fix(installer): 静默安装落到 /D= 而不是记忆目录 + 静默路径绕开 UAC 插件
真机实测发现:`安装包.exe /S /currentuser /D=D:\tmp\sbsilent` 退出码 0、耗时 12 秒,
但 /D= 目录是空的 —— 安装其实**落到了「安装目录记忆」里的那个目录**。
(我最初的判断「DoInstall 从未被调用」是错的:给 Section/DoInstall 打 trace 后实测整条链
从头跑到尾,只是 $INSTDIR 被 customInit 用注册表记忆值盖掉了;空目录是 .onInit 的
SetOutPath 先建出来的假象。)
- **/D= 优先级最高**:.onInit 先取 GetDParameter,customInit 之后再盖回 $INSTDIR
(/D= 不进 ${GetParameters},必须走 StdUtils.GetAllParameters)
- **静默入口**:Section "install" 在 ${Silent} 时 Call DoInstall + quitSuccess;
DoInstall 加一次性守卫 $instStarted,Tick 与 Section 谁先到谁干活,绝不装两遍
- **静默不弹 UAC**:per-machine 分支在静默时改用查进程令牌,非管理员退回按用户安装
- **${UAC_IsAdmin} 全局换成 CheckAdmin**(按 BUILD_UNINSTALLER 分流成 un.CheckAdmin):
构建期生成的 installer-custom.nsh 里 netsh 放行前那句判断原来走 UAC 插件,
无页窗口时不返回,是静默跑不通的最后一关
- 脚本不带 BOM(BOM 会被拼进大脚本中间),本机用 -INPUTCHARSET UTF8 编译
验证(wine):非静默 P1→P5 走完且目录/卸载器齐全;静默 /S /currentuser /D=C:\v4 → exit 0、
文件与真卸载器都在 C:\v4;再静默卸载 → 目录消失、注册表零残留;/S /D=... 亦落在 /D=。
真机由主代理复验。
This commit is contained in:
1 parent
9fee6f2da5
commit
a1aeed3c55
1 file changed
+134
-17
+134
-17
@@ -145,7 +145,9 @@ Var artReady
|
|||||||
Var licOk ; 许可页勾选
|
Var licOk ; 许可页勾选
|
||||||
Var runApp ; 完成页「立即运行」
|
Var runApp ; 完成页「立即运行」
|
||||||
Var modeAll ; 安装模式单选:1=为所有用户,0=仅为我
|
Var modeAll ; 安装模式单选:1=为所有用户,0=仅为我
|
||||||
Var instStarted ; 安装是否已经开工(Tick 里触发,只触发一次)
|
Var instStarted ; 安装流程是否已经跑起来(一次性,见 DoInstall 开头的守卫)
|
||||||
|
Var gIsAdmin ; CheckAdmin 的结果:1 = 当前进程已提权
|
||||||
|
Var gDparam ; 命令行 /D= 指定的安装目录(要在 customInit 的记忆目录之后盖回去)
|
||||||
Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局)
|
Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局)
|
||||||
Var dragging
|
Var dragging
|
||||||
|
|
||||||
@@ -255,6 +257,47 @@ Var dragging
|
|||||||
; 2) MUI_INSERT 是在第一个 MUI_LANGUAGE 处展开的,界面初始化钩子必须在那之前定义。
|
; 2) MUI_INSERT 是在第一个 MUI_LANGUAGE 处展开的,界面初始化钩子必须在那之前定义。
|
||||||
!define MUI_CUSTOMFUNCTION_GUIINIT "GuiInit"
|
!define MUI_CUSTOMFUNCTION_GUIINIT "GuiInit"
|
||||||
|
|
||||||
|
; 当前进程是否已提权(结果放 $gIsAdmin)。
|
||||||
|
; 静默安装要靠它决定「为所有用户」还是退一步「仅为我安装」,而这条路上一个 UAC 插件调用都不能有:
|
||||||
|
; 静默模式没有页窗口,UAC::_ 会卡住不返回(本机 wine 实测),真机上 UAC_RunElevated 会弹框等人点 ——
|
||||||
|
; 无人值守时那两件事都等于装不完。查令牌问的是同一个问题,且不依赖任何窗口。
|
||||||
|
Function CheckAdmin
|
||||||
|
Push $0
|
||||||
|
Push $R1
|
||||||
|
Push $R2
|
||||||
|
Push $R3
|
||||||
|
Push $R4
|
||||||
|
Push $R5
|
||||||
|
Push $R6
|
||||||
|
Push $R7
|
||||||
|
StrCpy $gIsAdmin 0
|
||||||
|
System::Call 'kernel32::GetCurrentProcess() p .R1'
|
||||||
|
System::Call '*(p 0) p .R2'
|
||||||
|
System::Call 'advapi32::OpenProcessToken(p R1, i 0x0008, p R2) i .R3' ; TOKEN_QUERY
|
||||||
|
${If} $R3 != 0
|
||||||
|
System::Call '*$R2(p .R4)'
|
||||||
|
System::Call '*(i 0) p .R5'
|
||||||
|
System::Call 'advapi32::GetTokenInformation(p R4, i 20, p R5, i 4, p 0) i .R6' ; TokenElevation
|
||||||
|
${If} $R6 != 0
|
||||||
|
System::Call '*$R5(i .R7)'
|
||||||
|
${If} $R7 != 0
|
||||||
|
StrCpy $gIsAdmin 1
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
System::Call 'kernel32::CloseHandle(p R4)'
|
||||||
|
System::Free $R5
|
||||||
|
${EndIf}
|
||||||
|
System::Free $R2
|
||||||
|
Pop $R7
|
||||||
|
Pop $R6
|
||||||
|
Pop $R5
|
||||||
|
Pop $R4
|
||||||
|
Pop $R3
|
||||||
|
Pop $R2
|
||||||
|
Pop $R1
|
||||||
|
Pop $0
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
; 「应用还在跑吗」的检查。不查的话覆盖安装时 CopyFiles 会一路失败,
|
; 「应用还在跑吗」的检查。不查的话覆盖安装时 CopyFiles 会一路失败,
|
||||||
; 用户最后只看到一个「无法关闭,重试/取消」的框,根本不知道要先关程序。
|
; 用户最后只看到一个「无法关闭,重试/取消」的框,根本不知道要先关程序。
|
||||||
; 逻辑取自 allowOnlyOneInstallerInstance.nsh 的 CHECK_APP_RUNNING,但不能直接插那个宏:
|
; 逻辑取自 allowOnlyOneInstallerInstance.nsh 的 CHECK_APP_RUNNING,但不能直接插那个宏:
|
||||||
@@ -315,6 +358,25 @@ Var dragging
|
|||||||
; 顺带这也让 un.onInit 里的 initMultiUser 走到「卸载器需要提权」的那条分支,
|
; 顺带这也让 un.onInit 里的 initMultiUser 走到「卸载器需要提权」的那条分支,
|
||||||
; 与上游卸载器编译结果一致。
|
; 与上游卸载器编译结果一致。
|
||||||
|
|
||||||
|
; ── 提权判断:把 ${UAC_IsAdmin} 换成自己的实现 ─────────────────────────────
|
||||||
|
; 理由只有一个:静默安装 / 静默卸载都不能碰 UAC 插件。没有页窗口时 UAC::_ 在本机 wine 下
|
||||||
|
; 直接不返回(真机上 UAC_RunElevated 也会弹一个没人点的框)—— 无人值守装机就卡在那儿装不完。
|
||||||
|
; 问的是同一个问题(进程令牌是否已提权),换掉之后连构建期生成的 installer-custom.nsh
|
||||||
|
; (netsh 放行/回收前后那段判断)也一起受益,不用去改生成器。
|
||||||
|
; 必须放在 !define BUILD_UNINSTALLER 之前:下面按那个 define 决定叫哪一个实现
|
||||||
|
; (卸载器里的 Call 只能指向 un. 开头的函数)。
|
||||||
|
!undef UAC_IsAdmin
|
||||||
|
!macroundef _UAC_IsAdmin
|
||||||
|
!define UAC_IsAdmin `"" UAC_IsAdmin ""`
|
||||||
|
!macro _UAC_IsAdmin _a _b _t _f
|
||||||
|
!ifdef BUILD_UNINSTALLER
|
||||||
|
Call un.CheckAdmin
|
||||||
|
!else
|
||||||
|
Call CheckAdmin
|
||||||
|
!endif
|
||||||
|
StrCmp $gIsAdmin "1" `${_t}` `${_f}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
!macro customCheckAppRunning
|
!macro customCheckAppRunning
|
||||||
Call un.CheckAppRunningImpl
|
Call un.CheckAppRunningImpl
|
||||||
!macroend
|
!macroend
|
||||||
@@ -331,11 +393,48 @@ FunctionEnd
|
|||||||
Function un.CheckAppRunningImpl
|
Function un.CheckAppRunningImpl
|
||||||
!insertmacro AppRunningCheck
|
!insertmacro AppRunningCheck
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
; 卸载器侧的提权判断(同一套令牌查询,只是名字要带 un. 前缀)
|
||||||
|
Function un.CheckAdmin
|
||||||
|
Push $0
|
||||||
|
Push $R1
|
||||||
|
Push $R2
|
||||||
|
Push $R3
|
||||||
|
Push $R4
|
||||||
|
Push $R5
|
||||||
|
Push $R6
|
||||||
|
Push $R7
|
||||||
|
StrCpy $gIsAdmin 0
|
||||||
|
System::Call 'kernel32::GetCurrentProcess() p .R1'
|
||||||
|
System::Call '*(p 0) p .R2'
|
||||||
|
System::Call 'advapi32::OpenProcessToken(p R1, i 0x0008, p R2) i .R3'
|
||||||
|
${If} $R3 != 0
|
||||||
|
System::Call '*$R2(p .R4)'
|
||||||
|
System::Call '*(i 0) p .R5'
|
||||||
|
System::Call 'advapi32::GetTokenInformation(p R4, i 20, p R5, i 4, p 0) i .R6'
|
||||||
|
${If} $R6 != 0
|
||||||
|
System::Call '*$R5(i .R7)'
|
||||||
|
${If} $R7 != 0
|
||||||
|
StrCpy $gIsAdmin 1
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
System::Call 'kernel32::CloseHandle(p R4)'
|
||||||
|
System::Free $R5
|
||||||
|
${EndIf}
|
||||||
|
System::Free $R2
|
||||||
|
Pop $R7
|
||||||
|
Pop $R6
|
||||||
|
Pop $R5
|
||||||
|
Pop $R4
|
||||||
|
Pop $R3
|
||||||
|
Pop $R2
|
||||||
|
Pop $R1
|
||||||
|
Pop $0
|
||||||
|
FunctionEnd
|
||||||
!undef BUILD_UNINSTALLER
|
!undef BUILD_UNINSTALLER
|
||||||
|
|
||||||
!insertmacro addLangs
|
!insertmacro addLangs
|
||||||
|
|
||||||
|
|
||||||
; ── 通用函数 ───────────────────────────────────────────────────────────────
|
; ── 通用函数 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Function CheckAppRunningImpl
|
Function CheckAppRunningImpl
|
||||||
@@ -530,7 +629,6 @@ Function Tick
|
|||||||
; 我们挂在 StdUtils 定时器上的悬停/进度刷新全都收不到回调(实测:P4 上按钮不悬停、
|
; 我们挂在 StdUtils 定时器上的悬停/进度刷新全都收不到回调(实测:P4 上按钮不悬停、
|
||||||
; 定时器不触发)。改成自绘页 + 定时器驱动,消息泵一直是通的。
|
; 定时器不触发)。改成自绘页 + 定时器驱动,消息泵一直是通的。
|
||||||
${If} $instStarted == 0
|
${If} $instStarted == 0
|
||||||
StrCpy $instStarted 1
|
|
||||||
Call DoInstall
|
Call DoInstall
|
||||||
GetDlgItem $0 $HWNDPARENT 1
|
GetDlgItem $0 $HWNDPARENT 1
|
||||||
SendMessage $0 ${BM_CLICK} 0 0
|
SendMessage $0 ${BM_CLICK} 0 0
|
||||||
@@ -1197,10 +1295,21 @@ Function .onInit
|
|||||||
${AndIfNot} ${UAC_IsInnerInstance}
|
${AndIfNot} ${UAC_IsInnerInstance}
|
||||||
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
|
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
; 命令行 /D= 的优先级最高:用户明说了装哪,就不该被「上次装到哪」的记忆改掉。
|
||||||
|
; initMultiUser 内部已经把 /D= 应用过一次,但紧随其后的 customInit(构建期生成)会拿
|
||||||
|
; 注册表里的记忆目录盖掉 $INSTDIR —— 这里先把 /D= 的值留一份,customInit 之后盖回去。
|
||||||
|
; 现场装机脚本就是靠 /D= 指定目录的(安装包.exe /S /currentuser /D=...),
|
||||||
|
; 记忆把它顶掉的话,脚本会以为装到了指定目录,其实文件落在别处。
|
||||||
|
; 注意 /D= 不在 ${GetParameters} 里(NSIS 自己把它从命令行里摘掉了),
|
||||||
|
; 所以用上游 multiUser.nsh 的 GetDParameter —— 它走 StdUtils.GetAllParameters 拿原始命令行再手工解析。
|
||||||
|
!insertmacro GetDParameter $gDparam
|
||||||
!insertmacro initMultiUser
|
!insertmacro initMultiUser
|
||||||
!ifmacrodef customInit
|
!ifmacrodef customInit
|
||||||
!insertmacro customInit
|
!insertmacro customInit
|
||||||
!endif
|
!endif
|
||||||
|
${If} $gDparam != ""
|
||||||
|
StrCpy $INSTDIR $gDparam
|
||||||
|
${EndIf}
|
||||||
StrCpy $runApp 1
|
StrCpy $runApp 1
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
@@ -1238,6 +1347,13 @@ FunctionEnd
|
|||||||
; 安装流程本体。放在函数里而不是 Section 里,是为了让 P4 能是自绘页(原因见 Tick)。
|
; 安装流程本体。放在函数里而不是 Section 里,是为了让 P4 能是自绘页(原因见 Tick)。
|
||||||
; 顺序逐行对照 electron-builder 的 templates/nsis/installSection.nsh。
|
; 顺序逐行对照 electron-builder 的 templates/nsis/installSection.nsh。
|
||||||
Function DoInstall
|
Function DoInstall
|
||||||
|
; 一次性守卫:交互安装由 P4 的定时器触发,静默安装由安装段触发,
|
||||||
|
; 两条路都从这里进,谁先到谁干活,绝不可能装两遍。
|
||||||
|
${If} $instStarted == 1
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $instStarted 1
|
||||||
|
|
||||||
; 静默安装没有 UI 可选安装模式,per-machine 这一趟要在这里补提权。
|
; 静默安装没有 UI 可选安装模式,per-machine 这一趟要在这里补提权。
|
||||||
; 比上游多一个 $installMode == "all" 的条件:上游只在「已有 per-machine 安装」时提权,
|
; 比上游多一个 $installMode == "all" 的条件:上游只在「已有 per-machine 安装」时提权,
|
||||||
; 而我们把默认模式改成了「为所有用户」,全新静默安装(/S)就会默认往 Program Files 写,
|
; 而我们把默认模式改成了「为所有用户」,全新静默安装(/S)就会默认往 Program Files 写,
|
||||||
@@ -1246,21 +1362,17 @@ Function DoInstall
|
|||||||
!ifndef ONE_CLICK
|
!ifndef ONE_CLICK
|
||||||
${if} $installMode == "all"
|
${if} $installMode == "all"
|
||||||
${andIf} ${Silent}
|
${andIf} ${Silent}
|
||||||
${ifNot} ${UAC_IsAdmin}
|
; 静默安装**绝不弹 UAC**:无人值守时没人点那个框,弹出即等于装不完。
|
||||||
ShowWindow $HWNDPARENT ${SW_HIDE}
|
; 所以这里完全不碰 UAC 插件(提权与判断都不做),只查一眼令牌:
|
||||||
!insertmacro UAC_RunElevated
|
; 已经是管理员 → 按「为所有用户」继续,装到 Program Files 并放行防火墙;
|
||||||
${Switch} $0
|
; 不是管理员 → 退回「仅为我安装」(%LOCALAPPDATA%\Programs\...),
|
||||||
${Case} 0
|
; 不需要提权也能装完,退出码仍然是 0。
|
||||||
${Break}
|
; 交互安装不提权的事在 P3 离开时按 multiUserUi 的范式处理,与这里无关。
|
||||||
${Case} 1223 ;用户取消了 UAC
|
Call CheckAdmin
|
||||||
${Break}
|
${if} $gIsAdmin == 1
|
||||||
${Default}
|
|
||||||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
|
|
||||||
${Break}
|
|
||||||
${EndSwitch}
|
|
||||||
Quit
|
|
||||||
${else}
|
|
||||||
!insertmacro setInstallModePerAllUsers
|
!insertmacro setInstallModePerAllUsers
|
||||||
|
${else}
|
||||||
|
!insertmacro setInstallModePerUser
|
||||||
${endIf}
|
${endIf}
|
||||||
${endIf}
|
${endIf}
|
||||||
!endif
|
!endif
|
||||||
@@ -1368,6 +1480,11 @@ FunctionEnd
|
|||||||
; 之所以不把静默安装也塞进 .onInit:实测那样会在 wine 里挂死(.onInit 阶段还没有
|
; 之所以不把静默安装也塞进 .onInit:实测那样会在 wine 里挂死(.onInit 阶段还没有
|
||||||
; 页对话框,DoInstall 里的界面相关动作没有宿主)。段这条路是上游验证过的。
|
; 页对话框,DoInstall 里的界面相关动作没有宿主)。段这条路是上游验证过的。
|
||||||
Section "install" INSTALL_SECTION_ID
|
Section "install" INSTALL_SECTION_ID
|
||||||
|
; 静默安装(/S):没有任何页面会被创建,P4 的定时器永远不会触发,所以这里就是唯一入口。
|
||||||
|
; NSIS 在静默模式下照常执行 Section、只是不显示页面 —— 和上游 electron-builder 安装器
|
||||||
|
; 「段在 instfiles 页上执行」是同一条路(区别只是我们把 instfiles 页 Abort 掉了)。
|
||||||
|
; 交互安装时这一支不执行:那时安装段根本不会被走到(instfiles 页被 Abort),
|
||||||
|
; 安装由 P4 的定时器调 DoInstall(DoInstall 自带一次性守卫,不会装两遍)。
|
||||||
${If} ${Silent}
|
${If} ${Silent}
|
||||||
Call DoInstall
|
Call DoInstall
|
||||||
!insertmacro quitSuccess
|
!insertmacro quitSuccess
|
||||||
|
|||||||
Reference in new issue
Block a user