diff --git a/console/installer/installer.nsi b/console/installer/installer.nsi index aa9fadf..a7aa422 100644 --- a/console/installer/installer.nsi +++ b/console/installer/installer.nsi @@ -81,6 +81,9 @@ !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 @@ -133,8 +136,8 @@ Var stCancel Var stBrowse Var stFinish Var uiPage ; 0=无页面,1..5=当前页;定时器靠它判断还能碰哪些控件 -Var timerId -Var timerOn ; 定时器是否已经创建过(销毁时只认这个哨兵,不靠 id 猜) +Var timerOn ; tick 定时器当前是不是活的(建/销毁都认这个哨兵,幂等) +Var stageT0 ; 上一段进度的时刻(GetTickCount),用来保证每段都看得见 Var gDpi ; 系统 DPI(LOGPIXELSX) Var gArt ; 本次要用的素材目录($PLUGINSDIR\1x 或 \2x) Var gCx @@ -149,6 +152,7 @@ Var licOk ; 许可页勾选 Var runApp ; 完成页「立即运行」 Var modeAll ; 安装模式单选:1=为所有用户,0=仅为我 Var instStarted ; 安装流程是否已经跑起来(一次性,见 DoInstall 开头的守卫) +Var instDone ; 安装流程是否已经跑完(tick 里据此才去点「下一步」翻到完成页) Var gAdminDbg ; CheckAdmin 的原始值(判定为非提权时写进 %TEMP% 便于现场定位) Var gIsAdmin ; CheckAdmin 的结果:1 = 当前进程已提权 Var dragCx ; 拖动锚点:按下瞬间的光标屏幕坐标 @@ -843,6 +847,12 @@ Function Tick ; 定时器不触发)。改成自绘页 + 定时器驱动,消息泵一直是通的。 ${If} $instStarted == 0 Call DoInstall + ${ElseIf} $instDone == 1 + ; 安装已经跑完,但页面还停在「正在安装」(真机复验:卡在最后一段不翻页)。 + ; 翻页靠点被藏起来的「下一步」,这一下有可能没生效,所以每个 tick 重试一次, + ; 直到页面真的不再是这一页 —— 翻页成功后 $uiPage 立刻变了,不会多点到下一页去。 + ; ⚠️ 必须等 $instDone:安装途中的 Sleep 会把消息泵转起来,那时候也会跑进 Tick, + ; 没有这道门就会在装到一半时去点「下一步」。 GetDlgItem $0 $HWNDPARENT 1 SendMessage $0 ${BM_CLICK} 0 0 ${EndIf} @@ -975,14 +985,15 @@ Function NewPage ${If} $hPage == error Abort ${EndIf} - ; 悬停/拖动全靠这个 60ms 轮询(POC 结论:比窗口子类化稳,代价是光标移动要等一个 tick)。 - ; 建在页面里而不是 .onGUIInit:StdUtils 的定时器要挂在页对话框上才跑得起来。 - ; ⚠️ 全程只建一个:它挂在 StdUtils 的隐藏窗口上,不随页面消失,而 NewPage 每页都会跑 —— - ; 不加哨兵就会越翻页越多(越跑越卡,退出时还会漏掉没销毁的那些)。 - ${If} $timerOn == 0 - StrCpy $timerOn 1 - ${StdUtils.TimerCreate} $timerId Tick 60 - ${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 @@ -1248,6 +1259,18 @@ Function Stage ${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" @@ -1504,29 +1527,29 @@ FunctionEnd ; 逐项对照 electron-builder 的 templates/nsis/installer.nsi,只去掉 setInstallSectionSpaceRequired ; (那是给组件页算体积用的,我们没有组件页)。 ; 安装器 GUI 结束时收尾(正常点「完成」退出、以及中途关窗都走这里)。 -; 不销毁的话 StdUtils 插件在卸载时会弹「Plug-in unloaded before all timers were destroyed」。 -; 只认哨兵 $timerOn:对没建过 / 已销毁的 id 调 TimerDestroy 本身也是一种报错源。 -Function AbortCleanup +; 定时器是 nsDialogs 建在当前页对话框上的,页被销毁时它自己也随窗口没了; +; 这里显式再收一次只是为了「页面还在、窗口要关」的那条路,靠 $timerOn 幂等,重复调用无害。 +Function KillTick ${If} $timerOn == 1 StrCpy $timerOn 0 - ${StdUtils.TimerDestroy} $R0 $timerId - StrCpy $timerId 0 + ${NSD_KillTimer} Tick ${EndIf} FunctionEnd +Function AbortCleanup + Call KillTick +FunctionEnd + Function .onGUIEnd - ${If} $timerOn == 1 - StrCpy $timerOn 0 - ${StdUtils.TimerDestroy} $R0 $timerId - StrCpy $timerId 0 - ${EndIf} + Call KillTick FunctionEnd Function .onInit - StrCpy $timerId 0 + StrCpy $timerOn 0 StrCpy $timerOn 0 StrCpy $uiPage 0 StrCpy $instStarted 0 + StrCpy $instDone 0 SetOutPath $INSTDIR ${LogSet} on !insertmacro check64BitAndSetRegView @@ -1595,6 +1618,9 @@ Function DoInstall 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 安装」时提权, @@ -1694,6 +1720,7 @@ Function DoInstall !insertmacro customInstall !endif !insertmacro StageCall 5 + StrCpy $instDone 1 ; 真卸载器在这里现写。先删掉 installApplicationFiles 落下来的那个占位文件, ; 免得「万一没写成功、$INSTDIR 里却留着一个假卸载器」这种最坏情况。