diff --git a/console/installer/installer.nsi b/console/installer/installer.nsi index 587c2c4..3a2d901 100644 --- a/console/installer/installer.nsi +++ b/console/installer/installer.nsi @@ -145,7 +145,9 @@ Var artReady Var licOk ; 许可页勾选 Var runApp ; 完成页「立即运行」 Var modeAll ; 安装模式单选:1=为所有用户,0=仅为我 -Var instStarted ; 安装是否已经开工(Tick 里触发,只触发一次) +Var instStarted ; 安装流程是否已经跑起来(一次性,见 DoInstall 开头的守卫) +Var gIsAdmin ; CheckAdmin 的结果:1 = 当前进程已提权 +Var gDparam ; 命令行 /D= 指定的安装目录(要在 customInit 的记忆目录之后盖回去) Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局) Var dragging @@ -255,6 +257,47 @@ Var dragging ; 2) MUI_INSERT 是在第一个 MUI_LANGUAGE 处展开的,界面初始化钩子必须在那之前定义。 !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 会一路失败, ; 用户最后只看到一个「无法关闭,重试/取消」的框,根本不知道要先关程序。 ; 逻辑取自 allowOnlyOneInstallerInstance.nsh 的 CHECK_APP_RUNNING,但不能直接插那个宏: @@ -315,6 +358,25 @@ Var dragging ; 顺带这也让 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 Call un.CheckAppRunningImpl !macroend @@ -331,11 +393,48 @@ FunctionEnd Function un.CheckAppRunningImpl !insertmacro AppRunningCheck 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 !insertmacro addLangs - ; ── 通用函数 ─────────────────────────────────────────────────────────────── Function CheckAppRunningImpl @@ -530,7 +629,6 @@ Function Tick ; 我们挂在 StdUtils 定时器上的悬停/进度刷新全都收不到回调(实测:P4 上按钮不悬停、 ; 定时器不触发)。改成自绘页 + 定时器驱动,消息泵一直是通的。 ${If} $instStarted == 0 - StrCpy $instStarted 1 Call DoInstall GetDlgItem $0 $HWNDPARENT 1 SendMessage $0 ${BM_CLICK} 0 0 @@ -1197,10 +1295,21 @@ Function .onInit ${AndIfNot} ${UAC_IsInnerInstance} !insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE ${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 !ifmacrodef customInit !insertmacro customInit !endif + ${If} $gDparam != "" + StrCpy $INSTDIR $gDparam + ${EndIf} StrCpy $runApp 1 FunctionEnd @@ -1238,6 +1347,13 @@ FunctionEnd ; 安装流程本体。放在函数里而不是 Section 里,是为了让 P4 能是自绘页(原因见 Tick)。 ; 顺序逐行对照 electron-builder 的 templates/nsis/installSection.nsh。 Function DoInstall + ; 一次性守卫:交互安装由 P4 的定时器触发,静默安装由安装段触发, + ; 两条路都从这里进,谁先到谁干活,绝不可能装两遍。 + ${If} $instStarted == 1 + Return + ${EndIf} + StrCpy $instStarted 1 + ; 静默安装没有 UI 可选安装模式,per-machine 这一趟要在这里补提权。 ; 比上游多一个 $installMode == "all" 的条件:上游只在「已有 per-machine 安装」时提权, ; 而我们把默认模式改成了「为所有用户」,全新静默安装(/S)就会默认往 Program Files 写, @@ -1246,21 +1362,17 @@ Function DoInstall !ifndef ONE_CLICK ${if} $installMode == "all" ${andIf} ${Silent} - ${ifNot} ${UAC_IsAdmin} - ShowWindow $HWNDPARENT ${SW_HIDE} - !insertmacro UAC_RunElevated - ${Switch} $0 - ${Case} 0 - ${Break} - ${Case} 1223 ;用户取消了 UAC - ${Break} - ${Default} - MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0" - ${Break} - ${EndSwitch} - Quit - ${else} + ; 静默安装**绝不弹 UAC**:无人值守时没人点那个框,弹出即等于装不完。 + ; 所以这里完全不碰 UAC 插件(提权与判断都不做),只查一眼令牌: + ; 已经是管理员 → 按「为所有用户」继续,装到 Program Files 并放行防火墙; + ; 不是管理员 → 退回「仅为我安装」(%LOCALAPPDATA%\Programs\...), + ; 不需要提权也能装完,退出码仍然是 0。 + ; 交互安装不提权的事在 P3 离开时按 multiUserUi 的范式处理,与这里无关。 + Call CheckAdmin + ${if} $gIsAdmin == 1 !insertmacro setInstallModePerAllUsers + ${else} + !insertmacro setInstallModePerUser ${endIf} ${endIf} !endif @@ -1368,6 +1480,11 @@ FunctionEnd ; 之所以不把静默安装也塞进 .onInit:实测那样会在 wine 里挂死(.onInit 阶段还没有 ; 页对话框,DoInstall 里的界面相关动作没有宿主)。段这条路是上游验证过的。 Section "install" INSTALL_SECTION_ID + ; 静默安装(/S):没有任何页面会被创建,P4 的定时器永远不会触发,所以这里就是唯一入口。 + ; NSIS 在静默模式下照常执行 Section、只是不显示页面 —— 和上游 electron-builder 安装器 + ; 「段在 instfiles 页上执行」是同一条路(区别只是我们把 instfiles 页 Abort 掉了)。 + ; 交互安装时这一支不执行:那时安装段根本不会被走到(instfiles 页被 Abort), + ; 安装由 P4 的定时器调 DoInstall(DoInstall 自带一次性守卫,不会装两遍)。 ${If} ${Silent} Call DoInstall !insertmacro quitSuccess