From b1c6d5d54a389404ab905501d9143a470105069a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 13 Sep 2026 21:13:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(installer):=20=E5=AE=9A=E6=97=B6=E5=99=A8?= =?UTF-8?q?=E5=88=86=E8=BE=A8=E7=8E=87=201.5=20=E7=A7=92=20=E2=86=92=20?= =?UTF-8?q?=E6=94=B9=E7=94=A8=20nsDialogs=20=E5=AE=9A=E6=97=B6=E5=99=A8?= =?UTF-8?q?=EF=BC=88=E6=8B=96=E4=B8=8D=E5=8A=A8/=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E8=B7=B3/=E5=8D=A1=E6=94=B6=E5=B0=BE=E4=B8=89=E7=97=87?= =?UTF-8?q?=E7=8A=B6=E5=90=8C=E6=A0=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机取证: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 频率证据。 --- console/installer/installer.nsi | 69 +++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 21 deletions(-) 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 里却留着一个假卸载器」这种最坏情况。