真机取证:StdUtils.TimerCreate 实测**每 ~1500ms 才跳一次**(真机 tick 计数 7 次/10.7 秒),
且请求 4/20/60/250ms 四个间隔出来的都是 1500ms 且完全同步 —— 等于与请求值无关的低分辨率实现。
上一版拿它做 60ms 轮询,于是:
- **窗口拖不动**:1.5 秒才采一次光标,而人手一次拖(按下→移动→松开)不到 1.5 秒,
永远采不到「左键按下」那一刻。判别性实验(wine,人手速度 ~0.6 秒):旧代码 dx=0/dy=0,
新代码 dx=200/dy=100。(上一轮 wine 测的是 6 秒慢速拖,慢到 1.5s 的 tick 也能采到,所以漏了。)
- **进度只显示 0% 与 100%**:真因不是 tick,而是分段耗时极不均匀(几乎全在解压,后 4 段每段 <1ms)
→ 每段加 400ms 下限,真机 trace 407/484/656ms,肉眼可见。
- **装完卡在「收尾」**:翻页那次 BM_CLICK 只发一次,丢了就永远停在第 4 页 → 改成装完后每 tick 重试
($instDone 门防止安装途中的 Sleep 泵出 tick 乱点)。
- 顺带修好 P5「完成」页:定时器改为每页重建,P5 有独立一份,悬停与拖动都恢复。
改法:${StdUtils.TimerCreate} → ${NSD_CreateTimer}(挂在当前页对话框上,每页 NewPage 重建、
KillTick 里 ${NSD_KillTimer},删掉 $timerId);Stage 加 STAGE_DWELL 400 + GetTickCount 补 Sleep。
真机验证:每页 tick ≈63ms 且 P1..P5 都活着;分段停留 ≥400ms;P4 自动翻到完成页;
静默 /S exit=0 / 7.8s、旧版在跑时 exit=0 / 10.5s 且 running_after=0。
wine 回归:人手速度拖动、原生控件隐藏、P3 翻页、✕ 退出无 StdUtils 报错、/S /currentuser /D= 正常。
真机无输入桌面,鼠标驱动的悬停/拖动只能靠 wine 几何证据 + 真机 tick 频率证据。
1771 lines
73 KiB
NSIS
1771 lines
73 KiB
NSIS
; =============================================================================
|
||
; 赛事积分榜 安装器 —— 完全自绘的五页向导
|
||
;
|
||
; 这是 electron-builder 的「自定义 installer 脚本」(package.json 的 build.nsis.script)。
|
||
; 只换 UI 层:安装/卸载/升级/快捷方式/注册表/防火墙全部复用 electron-builder 模板里的宏,
|
||
; 一个字节的安装逻辑都不重写 —— 现场装机成功率不被这次改版动摇。
|
||
; 唯一被换掉的是「关闭在跑的旧版本」那一步(上游 CHECK_APP_RUNNING 的提示框在自绘窗口下
|
||
; 用户根本看不见,真机上表现为「旧程序一直没被关掉」),换法见下面 CloseRunningApp 的注释。
|
||
; 提权判断也一样:${UAC_IsAdmin} 换成了进程令牌查询,理由见 CheckAdmin 上面的注释。
|
||
;
|
||
; 为什么整窗去边框、铺满位图:
|
||
; 设计要求是 760×480 的固定版面(左侧品牌栏 / 顶部标题栏 / 右侧内容区 / 底部按钮条)。
|
||
; 系统标题栏和原生按钮条在不同 Windows 版本上高度都不一样,要对齐只能自己画。
|
||
; 所以:去掉窗口边框 → 内层对话框铺满整个客户区 → 位图当画布 → 标准按钮条被盖住,
|
||
; 但仍然是「程序化翻页」的工具(点我们的按钮 = 点被盖住的系统按钮)。
|
||
;
|
||
; 页面流程:P1 欢迎 → P2 许可协议 → P3 安装位置与安装模式 → P4 正在安装 → P5 完成。
|
||
; P1/P2/P3/P5 是 Page custom(nsDialogs 自建内层对话框,POC 已验证);
|
||
; P4 必须是**真正的** instfiles 页,原因有两个,都不是偏好问题:
|
||
; 1. 安装段(Section)只在 instfiles 页上执行。不用 instfiles 页就得把整段安装逻辑塞进
|
||
; 一个函数,静默安装、取消中断、进度刷新全得自己重写 —— 那才是真正的风险源;
|
||
; 2. WriteUninstaller / 卸载段的执行时机也依赖这条路径。
|
||
; 代价是 instfiles 页自带的内层对话框(原生进度条 + 详情列表)只能藏起来,
|
||
; 在上面再盖一层我们自己的对话框。
|
||
;
|
||
; 高 DPI(现场是 4K@200%):
|
||
; ManifestDPIAware true + 运行时读 LOGPIXELSX,所有坐标 = 1x 设计值 × dpi / 96,
|
||
; 一个绝对像素值都不许写死在控件上;素材出 1x/2x 两套,dpi ≥ 144 用 2x,
|
||
; 并且给 LoadImage 传目标 cx/cy,让系统在 125%/150% 这类中间档位上兜底缩放。
|
||
; 字号不加倍:NSIS 的 CreateFont 自己会按系统 DPI 把点数换成像素,再乘一次就双倍了。
|
||
; =============================================================================
|
||
|
||
; ── 素材位置 ────────────────────────────────────────────────────────────────
|
||
; 走 ${PROJECT_DIR}(electron-builder 注入的绝对路径)而不是相对路径:
|
||
; electron-builder 是把脚本喂给 makensis 的 stdin、并把 makensis 的工作目录设成它的模板目录,
|
||
; 相对路径落点不确定。
|
||
!define ART_SRC "${PROJECT_DIR}\installer\art"
|
||
|
||
; ── 版面常量 ────────────────────────────────────────────────────────────────
|
||
; 全部是 art-gen/gen_art.py 里的 1x 设计值,改素材就要回来改这里(两边必须一致)。
|
||
!define WIN_W 760
|
||
!define WIN_H 480
|
||
!define TB_H 40
|
||
!define SIDE_W 240
|
||
!define BRAND_H 440
|
||
!define PG_X 240
|
||
!define PG_Y 40
|
||
!define PG_W 520
|
||
!define PG_H 440
|
||
!define PAD_L 32
|
||
!define CONTENT_W 456
|
||
!define BTN_W 108
|
||
!define BTN_H 36
|
||
!define BTN_Y 416
|
||
!define BTN_PRIMARY_X 620
|
||
!define BTN_SECONDARY_X 500
|
||
!define BROWSE_W 96
|
||
!define BROWSE_H 34
|
||
!define TB_MIN_X 672
|
||
!define TB_CLOSE_X 716
|
||
!define TB_BTN_W 44
|
||
; 页面内控件 → 窗口坐标 = 页内坐标 + (PG_X, PG_Y),这里直接写成窗口坐标
|
||
!define LIC_EDIT_X 273
|
||
!define LIC_EDIT_Y 237
|
||
!define LIC_EDIT_H 174
|
||
!define LIC_EDIT_W 454
|
||
!define LIC_CHK_X 272
|
||
!define LIC_CHK_Y 432
|
||
!define PATH_EDIT_X 273
|
||
!define PATH_EDIT_Y 151
|
||
!define PATH_EDIT_W 357
|
||
!define PATH_EDIT_H 32
|
||
!define BROWSE_X 632
|
||
!define BROWSE_Y 150
|
||
!define RADIO1_X 272
|
||
!define RADIO1_Y 260
|
||
!define RADIO2_X 272
|
||
!define RADIO2_Y 300
|
||
!define SIZE_TXT_Y 336
|
||
!define STAGE_X 272
|
||
!define STAGE_Y 156
|
||
!define STAGE_W 456
|
||
!define STAGE_H 18
|
||
; 每一段进度(stage-N.bmp)至少显示多久(毫秒):见 Stage 里的注释 ——
|
||
; 耗时几乎全在「解压」那一段,不给个下限的话后几段肉眼根本看不见。
|
||
!define STAGE_DWELL 400
|
||
!define PROG_X 272
|
||
!define PROG_Y 180
|
||
!define PROG_W 456
|
||
!define PROG_H 8
|
||
!define FIN_CHK_X 272
|
||
!define FIN_CHK_Y 338
|
||
!define FIN_PATH_Y 376
|
||
|
||
; 颜色取自 gen_art.py 的配色常量(SetCtlColors 收 RRGGBB,不是 BGR)
|
||
!define CLR_TXT "0x16233a"
|
||
!define CLR_DIM "0x5a6b85"
|
||
!define CLR_BG "0xf6f9fd"
|
||
|
||
; ── 变量 ───────────────────────────────────────────────────────────────────
|
||
; electron-builder 的模板(assistedInstaller / installSection)引用了这些变量,
|
||
; 平时由 installer.nsi 主模板声明;我们用自定义脚本,得自己声明,名字必须一致。
|
||
Var newStartMenuLink
|
||
Var oldStartMenuLink
|
||
Var newDesktopLink
|
||
Var oldDesktopLink
|
||
Var oldShortcutName
|
||
Var oldMenuDirectory
|
||
Var appExe
|
||
Var launchLink
|
||
|
||
; 自绘界面状态
|
||
Var hPage ; 当前页的内层对话框
|
||
Var hBg
|
||
Var hBrand
|
||
Var hTitleBar
|
||
Var hMin
|
||
Var hClose
|
||
Var hNext
|
||
Var hBack
|
||
Var hCancel
|
||
Var hBrowse
|
||
Var hFinish
|
||
Var hCheck
|
||
Var hRadio1
|
||
Var hRadio2
|
||
Var hPath
|
||
Var hLicEdit
|
||
Var hStage
|
||
Var hProg
|
||
Var stMin
|
||
Var stClose
|
||
Var stNext
|
||
Var stBack
|
||
Var stCancel
|
||
Var stBrowse
|
||
Var stFinish
|
||
Var uiPage ; 0=无页面,1..5=当前页;定时器靠它判断还能碰哪些控件
|
||
Var timerOn ; tick 定时器当前是不是活的(建/销毁都认这个哨兵,幂等)
|
||
Var stageT0 ; 上一段进度的时刻(GetTickCount),用来保证每段都看得见
|
||
Var gDpi ; 系统 DPI(LOGPIXELSX)
|
||
Var gArt ; 本次要用的素材目录($PLUGINSDIR\1x 或 \2x)
|
||
Var gCx
|
||
Var gCy
|
||
Var gBw ; Track 宏把基准宽高放这里,转交给 SetBmp
|
||
Var gBh
|
||
Var tmpName
|
||
Var tmpPath
|
||
Var tmpTxt
|
||
Var artReady
|
||
Var licOk ; 许可页勾选
|
||
Var runApp ; 完成页「立即运行」
|
||
Var modeAll ; 安装模式单选:1=为所有用户,0=仅为我
|
||
Var instStarted ; 安装流程是否已经跑起来(一次性,见 DoInstall 开头的守卫)
|
||
Var instDone ; 安装流程是否已经跑完(tick 里据此才去点「下一步」翻到完成页)
|
||
Var gAdminDbg ; CheckAdmin 的原始值(判定为非提权时写进 %TEMP% 便于现场定位)
|
||
Var gIsAdmin ; CheckAdmin 的结果:1 = 当前进程已提权
|
||
Var dragCx ; 拖动锚点:按下瞬间的光标屏幕坐标
|
||
Var dragCy
|
||
Var dragWx ; 拖动锚点:按下瞬间的窗口左上角
|
||
Var dragWy
|
||
Var gDparam ; 命令行 /D= 指定的安装目录(要在 customInit 的记忆目录之后盖回去)
|
||
Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局)
|
||
Var dragging
|
||
|
||
|
||
; ── include ────────────────────────────────────────────────────────────────
|
||
; MUI2 不是为了它的页面,是为了 LangString / MUI_LANGUAGE:
|
||
; electron-builder 注入的 messages.nsh(以及 assistedMessages.nsh)用的是 MUI 的 LangString,
|
||
; 没有 MUI2 直接编译不过。我们的页面全是 Page custom,一个 MUI_PAGE_* 都不插。
|
||
!define MUI_DISABLE_INSERT_LANGUAGE_AFTER_PAGES_WARNING
|
||
!include "MUI2.nsh"
|
||
!include "nsDialogs.nsh"
|
||
!include "WinMessages.nsh"
|
||
!include "LogicLib.nsh"
|
||
!include "FileFunc.nsh"
|
||
!include "StrContains.nsh"
|
||
!include "common.nsh"
|
||
!include "multiUser.nsh"
|
||
!include "allowOnlyOneInstallerInstance.nsh"
|
||
|
||
; ── 「旧版本卸载失败」的处理:替换掉上游那个会把静默安装挂死的框 ──────────────
|
||
; 上游 installUtil.nsh 的 handleUninstallResult 在旧卸载器返回非 0 时弹
|
||
; MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
|
||
; 这个框**没有 /SD** —— 静默安装里没人能点,进程就一直等在那句上。
|
||
; 真机实例(2026-09-13):/S 原地升级时旧卸载器要为自己再要一次 UAC 提权,无人值守的会话里
|
||
; 那个请求直接被系统取消($R0=1223 = ERROR_CANCELLED),于是走到这个框:
|
||
; 安装器进程活着、没有任何窗口、十分钟也不结束(用窗口枚举才看到那个隐藏的模态框)。
|
||
; 上游给这两个 rootKey 各留了一个替换钩子(customUnInstallCheck / customUnInstallCheckCurrentUser),
|
||
; 定义它们即可整段接管,下面就是我们的处理:
|
||
; 静默安装:只记日志、继续装 —— 我们是就地覆盖同一个目录、文件锁由 CloseRunningApp 负责,
|
||
; 旧文件会被新安装覆盖,绝不能因为「旧卸载器自己没能提权」把人挂住;
|
||
; 交互安装:仍然按上游语义中止,但提示框是 topmost 的(自绘窗口会把普通框压在下面)。
|
||
; 必须放在 !include "installUtil.nsh" **之前**:那两个钩子是在 compile 那个函数时用 !ifmacrodef 判定的。
|
||
!macro customUnInstallCheck
|
||
${If} $R0 != 0
|
||
DetailPrint `旧版本卸载器返回 $R0(多为无人值守时无法提权),安装将直接覆盖旧文件继续。`
|
||
${IfNot} ${Silent}
|
||
MessageBox MB_OK|MB_ICONEXCLAMATION|MB_TOPMOST "$(uninstallFailed): $R0"
|
||
SetErrorLevel 2
|
||
Quit
|
||
${EndIf}
|
||
${EndIf}
|
||
!macroend
|
||
|
||
!macro customUnInstallCheckCurrentUser
|
||
; 与上面同一套:HKEY_CURRENT_USER 那份旧安装(早年装过 per-user 版本时才会有)
|
||
${If} $R0 != 0
|
||
DetailPrint `旧版本(当前用户)卸载器返回 $R0,安装将直接覆盖旧文件继续。`
|
||
${IfNot} ${Silent}
|
||
MessageBox MB_OK|MB_ICONEXCLAMATION|MB_TOPMOST "$(uninstallFailed): $R0"
|
||
SetErrorLevel 2
|
||
Quit
|
||
${EndIf}
|
||
${EndIf}
|
||
!macroend
|
||
|
||
!include "installUtil.nsh"
|
||
!include "installer.nsh"
|
||
|
||
; 安装模式初值逻辑(照抄 templates/nsis/assistedInstaller.nsh 的 initMultiUser):
|
||
; 上次怎么装的还怎么装;命令行 --allusers / --currentuser 压过它;
|
||
; 都没有时看 INSTALL_MODE_PER_ALL_USERS_DEFAULT(= package.json 的 selectPerMachineByDefault)。
|
||
; 我们不 include assistedInstaller.nsh(它会插一堆 MUI 原生页面),所以这个宏得自己带一份,
|
||
; 否则 uninstaller.nsh 的 un.onInit 里那句 !insertmacro initMultiUser 直接找不到宏。
|
||
!macro initMultiUser
|
||
!ifdef INSTALL_MODE_PER_ALL_USERS
|
||
!insertmacro setInstallModePerAllUsers
|
||
!else
|
||
; 静默安装跳过「内层进程 / 非管理员」这道判断:它俩走 UAC 插件的 UAC::_,
|
||
; 而静默模式下没有页窗口,插件会在这一句上不返回(本机 wine 实测卡死 60 秒)。
|
||
; 静默安装要提权时由安装流程开头那段($installMode == "all" + ${Silent})自己处理。
|
||
${IfNot} ${Silent}
|
||
${If} ${UAC_IsInnerInstance}
|
||
${AndIfNot} ${UAC_IsAdmin}
|
||
SetErrorLevel 0x666666
|
||
Quit
|
||
${EndIf}
|
||
${EndIf}
|
||
|
||
!ifndef MULTIUSER_INIT_TEXT_ADMINREQUIRED
|
||
!define MULTIUSER_INIT_TEXT_ADMINREQUIRED "$(^Caption) requires administrator privileges."
|
||
!endif
|
||
!ifndef MULTIUSER_INIT_TEXT_POWERREQUIRED
|
||
!define MULTIUSER_INIT_TEXT_POWERREQUIRED "$(^Caption) requires at least Power User privileges."
|
||
!endif
|
||
!ifndef MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE
|
||
!define MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE "Your user account does not have sufficient privileges to install $(^Name) for all users of this computer."
|
||
!endif
|
||
|
||
StrCpy $hasPerMachineInstallation "0"
|
||
StrCpy $hasPerUserInstallation "0"
|
||
ReadRegStr $perMachineInstallationFolder HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation
|
||
${if} $perMachineInstallationFolder != ""
|
||
StrCpy $hasPerMachineInstallation "1"
|
||
${endif}
|
||
ReadRegStr $perUserInstallationFolder HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation
|
||
${if} $perUserInstallationFolder != ""
|
||
StrCpy $hasPerUserInstallation "1"
|
||
${endif}
|
||
|
||
${GetParameters} $R0
|
||
${GetOptions} $R0 "/allusers" $R1
|
||
${IfNot} ${Errors}
|
||
StrCpy $hasPerMachineInstallation "1"
|
||
StrCpy $hasPerUserInstallation "0"
|
||
${EndIf}
|
||
${GetOptions} $R0 "/currentuser" $R1
|
||
${IfNot} ${Errors}
|
||
StrCpy $hasPerMachineInstallation "0"
|
||
StrCpy $hasPerUserInstallation "1"
|
||
${EndIf}
|
||
|
||
${if} $hasPerUserInstallation == "1"
|
||
${andif} $hasPerMachineInstallation == "0"
|
||
!insertmacro setInstallModePerUser
|
||
${elseif} $hasPerUserInstallation == "0"
|
||
${andif} $hasPerMachineInstallation == "1"
|
||
!insertmacro setInstallModePerAllUsers
|
||
${else}
|
||
!ifdef INSTALL_MODE_PER_ALL_USERS
|
||
!insertmacro setInstallModePerAllUsers
|
||
!else
|
||
!ifdef INSTALL_MODE_PER_ALL_USERS_DEFAULT
|
||
!insertmacro setInstallModePerAllUsers
|
||
!else
|
||
!insertmacro setInstallModePerUser
|
||
!endif
|
||
!endif
|
||
${endif}
|
||
!endif
|
||
!macroend
|
||
|
||
; ── 执行级别 ───────────────────────────────────────────────────────────────
|
||
; highest(= 清单里的 highestAvailable),不是硬 admin:
|
||
; 管理员账户的机器:一启动就弹 UAC、之后**全程**是提权态 —— 不再有「装到一半才提权」
|
||
; 那种闪退/卡住,防火墙规则也必然放行得掉;
|
||
; 受限账户的机器:不弹 UAC,按普通权限启动,走下面的降级路径装成「仅为我安装」,
|
||
; 至少装得上;硬 admin 在那种机器上直接起不来。
|
||
RequestExecutionLevel highest
|
||
|
||
; ── 编译期替 electron-builder 补的两处 ──────────────────────────────────────
|
||
; 1) 自定义脚本下 electron-builder 不做 BUILD_UNINSTALLER 那一趟,UNINSTALLER_OUT_FILE
|
||
; 被留成空值,而 include/installer.nsh 里有一条无条件的
|
||
; File "/oname=${UNINSTALL_FILENAME}" "${UNINSTALLER_OUT_FILE}"
|
||
; 空路径直接编译失败。真卸载器由安装段末尾的 WriteUninstaller 现写,
|
||
; 这里顶一个占位文件上去只为让编译过去(安装时会被立刻删掉再重写,见安装段)。
|
||
!ifdef UNINSTALLER_OUT_FILE
|
||
!define /redef UNINSTALLER_OUT_FILE "${PROJECT_DIR}\installer\uninstaller-stub.txt"
|
||
!else
|
||
!define UNINSTALLER_OUT_FILE "${PROJECT_DIR}\installer\uninstaller-stub.txt"
|
||
!endif
|
||
|
||
; 2) MUI_INSERT 是在第一个 MUI_LANGUAGE 处展开的,界面初始化钩子必须在那之前定义。
|
||
!define MUI_CUSTOMFUNCTION_GUIINIT "GuiInit"
|
||
; 用户中途取消/关窗时也把定时器收掉(先于 .onGUIEnd 跑,靠 $timerOn 幂等,不会重复销毁)
|
||
!define MUI_CUSTOMFUNCTION_ABORT "AbortCleanup"
|
||
|
||
; 当前进程是否已提权(结果放 $gIsAdmin)。判据只有一个:进程令牌的 TokenElevation 标志。
|
||
; 为什么不用 ${UAC_IsAdmin}(UAC 插件):静默安装那条约路上一个插件调用都不能有 ——
|
||
; 没有页窗口时 UAC::_ 会卡住不返回(本机 wine 实测),真机上 UAC_RunElevated 会弹框等人点,
|
||
; 无人值守装机就永远装不完。查令牌问的是同一个问题,且不依赖任何窗口。
|
||
; 为什么不用 IsUserAnAdmin / 组成员判定:那是「账户在不在管理员组」,在 UAC 拆分令牌下
|
||
; 受限进程照样回答「是」,答不了「这个进程提没提权」——真机上要判的正是后者。
|
||
;
|
||
; ⚠️ 真机踩过的坑(2026-09-13,主人真机复验「明明弹过 UAC 却说没有管理员权限」):
|
||
; GetTokenInformation 的 ReturnLength 参数传 NULL 时,Windows 直接返回失败
|
||
; (ERROR_INVALID_PARAMETER = 80),于是 $gIsAdmin 永远停在 0 —— 提权态被误判成非提权,
|
||
; 选「为所有用户安装」就被降级成「仅为我安装」。wine 不校验这个参数(真机上失败、wine 里成功),
|
||
; 所以本机怎么点都复现不出来,只有真机才暴露。必须给一个真实可写的 ReturnLength。
|
||
; 真机原始值:NULL → rc=0 err=80 val=0;给真指针 → rc=1 err=0 val=1。
|
||
Function CheckAdmin
|
||
Push $0
|
||
Push $R1
|
||
Push $R2
|
||
Push $R3
|
||
Push $R4
|
||
Push $R5
|
||
Push $R6
|
||
Push $R7
|
||
Push $R8
|
||
Push $R9
|
||
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
|
||
System::Call 'kernel32::GetLastError() i .R8'
|
||
StrCpy $gAdminDbg "OpenProcessToken=$R3 err=$R8"
|
||
${If} $R3 != 0
|
||
System::Call '*$R2(p .R4)'
|
||
System::Call '*(i 0, i 0) p .R5' ; TOKEN_ELEVATION(一个 DWORD)
|
||
System::Call '*(i 0) p .R6' ; ReturnLength:必须给真指针,见上面真机那条
|
||
System::Call 'advapi32::GetTokenInformation(p R4, i 20, p R5, i 4, p R6) i .R7'
|
||
System::Call 'kernel32::GetLastError() i .R8'
|
||
; 取值寄存器必须写 .R9 这种「大写 R + 数字」的形式:System 插件不认 .0,
|
||
; 那样 $0 会保持调用前的原样(常常是空串),而空串跟 "0" 的字符串比较是「不相等」,
|
||
; 判定就变成恒真了(真机探针上实测到过 val= 空串)。
|
||
System::Call '*$R5(i .R9)' ; TokenIsElevated:0 = 非提权,非 0 = 已提权
|
||
StrCpy $gAdminDbg "$gAdminDbg | TokenElevation rc=$R7 err=$R8 val=$R9" ; err 只在 rc=0 时有意义
|
||
; 查询本身失败时按非提权处理(与旧行为一致:宁可退一步装成「仅为我安装」,也不假装管理员)
|
||
${If} $R7 != 0
|
||
${If} $R9 != 0
|
||
StrCpy $gIsAdmin 1
|
||
${EndIf}
|
||
${EndIf}
|
||
System::Call 'kernel32::CloseHandle(p R4)'
|
||
System::Free $R5
|
||
System::Free $R6
|
||
${EndIf}
|
||
System::Free $R2
|
||
; 判定为「非提权」时把原始值落到 %TEMP%,真机上判定再出问题时能直接看出是哪一步不对
|
||
${If} $gIsAdmin == 0
|
||
FileOpen $R9 "$TEMP\Scoreboard-Console-elevation.txt" w
|
||
FileWrite $R9 "not elevated: $gAdminDbg$\r$\n"
|
||
FileClose $R9
|
||
${EndIf}
|
||
Pop $R9
|
||
Pop $R8
|
||
Pop $R7
|
||
Pop $R6
|
||
Pop $R5
|
||
Pop $R4
|
||
Pop $R3
|
||
Pop $R2
|
||
Pop $R1
|
||
Pop $0
|
||
FunctionEnd
|
||
|
||
; 「旧版本还在跑吗」的检查与关闭。不查的话覆盖安装时文件写入会一路失败,
|
||
; 用户最后只看到一个「无法关闭,重试/取消」的框,根本不知道要先关程序。
|
||
;
|
||
; 为什么不直接用上游 allowOnlyOneInstallerInstance.nsh 的 CHECK_APP_RUNNING(真机踩过的坑):
|
||
; 1. 它先弹 MessageBox 问「点确定就关掉」。我们的主窗口是无边框铺满的自绘窗口,
|
||
; 那个框既没有父窗口、也不是 topmost,实测被压在自绘层底下(wine 里干脆在那儿画一块黑矩形),
|
||
; 用户看不见、点不到,整条安装流程却卡在那句模态框上 —— 现场表现就是「旧程序一直没被关掉」;
|
||
; 2. 它探测进程的两条路都不适合现场:PowerShell 那条按 $INSTDIR 前缀过滤进程路径
|
||
; (绿色版、换过目录跑的实例一律漏掉;WMI 不可用时还会静默判成「没在跑」,
|
||
; 于是安装器照常往下装、文件被占用),tasklist 那条按 %USERNAME% 过滤
|
||
; (提权安装器跑在另一个账户下就漏);
|
||
; 3. 它的宏体自带 Var /GLOBAL CmdPath / PowerShellPath,一个脚本只能插一次,
|
||
; 而这一处已经被 un.checkAppRunning 占掉;卸载器侧的 Call 又只能指向 un. 函数。
|
||
;
|
||
; ⚠️ 为什么探测/关闭一律用 nsProcess 插件,而不是 cmd+tasklist|findstr+taskkill(真机复验的教训):
|
||
; 上一版就是那么写的,在真机上把 `/S` 静默安装**挂死了 7 分钟以上不结束**(同一台机器、同一个包,
|
||
; 改动前 18 秒 exit 0)。原因是那一套要起外部进程 cmd.exe,再经管道接 tasklist / findstr,
|
||
; 而 nsExec::Exec **没有任何超时**:子进程只要不退出(非交互/服务会话里 tasklist 起不来、
|
||
; 管道那头没人收),安装器就永远停在那一句上 —— 静默装机没有窗口,现场只能看到「进程还在、没反应」。
|
||
; wine 的 tasklist 是瞬间返回的,所以本机怎么测都不复现。
|
||
; nsProcess 是**进程内**插件(内部就是 Toolhelp 快照),不起 shell、不走管道、不等待外部进程,
|
||
; 只可能返回一个错误码,不可能挂住;上游模板自己也带着它(include/nsProcess.nsh)。
|
||
;
|
||
; 关闭顺序:① 先温和关应用本体(_CloseProcess = 给主窗口发 WM_CLOSE,等同用户点右上角,
|
||
; Electron 能自己收尾并带走它拉起的服务端)→ ② 还活着就强杀(_KillProcess)
|
||
; → ③ 再单独收一遍 scoreboard.exe 子进程(父进程被强杀时它会变成孤儿,还抓着 $INSTDIR 里的文件)
|
||
; → ④ 真关不掉:交互给 topmost 重试框、静默以非 0 退出,两种都绝不静默往下装
|
||
; (覆盖正被占用的文件,装出来的东西是坏的)。所有等待都是「固定次数 × 固定间隔」,全程有界。
|
||
; 两侧(安装/卸载)共用这份宏体:宏是在**插入处**展开的,所以 ${GetProcessInfo} 会按当时的
|
||
; BUILD_UNINSTALLER 自动选 _GetProcessInfo / un._GetProcessInfo。
|
||
!ifndef nsProcess::FindProcess
|
||
!include "nsProcess.nsh"
|
||
!endif
|
||
|
||
!macro CloseRunningApp
|
||
Push $0
|
||
Push $R1
|
||
Push $R2
|
||
; 本进程映像名:万一安装包被改名成跟应用同名,下面「按名字」关/杀就会打到安装器自己,
|
||
; 所以只有确认本进程名跟应用名不同才动手(同上游那句 ${if} $3 != "${APP_EXECUTABLE_FILENAME}")。
|
||
${GetProcessInfo} 0 $pid $1 $2 $3 $4
|
||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R1
|
||
${If} $R1 == 0
|
||
${If} $3 != "${APP_EXECUTABLE_FILENAME}"
|
||
DetailPrint "$(appClosing)"
|
||
${nsProcess::CloseProcess} "${APP_EXECUTABLE_FILENAME}" $0
|
||
${EndIf}
|
||
StrCpy $R2 0
|
||
cra_close: ; 给它时间自己收尾(也等文件句柄释放)
|
||
Sleep 700
|
||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R1
|
||
${If} $R1 != 0
|
||
Goto cra_server
|
||
${EndIf}
|
||
IntOp $R2 $R2 + 1
|
||
${If} $R2 < 5
|
||
Goto cra_close
|
||
${EndIf}
|
||
${If} $3 != "${APP_EXECUTABLE_FILENAME}"
|
||
${nsProcess::KillProcess} "${APP_EXECUTABLE_FILENAME}" $0
|
||
${EndIf}
|
||
StrCpy $R2 0
|
||
cra_kill:
|
||
Sleep 700
|
||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R1
|
||
${If} $R1 != 0
|
||
Goto cra_server
|
||
${EndIf}
|
||
IntOp $R2 $R2 + 1
|
||
${If} $R2 < 8
|
||
Goto cra_kill
|
||
${EndIf}
|
||
; 关不掉。交互安装:topmost 的重试框,点「重试」重新探测、点「取消」以非 0 退出;
|
||
; 静默安装没人点框,直接非 0 退出。两种都不往下装。
|
||
cra_ask:
|
||
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R1
|
||
${If} $R1 != 0
|
||
Goto cra_server
|
||
${EndIf}
|
||
${IfNot} ${Silent}
|
||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_TOPMOST "$(appCannotBeClosed)" IDRETRY cra_ask
|
||
${EndIf}
|
||
DetailPrint "$(appCannotBeClosed)"
|
||
SetErrorLevel 0x666667
|
||
Quit
|
||
${EndIf}
|
||
|
||
cra_server:
|
||
${nsProcess::FindProcess} "scoreboard.exe" $R1
|
||
${If} $R1 == 0
|
||
DetailPrint "$(appClosing)"
|
||
${nsProcess::KillProcess} "scoreboard.exe" $0
|
||
StrCpy $R2 0
|
||
cra_srv:
|
||
Sleep 500
|
||
${nsProcess::FindProcess} "scoreboard.exe" $R1
|
||
${If} $R1 != 0
|
||
Goto cra_done
|
||
${EndIf}
|
||
IntOp $R2 $R2 + 1
|
||
${If} $R2 < 12
|
||
Goto cra_srv
|
||
${EndIf}
|
||
cra_srv_ask:
|
||
${nsProcess::FindProcess} "scoreboard.exe" $R1
|
||
${If} $R1 != 0
|
||
Goto cra_done
|
||
${EndIf}
|
||
${IfNot} ${Silent}
|
||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_TOPMOST "$(appCannotBeClosed)" IDRETRY cra_srv_ask
|
||
${EndIf}
|
||
DetailPrint "$(appCannotBeClosed)"
|
||
SetErrorLevel 0x666668
|
||
Quit
|
||
${EndIf}
|
||
|
||
cra_done:
|
||
${nsProcess::Unload}
|
||
Pop $R2
|
||
Pop $R1
|
||
Pop $0
|
||
!macroend
|
||
|
||
; ── 卸载段 ─────────────────────────────────────────────────────────────────
|
||
; 卸载逻辑与 un.onInit 直接用 electron-builder 的模板(uninstaller.nsh),一个字节都不改:
|
||
; 升级、快捷方式回收、防火墙规则回收的行为与上游完全一致。
|
||
; 卸载器的界面仍是原生外观 —— 素材只有安装器那一套、5 张页面位图全带「安装」字样,
|
||
; 硬套上去只会更难看。
|
||
;
|
||
; ⚠️ 为什么这里要自己给 BUILD_UNINSTALLER:
|
||
; electron-builder 平时编两趟(安装器一趟、卸载器一趟),靠 BUILD_UNINSTALLER 这个
|
||
; define 区分两边的代码路径(典型的是 getProcessInfo.nsh:安装器定义 _GetProcessInfo、
|
||
; 卸载器定义 un._GetProcessInfo)。自定义脚本只有一趟编译,安装器和卸载器的代码同处
|
||
; 一个文件里,所以只能给「卸载器这一段」自己划一个作用域:
|
||
; · 定义 BUILD_UNINSTALLER → 让 un. 侧拿到 Call un._GetProcessInfo(否则卸载器里的
|
||
; Call 不许指向非 un. 函数,直接编译不过);
|
||
; · include 完之后 !undef,安装器侧的代码照旧走 _GetProcessInfo。
|
||
; 顺带这也让 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
|
||
|
||
!define BUILD_UNINSTALLER
|
||
!include "uninstaller.nsh"
|
||
|
||
; getProcessInfo.nsh 的 un._GetProcessInfo 只在 BUILD_UNINSTALLER 那一趟里定义;
|
||
; 我们的单趟编译里补一个,宏体用的还是上游那份 FUNC_GETPROCESSINFO。
|
||
Function un._GetProcessInfo
|
||
!insertmacro FUNC_GETPROCESSINFO
|
||
FunctionEnd
|
||
|
||
Function un.CheckAppRunningImpl
|
||
!insertmacro CloseRunningApp
|
||
FunctionEnd
|
||
|
||
; 卸载器侧的提权判断:与安装器侧 CheckAdmin 逐行同一套(ReturnLength 那个真机坑见上面的长注释),
|
||
; 只是名字必须带 un. 前缀 —— 卸载器里的 Call 只能指向 un. 开头的函数。
|
||
Function un.CheckAdmin
|
||
Push $0
|
||
Push $R1
|
||
Push $R2
|
||
Push $R3
|
||
Push $R4
|
||
Push $R5
|
||
Push $R6
|
||
Push $R7
|
||
Push $R8
|
||
Push $R9
|
||
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
|
||
System::Call 'kernel32::GetLastError() i .R8'
|
||
StrCpy $gAdminDbg "OpenProcessToken=$R3 err=$R8"
|
||
${If} $R3 != 0
|
||
System::Call '*$R2(p .R4)'
|
||
System::Call '*(i 0, i 0) p .R5' ; TOKEN_ELEVATION
|
||
System::Call '*(i 0) p .R6' ; ReturnLength:真机上传 NULL 会被拒(ERROR_INVALID_PARAMETER)
|
||
System::Call 'advapi32::GetTokenInformation(p R4, i 20, p R5, i 4, p R6) i .R7'
|
||
System::Call 'kernel32::GetLastError() i .R8'
|
||
System::Call '*$R5(i .R9)'
|
||
StrCpy $gAdminDbg "$gAdminDbg | TokenElevation rc=$R7 err=$R8 val=$R9"
|
||
${If} $R7 != 0
|
||
${If} $R9 != 0
|
||
StrCpy $gIsAdmin 1
|
||
${EndIf}
|
||
${EndIf}
|
||
System::Call 'kernel32::CloseHandle(p R4)'
|
||
System::Free $R5
|
||
System::Free $R6
|
||
${EndIf}
|
||
System::Free $R2
|
||
; 判定为「非提权」时把原始值落到 %TEMP%,真机上判定再出问题时能直接看出是哪一步不对
|
||
${If} $gIsAdmin == 0
|
||
FileOpen $R9 "$TEMP\Scoreboard-Console-elevation.txt" w
|
||
FileWrite $R9 "not elevated: $gAdminDbg$\r$\n"
|
||
FileClose $R9
|
||
${EndIf}
|
||
Pop $R9
|
||
Pop $R8
|
||
Pop $R7
|
||
Pop $R6
|
||
Pop $R5
|
||
Pop $R4
|
||
Pop $R3
|
||
Pop $R2
|
||
Pop $R1
|
||
Pop $0
|
||
FunctionEnd
|
||
!undef BUILD_UNINSTALLER
|
||
|
||
!insertmacro addLangs
|
||
|
||
; ── 通用函数 ───────────────────────────────────────────────────────────────
|
||
|
||
Function CheckAppRunningImpl
|
||
!insertmacro CloseRunningApp
|
||
FunctionEnd
|
||
|
||
; 按 DPI 缩放基准值。栈:基准像素值 → 返回缩放后的像素值。
|
||
; 只用 $R8/$R9:所有调用方都用 $R0..$R7 放自己的临时值,S 不能踩它们。
|
||
Function S
|
||
Exch $R9
|
||
Push $R8
|
||
IntOp $R8 $R9 * $gDpi
|
||
IntOp $R8 $R8 / 96
|
||
StrCpy $R9 $R8
|
||
Pop $R8
|
||
Exch $R9
|
||
FunctionEnd
|
||
|
||
; 贴位图。栈:句柄 文件名 消息 基准宽 基准高。
|
||
; 文件名只有名字(如 base.bmp),目录由 $gArt 决定;cx/cy 用控件的目标像素尺寸,
|
||
; 于是 2x 素材在 150% 这类非整数倍下也能由系统缩到刚好。
|
||
Function SetBmp
|
||
Pop $R4 ; 文件名
|
||
Pop $R3 ; 句柄
|
||
Pop $R2 ; 消息
|
||
Pop $R1 ; 基准宽
|
||
Pop $R0 ; 基准高
|
||
StrCpy $tmpPath "$gArt\$R4"
|
||
Push $R1
|
||
Call S
|
||
Pop $R5 ; 目标宽
|
||
Push $R0
|
||
Call S
|
||
Pop $R6 ; 目标高
|
||
System::Call 'user32::LoadImageW(p 0, w "$tmpPath", i 0, i $R5, i $R6, i 0x0010) p .R0'
|
||
${If} $R0 == 0
|
||
Return ; 素材没加载出来就保持原图,宁可画面旧一点也不画成空白
|
||
${EndIf}
|
||
System::Call 'user32::SendMessageW(p $R3, i $R2, p 0, p $R0)'
|
||
; 立即重画:安装段执行期间消息泵不一定马上转到,进度不能等到空闲才更新
|
||
System::Call 'user32::InvalidateRect(p $R3, p 0, i 1)'
|
||
System::Call 'user32::UpdateWindow(p $R3)'
|
||
FunctionEnd
|
||
|
||
; 建控件。栈顺序见宏;类名/样式是编译期文本,坐标是 1x 设计值。
|
||
!macro NewCtl CLASS STYLE BX BY BW BH OUT
|
||
Push ${BX}
|
||
Call S
|
||
Pop $R0
|
||
Push ${BY}
|
||
Call S
|
||
Pop $R1
|
||
Push ${BW}
|
||
Call S
|
||
Pop $R2
|
||
Push ${BH}
|
||
Call S
|
||
Pop $R3
|
||
nsDialogs::CreateControl "${CLASS}" "${STYLE}" 0 $R0 $R1 $R2 $R3 ""
|
||
Pop ${OUT}
|
||
!macroend
|
||
|
||
; 贴位图(参数按 1x 设计值写,内部转成像素)
|
||
!macro SetBmpBase HWND ONAME W H
|
||
Push ${H}
|
||
Push ${W}
|
||
Push ${BM_SETIMAGE}
|
||
Push ${HWND}
|
||
Push ${ONAME}
|
||
Call SetBmp
|
||
!macroend
|
||
|
||
!macro SetBmpStatic HWND ONAME W H
|
||
Push ${H}
|
||
Push ${W}
|
||
Push ${STM_SETIMAGE}
|
||
Push ${HWND}
|
||
Push ${ONAME}
|
||
Call SetBmp
|
||
!macroend
|
||
|
||
; 位图按钮:创建 + 贴常态图
|
||
!macro NewBmpBtn BX BY BW BH OUT ONAME
|
||
!insertmacro NewCtl "Button" "${BS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${BX} ${BY} ${BW} ${BH} ${OUT}
|
||
!insertmacro SetBmpBase ${OUT} ${ONAME} ${BW} ${BH}
|
||
!macroend
|
||
|
||
; 悬停跟踪:栈里带齐「基准矩形 + 句柄 + 当前状态 + 位图基名」。
|
||
; 状态机 0=常态 1=悬停 2=按下,和 art/ 的三态位图一一对应。
|
||
!macro Track HWND STATE ONAME BX BY BW BH
|
||
StrCpy $gBw ${BW}
|
||
StrCpy $gBh ${BH}
|
||
Push ${BX}
|
||
Push ${BY}
|
||
Push ${BW}
|
||
Push ${BH}
|
||
Push ${HWND}
|
||
Push ${STATE}
|
||
Push "${ONAME}"
|
||
Call HoverBtn
|
||
Pop ${STATE}
|
||
!macroend
|
||
|
||
; 光标位置换算 + 命中测试 → 需要时换位图。栈:基准x 基准y 基准宽 基准高 句柄 状态 位图基名(base)。
|
||
; 用 $R0..$R6 干活、$R7 存结果 —— SetBmp 只踩 $R0..$R6,所以 $R7 能活到返回。
|
||
Function HoverBtn
|
||
Pop $R0 ; 位图基名
|
||
Pop $R1 ; 当前状态
|
||
Pop $R2 ; 句柄
|
||
Pop $R3 ; 基准高
|
||
Pop $R4 ; 基准宽
|
||
Pop $R5 ; 基准 y
|
||
Pop $R6 ; 基准 x
|
||
Push $R6
|
||
Call S
|
||
Pop $R6 ; x
|
||
Push $R5
|
||
Call S
|
||
Pop $R5 ; y
|
||
Push $R4
|
||
Call S
|
||
Pop $R4 ; w
|
||
Push $R3
|
||
Call S
|
||
Pop $R3 ; h
|
||
; 右/下边界(命中测试用半开区间)
|
||
IntOp $R4 $R4 + $R6
|
||
IntOp $R3 $R3 + $R5
|
||
StrCpy $R7 0
|
||
${If} $gCx >= $R6
|
||
${AndIf} $gCx < $R4
|
||
${AndIf} $gCy >= $R5
|
||
${AndIf} $gCy < $R3
|
||
StrCpy $R7 1
|
||
System::Call 'user32::GetAsyncKeyState(i 1) i .R8'
|
||
IntOp $R8 $R8 & 0x8000
|
||
${If} $R8 != 0
|
||
StrCpy $R7 2
|
||
${EndIf}
|
||
${EndIf}
|
||
${If} $R7 != $R1
|
||
${If} $R7 == 0
|
||
StrCpy $tmpName "$R0-normal.bmp"
|
||
${ElseIf} $R7 == 1
|
||
StrCpy $tmpName "$R0-hover.bmp"
|
||
${Else}
|
||
StrCpy $tmpName "$R0-pressed.bmp"
|
||
${EndIf}
|
||
Push $gBh
|
||
Push $gBw
|
||
Push ${BM_SETIMAGE}
|
||
Push $R2
|
||
Push $tmpName
|
||
Call SetBmp
|
||
${EndIf}
|
||
Push $R7
|
||
FunctionEnd
|
||
|
||
; 起拖:记下锚点(光标屏幕坐标 + 窗口左上角)
|
||
Function DragStart
|
||
Pop $R0
|
||
StrCpy $dragging 1
|
||
System::Call '*(i, i) p .R1'
|
||
System::Call 'user32::GetCursorPos(p R1)'
|
||
System::Call '*$R1(i .R2, i .R3)'
|
||
System::Free $R1
|
||
StrCpy $dragCx $R2
|
||
StrCpy $dragCy $R3
|
||
System::Call '*(i, i, i, i) p .R4'
|
||
System::Call 'user32::GetWindowRect(p $HWNDPARENT, p R4)'
|
||
System::Call '*$R4(i .R5, i .R6)'
|
||
System::Free $R4
|
||
StrCpy $dragWx $R5
|
||
StrCpy $dragWy $R6
|
||
FunctionEnd
|
||
|
||
; 拖动中:窗口位置 = 锚点窗口位置 + 光标位移
|
||
Function DragApply
|
||
Pop $R0
|
||
System::Call '*(i, i) p .R1'
|
||
System::Call 'user32::GetCursorPos(p R1)'
|
||
System::Call '*$R1(i .R2, i .R3)'
|
||
System::Free $R1
|
||
IntOp $R2 $R2 - $dragCx
|
||
IntOp $R3 $R3 - $dragCy
|
||
IntOp $R2 $R2 + $dragWx
|
||
IntOp $R3 $R3 + $dragWy
|
||
System::Call 'user32::SetWindowPos(p $HWNDPARENT, p 0, i $R2, i $R3, i 0, i 0, i 0x0015)'
|
||
FunctionEnd
|
||
|
||
; 定时器回调:光标 → 客户区坐标 → 逐控件做悬停/按下 → 标题栏拖动。
|
||
; 不做窗口子类化(POC 结论):60ms 轮询足够跟手,也没有子类化回调崩掉整个安装器的风险。
|
||
Function Tick
|
||
System::Call '*(i, i) p .R0'
|
||
System::Call 'user32::GetCursorPos(p R0)'
|
||
System::Call 'user32::ScreenToClient(p $HWNDPARENT, p R0)'
|
||
System::Call '*$R0(i .R1, i .R2)'
|
||
System::Free $R0
|
||
StrCpy $gCx $R1
|
||
StrCpy $gCy $R2
|
||
|
||
${If} $uiPage == 0
|
||
Return
|
||
${EndIf}
|
||
|
||
; 标题栏两个按钮每页都在
|
||
!insertmacro Track $hMin $stMin "tb-min" ${TB_MIN_X} 0 ${TB_BTN_W} ${TB_H}
|
||
!insertmacro Track $hClose $stClose "tb-close" ${TB_CLOSE_X} 0 ${TB_BTN_W} ${TB_H}
|
||
|
||
${If} $uiPage == 1
|
||
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
${ElseIf} $uiPage == 2
|
||
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
${ElseIf} $uiPage == 3
|
||
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hBack $stBack "btn-back" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hBrowse $stBrowse "btn-browse" ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H}
|
||
${ElseIf} $uiPage == 4
|
||
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
; 安装在这里开工:等第一个 tick(窗口已经画完)再动手,然后完工后翻到 P5。
|
||
; 为什么不用 NSIS 自带的 instfiles 页:那一页在跑安装时只泵它自己的消息,
|
||
; 我们挂在 StdUtils 定时器上的悬停/进度刷新全都收不到回调(实测:P4 上按钮不悬停、
|
||
; 定时器不触发)。改成自绘页 + 定时器驱动,消息泵一直是通的。
|
||
${If} $instStarted == 0
|
||
Call DoInstall
|
||
${ElseIf} $instDone == 1
|
||
; 安装已经跑完,但页面还停在「正在安装」(真机复验:卡在最后一段不翻页)。
|
||
; 翻页靠点被藏起来的「下一步」,这一下有可能没生效,所以每个 tick 重试一次,
|
||
; 直到页面真的不再是这一页 —— 翻页成功后 $uiPage 立刻变了,不会多点到下一页去。
|
||
; ⚠️ 必须等 $instDone:安装途中的 Sleep 会把消息泵转起来,那时候也会跑进 Tick,
|
||
; 没有这道门就会在装到一半时去点「下一步」。
|
||
GetDlgItem $0 $HWNDPARENT 1
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
${EndIf}
|
||
${ElseIf} $uiPage == 5
|
||
!insertmacro Track $hFinish $stFinish "btn-finish" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
${EndIf}
|
||
|
||
; 标题栏拖动:按下时记锚点(光标屏幕坐标 + 窗口左上角),之后按位移 SetWindowPos。
|
||
; 不用系统的 WM_NCLBUTTONDOWN / HTCAPTION 移动循环 —— 它把移动交给 DefWindowProc,
|
||
; 真机上(窗口已经没有 WS_CAPTION 了)实测拖不动;自己算不依赖系统行为,wine 与真机一致。
|
||
; 两点关键:
|
||
; 1. 命中判定只管「起拖」;已经在拖的时候只要左键还按着就继续跟 —— 光标一旦甩出标题栏
|
||
; (窗口还没跟上来的那一瞬间)就停,正是「拖一下就断」的经典写法错误;
|
||
; 2. 起拖区里要扣掉标题栏按钮(x >= TB_MIN_X),否则点最小化/关闭会连带开始拖窗。
|
||
System::Call 'user32::GetAsyncKeyState(i 1) i .R3'
|
||
IntOp $R3 $R3 & 0x8000
|
||
${If} $dragging == 1
|
||
${If} $R3 != 0
|
||
Push true
|
||
Call DragApply
|
||
${Else}
|
||
StrCpy $dragging 0
|
||
${EndIf}
|
||
${Else}
|
||
${If} $gCy >= 0
|
||
${AndIf} $gCy < ${TB_H}
|
||
${AndIf} $gCx >= 0
|
||
${AndIf} $gCx < ${TB_MIN_X}
|
||
${AndIf} $R3 != 0
|
||
Push true
|
||
Call DragStart
|
||
${EndIf}
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; ── 素材嵌入 ───────────────────────────────────────────────────────────────
|
||
; 逐个文件写 File 而不是 File /r:素材缺一张就在编译期报错,
|
||
; 而不是运行期画出一块空白(装机现场没有回头补素材的机会)。
|
||
!macro ArtFiles SUB
|
||
SetOutPath "$PLUGINSDIR\${SUB}"
|
||
File /oname=base.bmp "${ART_SRC}\${SUB}\base.bmp"
|
||
File /oname=brand.bmp "${ART_SRC}\${SUB}\brand.bmp"
|
||
File /oname=titlebar.bmp "${ART_SRC}\${SUB}\titlebar.bmp"
|
||
File /oname=page-welcome.bmp "${ART_SRC}\${SUB}\page-welcome.bmp"
|
||
File /oname=page-license.bmp "${ART_SRC}\${SUB}\page-license.bmp"
|
||
File /oname=page-dir.bmp "${ART_SRC}\${SUB}\page-dir.bmp"
|
||
File /oname=page-progress.bmp "${ART_SRC}\${SUB}\page-progress.bmp"
|
||
File /oname=page-finish.bmp "${ART_SRC}\${SUB}\page-finish.bmp"
|
||
File /oname=btn-next-normal.bmp "${ART_SRC}\${SUB}\btn-next-normal.bmp"
|
||
File /oname=btn-next-hover.bmp "${ART_SRC}\${SUB}\btn-next-hover.bmp"
|
||
File /oname=btn-next-pressed.bmp "${ART_SRC}\${SUB}\btn-next-pressed.bmp"
|
||
File /oname=btn-install-normal.bmp "${ART_SRC}\${SUB}\btn-install-normal.bmp"
|
||
File /oname=btn-install-hover.bmp "${ART_SRC}\${SUB}\btn-install-hover.bmp"
|
||
File /oname=btn-install-pressed.bmp "${ART_SRC}\${SUB}\btn-install-pressed.bmp"
|
||
File /oname=btn-finish-normal.bmp "${ART_SRC}\${SUB}\btn-finish-normal.bmp"
|
||
File /oname=btn-finish-hover.bmp "${ART_SRC}\${SUB}\btn-finish-hover.bmp"
|
||
File /oname=btn-finish-pressed.bmp "${ART_SRC}\${SUB}\btn-finish-pressed.bmp"
|
||
File /oname=btn-cancel-normal.bmp "${ART_SRC}\${SUB}\btn-cancel-normal.bmp"
|
||
File /oname=btn-cancel-hover.bmp "${ART_SRC}\${SUB}\btn-cancel-hover.bmp"
|
||
File /oname=btn-cancel-pressed.bmp "${ART_SRC}\${SUB}\btn-cancel-pressed.bmp"
|
||
File /oname=btn-back-normal.bmp "${ART_SRC}\${SUB}\btn-back-normal.bmp"
|
||
File /oname=btn-back-hover.bmp "${ART_SRC}\${SUB}\btn-back-hover.bmp"
|
||
File /oname=btn-back-pressed.bmp "${ART_SRC}\${SUB}\btn-back-pressed.bmp"
|
||
File /oname=btn-browse-normal.bmp "${ART_SRC}\${SUB}\btn-browse-normal.bmp"
|
||
File /oname=btn-browse-hover.bmp "${ART_SRC}\${SUB}\btn-browse-hover.bmp"
|
||
File /oname=btn-browse-pressed.bmp "${ART_SRC}\${SUB}\btn-browse-pressed.bmp"
|
||
File /oname=tb-min-normal.bmp "${ART_SRC}\${SUB}\tb-min-normal.bmp"
|
||
File /oname=tb-min-hover.bmp "${ART_SRC}\${SUB}\tb-min-hover.bmp"
|
||
File /oname=tb-min-pressed.bmp "${ART_SRC}\${SUB}\tb-min-pressed.bmp"
|
||
File /oname=tb-close-normal.bmp "${ART_SRC}\${SUB}\tb-close-normal.bmp"
|
||
File /oname=tb-close-hover.bmp "${ART_SRC}\${SUB}\tb-close-hover.bmp"
|
||
File /oname=tb-close-pressed.bmp "${ART_SRC}\${SUB}\tb-close-pressed.bmp"
|
||
File /oname=check-off.bmp "${ART_SRC}\${SUB}\check-off.bmp"
|
||
File /oname=check-on.bmp "${ART_SRC}\${SUB}\check-on.bmp"
|
||
File /oname=radio-off.bmp "${ART_SRC}\${SUB}\radio-off.bmp"
|
||
File /oname=radio-on.bmp "${ART_SRC}\${SUB}\radio-on.bmp"
|
||
File /oname=progress-0.bmp "${ART_SRC}\${SUB}\progress-0.bmp"
|
||
File /oname=progress-1.bmp "${ART_SRC}\${SUB}\progress-1.bmp"
|
||
File /oname=progress-2.bmp "${ART_SRC}\${SUB}\progress-2.bmp"
|
||
File /oname=progress-3.bmp "${ART_SRC}\${SUB}\progress-3.bmp"
|
||
File /oname=progress-4.bmp "${ART_SRC}\${SUB}\progress-4.bmp"
|
||
File /oname=progress-5.bmp "${ART_SRC}\${SUB}\progress-5.bmp"
|
||
File /oname=stage-1.bmp "${ART_SRC}\${SUB}\stage-1.bmp"
|
||
File /oname=stage-2.bmp "${ART_SRC}\${SUB}\stage-2.bmp"
|
||
File /oname=stage-3.bmp "${ART_SRC}\${SUB}\stage-3.bmp"
|
||
File /oname=stage-4.bmp "${ART_SRC}\${SUB}\stage-4.bmp"
|
||
File /oname=stage-5.bmp "${ART_SRC}\${SUB}\stage-5.bmp"
|
||
!macroend
|
||
|
||
; 只解当前 DPI 需要的那一套:两套都解要写 26MB 临时文件,首次显示会明显卡一下。
|
||
Function EnsureArt
|
||
${If} $artReady == 1
|
||
Return
|
||
${EndIf}
|
||
StrCpy $artReady 1
|
||
InitPluginsDir
|
||
${If} $gDpi >= 144
|
||
StrCpy $gArt "$PLUGINSDIR\2x"
|
||
!insertmacro ArtFiles 2x
|
||
${Else}
|
||
StrCpy $gArt "$PLUGINSDIR\1x"
|
||
!insertmacro ArtFiles 1x
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; 系统 DPI。ManifestDPIAware 之后进程不会被人为放大,只能自己按这个数缩放版面。
|
||
Function PickDpi
|
||
System::Call 'user32::GetDC(p 0) p .R0'
|
||
System::Call 'gdi32::GetDeviceCaps(p R0, i 88) i .R1' ; LOGPIXELSX
|
||
System::Call 'user32::ReleaseDC(p 0, p R0)'
|
||
${If} $R1 < 96
|
||
StrCpy $R1 96
|
||
${EndIf}
|
||
StrCpy $gDpi $R1
|
||
FunctionEnd
|
||
|
||
; 藏一个外层对话框上的原生控件(控件不存在就跳过,$R7 是本宏专用的临时变量)
|
||
!macro HideStd ID
|
||
GetDlgItem $R7 $HWNDPARENT ${ID}
|
||
${If} $R7 != 0
|
||
ShowWindow $R7 ${SW_HIDE}
|
||
${EndIf}
|
||
!macroend
|
||
|
||
; 内层对话框铺满整个客户区:MUI 外层框架(页眉 / 品牌字 / 原生按钮条)全被盖住,
|
||
; 自绘位图才有完整的 760×480 画布。
|
||
Function NewPage
|
||
nsDialogs::Create 1018
|
||
Pop $hPage
|
||
${If} $hPage == error
|
||
Abort
|
||
${EndIf}
|
||
; 悬停/拖动/阶段刷新全靠这个 tick 轮询(POC 结论:比窗口子类化稳,代价是光标移动要等一个 tick)。
|
||
; ⚠️ 定时器用 nsDialogs 自带的 ${NSD_CreateTimer},**不要**用 StdUtils::TimerCreate:
|
||
; 真机实测(构建机 aeiva)StdUtils 那个不管请求 4ms / 20ms / 60ms / 250ms,
|
||
; 一律按 ~1500ms 的心跳跑(几个定时器还完全同步),wine 里也一样 ——
|
||
; 后果就是「拖不动窗口」(人手一次拖动不到 1.5 秒,tick 根本采不到按下状态)、
|
||
; 悬停要一秒多才反应。nsDialogs 的定时器实测就是请求的 60ms。
|
||
; 它挂在**当前页的内层对话框**上(页没了定时器也没了),所以每页(=每次 NewPage)都要建一次。
|
||
${NSD_CreateTimer} Tick 60
|
||
StrCpy $timerOn 1
|
||
Push ${WIN_W}
|
||
Call S
|
||
Pop $R0
|
||
Push ${WIN_H}
|
||
Call S
|
||
Pop $R1
|
||
System::Call 'user32::SetWindowPos(p $hPage, p 0, i 0, i 0, i $R0, i $R1, i 0x0004)'
|
||
; 把外层对话框上的原生控件**显式隐藏**。内层对话框只是盖在它们上面,鼠标划过那块区域时
|
||
; 原生按钮会自己重绘、从自绘层底下透出来(真机实测:Next/Back/Cancel 会闪出来)。
|
||
; NSIS 每页都重建内层对话框,所以每页(=每次 NewPage)都要重新藏一遍。
|
||
; ⚠️ 藏掉之后翻页仍然靠 SendMessage BM_CLICK 点那个被藏起来的 Next —— 隐藏窗口照样收得到
|
||
; 消息,本机实测翻页正常(这条一旦不成立,整套翻页就断了,所以改了要立刻验证)。
|
||
!insertmacro HideStd 1 ; Next
|
||
!insertmacro HideStd 2 ; Cancel
|
||
!insertmacro HideStd 3 ; Back
|
||
!insertmacro HideStd 1028 ; 底部品牌字底
|
||
!insertmacro HideStd 1256 ; 底部品牌字
|
||
!insertmacro HideStd 1034 ; 页眉底
|
||
!insertmacro HideStd 1035 ; 分隔线(标准)
|
||
!insertmacro HideStd 1045 ; 分隔线(满窗)
|
||
!insertmacro HideStd 1037 ; 页眉标题
|
||
!insertmacro HideStd 1038 ; 页眉副标题
|
||
!insertmacro HideStd 1046 ; 页眉图标
|
||
; 上一页的句柄全部作废:定时器靠这组变量判断「还能碰谁」
|
||
StrCpy $hBg 0
|
||
StrCpy $hBrand 0
|
||
StrCpy $hTitleBar 0
|
||
StrCpy $hMin 0
|
||
StrCpy $hClose 0
|
||
StrCpy $hNext 0
|
||
StrCpy $hBack 0
|
||
StrCpy $hCancel 0
|
||
StrCpy $hBrowse 0
|
||
StrCpy $hFinish 0
|
||
StrCpy $hCheck 0
|
||
StrCpy $hStage 0
|
||
StrCpy $hProg 0
|
||
StrCpy $stMin 0
|
||
StrCpy $stClose 0
|
||
StrCpy $stNext 0
|
||
StrCpy $stBack 0
|
||
StrCpy $stCancel 0
|
||
StrCpy $stBrowse 0
|
||
StrCpy $stFinish 0
|
||
FunctionEnd
|
||
|
||
; 全窗共用的底:背景 → 品牌栏 → 页面位图 → 标题栏 → 标题栏按钮。
|
||
; 栈:页面位图名(空字符串 = 该页不贴页面位图,比如安装中页自己控制进度)
|
||
Function Chrome
|
||
Pop $tmpName
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 0 ${WIN_W} ${WIN_H} $hBg
|
||
!insertmacro SetBmpStatic $hBg "base.bmp" ${WIN_W} ${WIN_H}
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 ${TB_H} ${SIDE_W} ${BRAND_H} $hBrand
|
||
!insertmacro SetBmpStatic $hBrand "brand.bmp" ${SIDE_W} ${BRAND_H}
|
||
${If} $tmpName != ""
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${PG_X} ${PG_Y} ${PG_W} ${PG_H} $hProg
|
||
!insertmacro SetBmpStatic $hProg "$tmpName" ${PG_W} ${PG_H}
|
||
${EndIf}
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 0 ${WIN_W} ${TB_H} $hTitleBar
|
||
!insertmacro SetBmpStatic $hTitleBar "titlebar.bmp" ${WIN_W} ${TB_H}
|
||
!insertmacro NewBmpBtn ${TB_MIN_X} 0 ${TB_BTN_W} ${TB_H} $hMin "tb-min-normal.bmp"
|
||
!insertmacro NewBmpBtn ${TB_CLOSE_X} 0 ${TB_BTN_W} ${TB_H} $hClose "tb-close-normal.bmp"
|
||
${NSD_OnClick} $hMin OnMin
|
||
${NSD_OnClick} $hClose OnClose
|
||
FunctionEnd
|
||
|
||
; 自绘文字标签:对话框默认底色是灰的,不显式设色就会在页面上贴出一块灰,
|
||
; 所以一律用「底色 = 内容区底色」把标签融进位图。
|
||
; 栈(后压先弹):文本 字号 基准高 基准宽 基准y 基准x
|
||
!macro AddLabel BX BY BW BH SIZE TEXT
|
||
Push ${BX}
|
||
Push ${BY}
|
||
Push ${BW}
|
||
Push ${BH}
|
||
Push ${SIZE}
|
||
Push "${TEXT}"
|
||
Call Label
|
||
!macroend
|
||
|
||
Function Label
|
||
Pop $R0
|
||
Pop $R1
|
||
Pop $R2
|
||
Pop $R3
|
||
Pop $R4
|
||
Pop $R5
|
||
Push $R5
|
||
Call S
|
||
Pop $R5
|
||
Push $R4
|
||
Call S
|
||
Pop $R4
|
||
Push $R3
|
||
Call S
|
||
Pop $R3
|
||
Push $R2
|
||
Call S
|
||
Pop $R2
|
||
nsDialogs::CreateControl "Static" "${SS_LEFT}|${SS_CENTERIMAGE}|${WS_CHILD}|${WS_VISIBLE}" 0 $R5 $R4 $R3 $R2 ""
|
||
Pop $R6
|
||
SetCtlColors $R6 "${CLR_TXT}" "${CLR_BG}"
|
||
CreateFont $R7 "Microsoft YaHei UI" "$R1" "400"
|
||
SendMessage $R6 ${WM_SETFONT} $R7 1
|
||
${NSD_SetText} $R6 "$R0"
|
||
FunctionEnd
|
||
|
||
; 栈(后压先弹):样式 基准高 基准宽 基准y 基准x → 返回控件句柄
|
||
!macro NewEdit STYLE BX BY BW BH OUT
|
||
Push ${BH}
|
||
Push ${BW}
|
||
Push ${BY}
|
||
Push ${BX}
|
||
Push "${STYLE}"
|
||
Call MakeEdit
|
||
Pop ${OUT}
|
||
!macroend
|
||
|
||
Function MakeEdit
|
||
Pop $R5
|
||
Pop $R0
|
||
Pop $R1
|
||
Pop $R2
|
||
Pop $R3
|
||
Push $R0
|
||
Call S
|
||
Pop $R0
|
||
Push $R1
|
||
Call S
|
||
Pop $R1
|
||
Push $R2
|
||
Call S
|
||
Pop $R2
|
||
Push $R3
|
||
Call S
|
||
Pop $R3
|
||
nsDialogs::CreateControl "Edit" "$R5" 0 $R0 $R1 $R2 $R3 ""
|
||
Pop $R6
|
||
SetCtlColors $R6 "${CLR_TXT}" "0xffffff"
|
||
CreateFont $R7 "Microsoft YaHei UI" "12" "400"
|
||
SendMessage $R6 ${WM_SETFONT} $R7 1
|
||
Push $R6
|
||
FunctionEnd
|
||
|
||
; ── 按钮动作 ───────────────────────────────────────────────────────────────
|
||
; 标准按钮条被自绘层盖住了,但按钮还在:点它等于点系统按钮,翻页/取消都由 NSIS 自己走。
|
||
Function GoNext
|
||
GetDlgItem $0 $HWNDPARENT 1
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function GoBack
|
||
GetDlgItem $0 $HWNDPARENT 3
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function GoCancel
|
||
GetDlgItem $0 $HWNDPARENT 2
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function OnMin
|
||
; 最小化走系统命令:自绘窗口没有系统按钮,但 ShowWindow 一样有效
|
||
System::Call 'user32::ShowWindow(p $HWNDPARENT, i 6)' ; SW_MINIMIZE
|
||
FunctionEnd
|
||
|
||
Function OnClose
|
||
Call GoCancel
|
||
FunctionEnd
|
||
|
||
Function OnLicCheck
|
||
; 用 != 1 而不是 == 0:LogicLib 的 == 是字符串比较,$licOk 初值是空串,"" == 0 为假
|
||
${If} $licOk != 1
|
||
StrCpy $licOk 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${Else}
|
||
StrCpy $licOk 0
|
||
!insertmacro SetBmpBase $hCheck "check-off.bmp" 16 16
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
Function OnLicNext
|
||
${If} $licOk != 1
|
||
; 没同意就不让走:这是许可页唯一的硬性要求
|
||
MessageBox MB_OK|MB_ICONINFORMATION|MB_TOPMOST "请先勾选「我已阅读并同意」再继续。"
|
||
Return
|
||
${EndIf}
|
||
Call GoNext
|
||
FunctionEnd
|
||
|
||
Function OnModeUser
|
||
StrCpy $modeAll 0
|
||
!insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16
|
||
!insertmacro SetBmpBase $hRadio2 "radio-off.bmp" 16 16
|
||
FunctionEnd
|
||
|
||
Function OnModeAll
|
||
StrCpy $modeAll 1
|
||
!insertmacro SetBmpBase $hRadio1 "radio-off.bmp" 16 16
|
||
!insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16
|
||
FunctionEnd
|
||
|
||
; 「浏览…」:系统文件夹选择框没有 NSIS 包装,用 System::Call 直接调 shell32。
|
||
; BROWSEINFO = { hwndOwner, pidlRoot, pszDisplayName, lpszTitle, ulFlags, lpfn, lParam, iImage }
|
||
; ulFlags = BIF_RETURNONLYFSDIRS(0x1) | BIF_NEWDIALOGSTYLE(0x40)
|
||
Function OnBrowse
|
||
System::Call 'ole32::CoInitialize(p 0)'
|
||
StrCpy $R6 $HWNDPARENT
|
||
System::Alloc 260
|
||
Pop $R7
|
||
System::Alloc 32
|
||
Pop $R0
|
||
System::Call '*$R0(p $R6, p 0, p $R7, p 0, i 0x41, p 0, p 0, i 0)'
|
||
System::Call 'shell32::SHBrowseForFolder(p R0) p .R1'
|
||
${If} $R1 != 0
|
||
System::Call 'shell32::SHGetPathFromIDList(p R1, t .R2)'
|
||
System::Call 'ole32::CoTaskMemFree(p R1)'
|
||
${If} $R2 != ""
|
||
StrCpy $tmpTxt $R2
|
||
${NSD_SetText} $hPath "$tmpTxt"
|
||
${EndIf}
|
||
${EndIf}
|
||
System::Free $R7
|
||
System::Free $R0
|
||
FunctionEnd
|
||
|
||
Function OnFinish
|
||
${If} $runApp == 1
|
||
Call StartAppNow
|
||
${EndIf}
|
||
; 结束安装器。这里踩过两个坑:页面回调里 Quit 不生效(窗口不退、反而被推去下一页),
|
||
; 对主窗口发 WM_CLOSE 也不生效;能真正退出的只有点系统那个「取消」按钮。
|
||
; 先把错误码归零,别让「正常装完」的退出码看起来像被取消。
|
||
SetErrorLevel 0
|
||
GetDlgItem $0 $HWNDPARENT 2
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function OnFinCheck
|
||
${If} $runApp != 1
|
||
StrCpy $runApp 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${Else}
|
||
StrCpy $runApp 0
|
||
!insertmacro SetBmpBase $hCheck "check-off.bmp" 16 16
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; ── 进度分段 ───────────────────────────────────────────────────────────────
|
||
; app-64.7z 是一次 Nsis7z::Extract 调用,拿不到百分比;所以进度按「宏观步骤」分段
|
||
; (解压 → 注册表 → 快捷方式 → 防火墙 → 收尾),不画假进度条。
|
||
; NSIS 的 Call 不收参数,用法统一走这个宏
|
||
!macro StageCall N
|
||
Push ${N}
|
||
Call Stage
|
||
!macroend
|
||
|
||
; 栈:段号 1..5
|
||
Function Stage
|
||
Pop $R0
|
||
${If} ${Silent}
|
||
Return
|
||
${EndIf}
|
||
${If} $uiPage != 4
|
||
Return
|
||
${EndIf}
|
||
; 每一段至少显示 ${STAGE_DWELL} 毫秒再换下一段。
|
||
; 真机复验原话:「进度条只显示 0% 和 100%,中间的输出会在几帧内突然跳完」——
|
||
; 因为耗时几乎全在「解压」那一段,剩下几段每段不到 1 毫秒,肉眼只能看到头和尾。
|
||
; 段本身是真做过的,这里只是保证它**看得见**(四段加起来最多多花 1.6 秒)。
|
||
System::Call 'kernel32::GetTickCount() i .R1'
|
||
IntOp $R1 $R1 - $stageT0
|
||
${If} $R1 < ${STAGE_DWELL}
|
||
IntOp $R1 ${STAGE_DWELL} - $R1
|
||
Sleep $R1
|
||
${EndIf}
|
||
System::Call 'kernel32::GetTickCount() i .R1'
|
||
StrCpy $stageT0 $R1
|
||
; 段号先搬进全局变量:SetBmp 会踩掉 $R0..$R7(它的局部值都放那儿)
|
||
StrCpy $tmpTxt $R0
|
||
StrCpy $tmpPath "stage-$tmpTxt.bmp"
|
||
Push ${STAGE_H}
|
||
Push ${STAGE_W}
|
||
Push ${STM_SETIMAGE}
|
||
Push $hStage
|
||
Push $tmpPath
|
||
Call SetBmp
|
||
StrCpy $tmpPath "progress-$tmpTxt.bmp"
|
||
Push ${PROG_H}
|
||
Push ${PROG_W}
|
||
Push ${STM_SETIMAGE}
|
||
Push $hProg
|
||
Push $tmpPath
|
||
Call SetBmp
|
||
FunctionEnd
|
||
|
||
; ── P1 欢迎 ────────────────────────────────────────────────────────────────
|
||
Function PageWelcome
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 1
|
||
Call NewPage
|
||
Push "page-welcome.bmp"
|
||
Call Chrome
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
|
||
${NSD_OnClick} $hCancel OnClose
|
||
${NSD_OnClick} $hNext GoNext
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
; ── P2 许可协议 ────────────────────────────────────────────────────────────
|
||
; 中文摘要烘焙在 page-license.bmp 里;英文原文(LICENSE)用真编辑框显示,
|
||
; 因为它比框大一截、必须能滚 —— 位图放不下整篇协议。
|
||
Function PageLicense
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 2
|
||
Call NewPage
|
||
Push "page-license.bmp"
|
||
Call Chrome
|
||
|
||
; 先按可写建,灌完文本再设只读:以只读样式创建的编辑框会被刷成灰底,
|
||
; 后期用 EM_SETREADONLY 设的只读框底色还是白的(SetCtlColors 再兜一层)。
|
||
!insertmacro NewEdit "${ES_MULTILINE}|${ES_AUTOVSCROLL}|${WS_VSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${LIC_EDIT_X} ${LIC_EDIT_Y} ${LIC_EDIT_W} ${LIC_EDIT_H} $hLicEdit
|
||
File /oname=$PLUGINSDIR\license.txt "${PROJECT_DIR}\..\LICENSE"
|
||
FileOpen $9 "$PLUGINSDIR\license.txt" r
|
||
lic_loop:
|
||
ClearErrors
|
||
FileRead $9 $8
|
||
IfErrors lic_done
|
||
SendMessage $hLicEdit ${EM_REPLACESEL} 1 "STR:$8$\r$\n"
|
||
Goto lic_loop
|
||
lic_done:
|
||
FileClose $9
|
||
; FileRead + EM_REPLACESEL 会把插入点一路推到末尾、视图跟着滚到底;
|
||
; 用户进这一页看到的应该是协议开头,所以显式回到顶部
|
||
SendMessage $hLicEdit ${EM_SETSEL} 0 0
|
||
SendMessage $hLicEdit ${WM_VSCROLL} 6 0 ; SB_TOP
|
||
SendMessage $hLicEdit ${EM_SETREADONLY} 1 0
|
||
|
||
!insertmacro NewBmpBtn 272 432 16 16 $hCheck "check-off.bmp"
|
||
${If} $licOk == 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${EndIf}
|
||
!insertmacro AddLabel 296 430 300 20 12 "我已阅读并同意上述条款"
|
||
${NSD_OnClick} $hCheck OnLicCheck
|
||
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
|
||
${NSD_OnClick} $hCancel OnClose
|
||
${NSD_OnClick} $hNext OnLicNext
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageLicenseLeave
|
||
StrCpy $uiPage 0
|
||
FunctionEnd
|
||
|
||
; ── P3 安装位置 + 安装模式 ─────────────────────────────────────────────────
|
||
Function PageDir
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 3
|
||
Call NewPage
|
||
Push "page-dir.bmp"
|
||
Call Chrome
|
||
|
||
!insertmacro NewEdit "${ES_AUTOHSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${PATH_EDIT_X} ${PATH_EDIT_Y} ${PATH_EDIT_W} ${PATH_EDIT_H} $hPath
|
||
${NSD_SetText} $hPath "$INSTDIR"
|
||
!insertmacro NewBmpBtn ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H} $hBrowse "btn-browse-normal.bmp"
|
||
${NSD_OnClick} $hBrowse OnBrowse
|
||
|
||
!insertmacro NewBmpBtn ${RADIO1_X} ${RADIO1_Y} 16 16 $hRadio1 "radio-off.bmp"
|
||
!insertmacro NewBmpBtn ${RADIO2_X} ${RADIO2_Y} 16 16 $hRadio2 "radio-off.bmp"
|
||
${NSD_OnClick} $hRadio1 OnModeUser
|
||
${NSD_OnClick} $hRadio2 OnModeAll
|
||
; 初始勾选跟随 $installMode:initMultiUser 已经按「上次装法 / 命令行 / 默认」定好了模式
|
||
${If} $installMode == "all"
|
||
StrCpy $modeAll 1
|
||
!insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16
|
||
${Else}
|
||
StrCpy $modeAll 0
|
||
!insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16
|
||
${EndIf}
|
||
|
||
; 位图上的「需要空间 约 260 MB」是设计稿的占位数字,用真实体积盖掉
|
||
!ifdef ESTIMATED_SIZE
|
||
IntOp $R0 ${ESTIMATED_SIZE} / 1024
|
||
StrCpy $tmpTxt "需要空间 约 $R0 MB"
|
||
!insertmacro AddLabel ${PG_X} ${SIZE_TXT_Y} ${CONTENT_W} 20 11 "$tmpTxt"
|
||
!endif
|
||
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hBack "btn-back-normal.bmp"
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
|
||
${NSD_OnClick} $hBack GoBack
|
||
${NSD_OnClick} $hNext GoNext
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageDirLeave
|
||
StrCpy $uiPage 0
|
||
; 手改过就以输入框为准(浏览按钮也是写回这个框)
|
||
${NSD_GetText} $hPath $R1
|
||
${If} $R1 != ""
|
||
StrCpy $INSTDIR $R1
|
||
${EndIf}
|
||
StrCpy $R2 $INSTDIR 1 -1
|
||
${If} $R2 == "\"
|
||
StrCpy $INSTDIR $INSTDIR -1
|
||
${EndIf}
|
||
; 和 electron-builder 的目录页同一套规矩:路径必须以应用目录名结尾,
|
||
; 否则用户填 D:\Games 会把整个文件树倒进 D:\Games 而不是 D:\Games\Scoreboard_Console
|
||
${StrContains} $R3 "${APP_FILENAME}" $INSTDIR
|
||
${If} $R3 == ""
|
||
StrCpy $INSTDIR "$INSTDIR\${APP_FILENAME}"
|
||
${EndIf}
|
||
|
||
; 兜底:万一路径是空的(记忆为空 + 默认值也没算出来),给一个合法的按用户目录,
|
||
; 绝不把空 $INSTDIR 带进安装流程(空路径是「点了没反应 / 装到莫名其妙的地方」的经典来源)。
|
||
${If} $INSTDIR == ""
|
||
StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${APP_FILENAME}"
|
||
${EndIf}
|
||
|
||
${If} $modeAll == 1
|
||
; 「为所有用户安装」。执行级别是 highest,所以**管理员账户的机器上启动时就提过权了**,
|
||
; 这里不需要(也不该)再走一次 UAC_RunElevated:中途提权会把窗口藏起来、让用户看见
|
||
; 「闪一下没反应」,真机上就是这么坏的(前几轮修过)。
|
||
; 受限账户的机器上 highest 拿不到管理员权限 —— 这时明确告知并降级成「仅为我安装」继续,
|
||
; 绝不停在这一页。
|
||
${If} ${UAC_IsAdmin}
|
||
!insertmacro setInstallModePerAllUsers
|
||
${Else}
|
||
MessageBox mb_IconExclamation|mb_TopMost|mb_SetForeground "当前账户没有管理员权限,无法「为所有用户安装」,已改为「仅为我安装」继续。"
|
||
StrCpy $modeAll 0
|
||
!insertmacro setInstallModePerUser
|
||
${EndIf}
|
||
${Else}
|
||
!insertmacro setInstallModePerUser
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; ── P4 正在安装 ────────────────────────────────────────────────────────────
|
||
; 自绘页:分段进度由 DoInstall 里的 Stage 调用推动(7z 一次解压拿不到百分比)。
|
||
; 不用 NSIS 自带的 instfiles 页,原因见 Tick 里的注释。
|
||
Function PageInstall
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 4
|
||
Call NewPage
|
||
Push "page-progress.bmp"
|
||
Call Chrome
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${STAGE_X} ${STAGE_Y} ${STAGE_W} ${STAGE_H} $hStage
|
||
!insertmacro SetBmpStatic $hStage "stage-1.bmp" ${STAGE_W} ${STAGE_H}
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${PROG_X} ${PROG_Y} ${PROG_W} ${PROG_H} $hProg
|
||
!insertmacro SetBmpStatic $hProg "progress-0.bmp" ${PROG_W} ${PROG_H}
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
|
||
${NSD_OnClick} $hCancel OnClose
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageInstallLeave
|
||
StrCpy $uiPage 0
|
||
FunctionEnd
|
||
|
||
; ── P5 完成 ────────────────────────────────────────────────────────────────
|
||
Function PageFinish
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 5
|
||
Call NewPage
|
||
Push "page-finish.bmp"
|
||
Call Chrome
|
||
|
||
!insertmacro NewBmpBtn ${FIN_CHK_X} ${FIN_CHK_Y} 16 16 $hCheck "check-off.bmp"
|
||
${If} $runApp == 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${EndIf}
|
||
!insertmacro AddLabel 296 336 300 20 13 "立即运行 赛事积分榜"
|
||
${NSD_OnClick} $hCheck OnFinCheck
|
||
|
||
; 台上的实际安装目录(位图里写的是设计稿的占位路径)
|
||
StrCpy $tmpTxt "安装位置:$INSTDIR"
|
||
!insertmacro AddLabel ${PG_X} ${FIN_PATH_Y} ${CONTENT_W} 18 11 "$tmpTxt"
|
||
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hFinish "btn-finish-normal.bmp"
|
||
${NSD_OnClick} $hFinish OnFinish
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageFinishLeave
|
||
StrCpy $uiPage 0
|
||
FunctionEnd
|
||
|
||
; ── 界面初始化 ─────────────────────────────────────────────────────────────
|
||
; 去系统边框 + 固定 760×480(按 DPI 缩放)+ 居中。
|
||
; MUI 的外层框架本身用不上,但它是 $HWNDPARENT、是标准按钮条的宿主,
|
||
; 所以不能不要,只能去边框、盖住、把标准按钮当程序化翻页的工具。
|
||
Function GuiInit
|
||
Call PickDpi
|
||
System::Call 'user32::GetWindowLong(p $HWNDPARENT, i -16) i .R0'
|
||
IntOp $R0 $R0 & 0xFF30FFFF ; 清 WS_CAPTION / BORDER / DLGFRAME / SYSMENU / MIN / MAX
|
||
System::Call 'user32::SetWindowLong(p $HWNDPARENT, i -16, i R0)'
|
||
Push ${WIN_W}
|
||
Call S
|
||
Pop $R0
|
||
Push ${WIN_H}
|
||
Call S
|
||
Pop $R1
|
||
System::Call 'user32::GetSystemMetrics(i 0) i .R2'
|
||
System::Call 'user32::GetSystemMetrics(i 1) i .R3'
|
||
IntOp $R2 $R2 - $R0
|
||
IntOp $R2 $R2 / 2
|
||
IntOp $R3 $R3 - $R1
|
||
IntOp $R3 $R3 / 2
|
||
${If} $R2 < 0
|
||
StrCpy $R2 0
|
||
${EndIf}
|
||
${If} $R3 < 0
|
||
StrCpy $R3 0
|
||
${EndIf}
|
||
System::Call 'user32::SetWindowPos(p $HWNDPARENT, p 0, i $R2, i $R3, i $R0, i $R1, i 0x0014)'
|
||
Call EnsureArt
|
||
FunctionEnd
|
||
|
||
; ── .onInit ────────────────────────────────────────────────────────────────
|
||
; 逐项对照 electron-builder 的 templates/nsis/installer.nsi,只去掉 setInstallSectionSpaceRequired
|
||
; (那是给组件页算体积用的,我们没有组件页)。
|
||
; 安装器 GUI 结束时收尾(正常点「完成」退出、以及中途关窗都走这里)。
|
||
; 定时器是 nsDialogs 建在当前页对话框上的,页被销毁时它自己也随窗口没了;
|
||
; 这里显式再收一次只是为了「页面还在、窗口要关」的那条路,靠 $timerOn 幂等,重复调用无害。
|
||
Function KillTick
|
||
${If} $timerOn == 1
|
||
StrCpy $timerOn 0
|
||
${NSD_KillTimer} Tick
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
Function AbortCleanup
|
||
Call KillTick
|
||
FunctionEnd
|
||
|
||
Function .onGUIEnd
|
||
Call KillTick
|
||
FunctionEnd
|
||
|
||
Function .onInit
|
||
StrCpy $timerOn 0
|
||
StrCpy $timerOn 0
|
||
StrCpy $uiPage 0
|
||
StrCpy $instStarted 0
|
||
StrCpy $instDone 0
|
||
SetOutPath $INSTDIR
|
||
${LogSet} on
|
||
!insertmacro check64BitAndSetRegView
|
||
; 静默安装跳过单实例检查:这个宏的头一句是 BringToFront,而静默模式压根没有窗口,
|
||
; 实测在这种情况下会挂死(wine 下 45 秒不返回 —— 安装器就卡在这儿永远装不完)。
|
||
; 交互安装照常查:那才是「用户重复双击」真正会发生的地方。
|
||
${IfNot} ${Silent}
|
||
${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
|
||
|
||
; ── 页面序列 ───────────────────────────────────────────────────────────────
|
||
|
||
Page custom PageWelcome
|
||
Page custom PageLicense PageLicenseLeave
|
||
Page custom PageDir PageDirLeave
|
||
|
||
Page custom PageInstall PageInstallLeave
|
||
Page custom PageFinish PageFinishLeave
|
||
|
||
; 空跑的 instfiles 页(放在页面序列最后):NSIS 要求「有安装段就必须有 instfiles 页」,
|
||
; 否则 -WX 下直接报 warning 8000 编译不过。在 PRE 回调里 Abort 掉、页面不会显示,
|
||
; 而 P5 的「完成」是直接结束安装器(见 OnFinish),根本走不到这一页。
|
||
PageEx instfiles
|
||
Caption " "
|
||
PageCallbacks SkipInstfiles "" ""
|
||
PageExEnd
|
||
|
||
Function SkipInstfiles
|
||
Abort
|
||
FunctionEnd
|
||
|
||
; ── 安装流程 ───────────────────────────────────────────────────────────────
|
||
; 「立即运行」用 electron-builder 的 StartApp 宏(common.nsh):它内部走 ExecShellAsUser,
|
||
; 以**登录用户**身份启动 —— 「为所有用户安装」那一趟进程是提权过的,直接 Exec 会让应用
|
||
; 继承管理员令牌,装完跑起来的会是个管理员身份的客户端(数据目录/开机自启都跑到别人家去)。
|
||
; 这个宏里带一句 Var /GLOBAL startAppArgs,整个脚本只能插入一次,所以包成一个函数给两处调。
|
||
Function StartAppNow
|
||
HideWindow
|
||
!insertmacro StartApp
|
||
FunctionEnd
|
||
|
||
; 安装流程本体。放在函数里而不是 Section 里,是为了让 P4 能是自绘页(原因见 Tick)。
|
||
; 顺序逐行对照 electron-builder 的 templates/nsis/installSection.nsh。
|
||
Function DoInstall
|
||
; 一次性守卫:交互安装由 P4 的定时器触发,静默安装由安装段触发,
|
||
; 两条路都从这里进,谁先到谁干活,绝不可能装两遍。
|
||
${If} $instStarted == 1
|
||
Return
|
||
${EndIf}
|
||
StrCpy $instStarted 1
|
||
; 进度分段的时间基准:第一段从开工这一刻算起,保证 stage-1 也至少显示 STAGE_DWELL 毫秒
|
||
System::Call 'kernel32::GetTickCount() i .R0'
|
||
StrCpy $stageT0 $R0
|
||
|
||
; 静默安装没有 UI 可选安装模式,per-machine 这一趟要在这里补提权。
|
||
; 比上游多一个 $installMode == "all" 的条件:上游只在「已有 per-machine 安装」时提权,
|
||
; 而我们把默认模式改成了「为所有用户」,全新静默安装(/S)就会默认往 Program Files 写,
|
||
; 不提权必然写不进去 —— 现场用脚本装机时这一步比交互装机更要紧。
|
||
!ifndef INSTALL_MODE_PER_ALL_USERS
|
||
!ifndef ONE_CLICK
|
||
${if} $installMode == "all"
|
||
${andIf} ${Silent}
|
||
; 静默安装**绝不弹 UAC**:无人值守时没人点那个框,弹出即等于装不完。
|
||
; 所以这里完全不碰 UAC 插件(提权与判断都不做),只查一眼令牌:
|
||
; 已经是管理员 → 按「为所有用户」继续,装到 Program Files 并放行防火墙;
|
||
; 不是管理员 → 退回「仅为我安装」(%LOCALAPPDATA%\Programs\...),
|
||
; 不需要提权也能装完,退出码仍然是 0。
|
||
; 交互安装不提权的事在 P3 离开时按 multiUserUi 的范式处理,与这里无关。
|
||
Call CheckAdmin
|
||
${if} $gIsAdmin == 1
|
||
!insertmacro setInstallModePerAllUsers
|
||
${else}
|
||
!insertmacro setInstallModePerUser
|
||
${endIf}
|
||
${endIf}
|
||
!endif
|
||
!endif
|
||
|
||
InitPluginsDir
|
||
|
||
${IfNot} ${Silent}
|
||
SetDetailsPrint none
|
||
${endif}
|
||
|
||
StrCpy $appExe "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
||
|
||
# must be called before uninstallOldVersion
|
||
!insertmacro setLinkVars
|
||
|
||
; 非 one-click 时只在最外层进程里查:提权出来的内层进程和用户看到的可能是同一个应用,
|
||
; 两个进程同时抢着关它会互相打架(上游 installer.nsh / installSection.nsh 就是这么分的)。
|
||
; 静默安装没有窗口,那句 UAC_IsInnerInstance 走插件、会卡住不返回,所以直接查。
|
||
${If} ${Silent}
|
||
Call CheckAppRunningImpl
|
||
${Else}
|
||
${ifNot} ${UAC_IsInnerInstance}
|
||
Call CheckAppRunningImpl
|
||
${endif}
|
||
${EndIf}
|
||
|
||
StrCpy $keepShortcuts "false"
|
||
!insertMacro setIsTryToKeepShortcuts
|
||
${if} $isTryToKeepShortcuts == "true"
|
||
ReadRegStr $R1 SHELL_CONTEXT "${INSTALL_REGISTRY_KEY}" KeepShortcuts
|
||
${if} $R1 == "true"
|
||
${andIf} ${FileExists} "$appExe"
|
||
StrCpy $keepShortcuts "true"
|
||
${endIf}
|
||
${endif}
|
||
|
||
; 覆盖安装:先跑旧卸载器(它自己会带 --updated / --keep-shortcuts)。
|
||
; ⚠️ 静默安装(/S)整段跳过:旧版本装的那个卸载器会为它自己再要一次 UAC 提权
|
||
; (老版本的提权判定有 bug,即使当前进程已经提权它也要再弹一次),无人值守时那个请求
|
||
; 要么没人点、要么被系统直接取消(1223 = ERROR_CANCELLED)—— 真机 /S 就是这么挂住的
|
||
; (旧卸载器重试 5 轮后 handleUninstallResult 弹一个没有 /SD 的框,安装器就永远等在那儿)。
|
||
; 我们是就地覆盖同一个安装目录,文件锁由上面的 CloseRunningApp 负责解开,
|
||
; 旧文件/旧快捷方式会被新安装直接覆盖,所以静默安装跳过它、继续往下装是安全的;
|
||
; 交互安装照旧(用户在场,那个提权框有人点,语义与上游完全一致)。
|
||
${IfNot} ${Silent}
|
||
!insertmacro uninstallOldVersion SHELL_CONTEXT
|
||
!insertmacro handleUninstallResult SHELL_CONTEXT
|
||
${if} $installMode == "all"
|
||
!insertmacro uninstallOldVersion HKEY_CURRENT_USER
|
||
!insertmacro handleUninstallResult HKEY_CURRENT_USER
|
||
${endIf}
|
||
${EndIf}
|
||
|
||
SetOutPath $INSTDIR
|
||
|
||
!insertmacro StageCall 1
|
||
!insertmacro installApplicationFiles
|
||
!insertmacro StageCall 2
|
||
!insertmacro registryAddInstallInfo
|
||
!insertmacro StageCall 3
|
||
!insertmacro addStartMenuLink $keepShortcuts
|
||
!insertmacro addDesktopLink $keepShortcuts
|
||
|
||
${if} ${FileExists} "$newStartMenuLink"
|
||
StrCpy $launchLink "$newStartMenuLink"
|
||
${else}
|
||
StrCpy $launchLink "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
||
${endif}
|
||
|
||
!ifmacrodef registerFileAssociations
|
||
!insertmacro registerFileAssociations
|
||
!endif
|
||
|
||
!insertmacro StageCall 4
|
||
!ifmacrodef customInstall
|
||
!insertmacro customInstall
|
||
!endif
|
||
!insertmacro StageCall 5
|
||
StrCpy $instDone 1
|
||
|
||
; 真卸载器在这里现写。先删掉 installApplicationFiles 落下来的那个占位文件,
|
||
; 免得「万一没写成功、$INSTDIR 里却留着一个假卸载器」这种最坏情况。
|
||
Delete "$INSTDIR\${UNINSTALL_FILENAME}"
|
||
WriteUninstaller "$INSTDIR\${UNINSTALL_FILENAME}"
|
||
|
||
|
||
|
||
!ifdef ONE_CLICK
|
||
!ifdef RUN_AFTER_FINISH
|
||
${ifNot} ${Silent}
|
||
${orIf} ${isForceRun}
|
||
Call StartAppNow
|
||
${endIf}
|
||
!else
|
||
${if} ${isForceRun}
|
||
Call StartAppNow
|
||
${endIf}
|
||
!endif
|
||
!insertmacro quitSuccess
|
||
!else
|
||
; 交互安装的「立即运行」在 P5 上处理;静默安装只能靠 --force-run
|
||
${if} ${isForceRun}
|
||
${andIf} ${Silent}
|
||
Call StartAppNow
|
||
${endIf}
|
||
!endif
|
||
FunctionEnd
|
||
|
||
; 安装段的两条路:
|
||
; 交互安装:安装段根本不会执行(页面序列里的 instfiles 页被 Abort 掉了),
|
||
; 安装流程由 P4 的定时器调 DoInstall 跑;
|
||
; 静默安装(/S):没有页面可走,段就是唯一入口 —— 这一步走的是 electron-builder
|
||
; 原生的「段在 instfiles 页上执行」,和上游同一条路。
|
||
; 之所以不把静默安装也塞进 .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
|
||
${EndIf}
|
||
SectionEnd
|