From a57df78f990eb862032048658271580bfd8ae230 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 13 Sep 2026 23:32:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(installer):=20=E5=AE=89=E8=A3=85=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E8=AF=86=E5=88=AB=E4=B8=8E=E6=A8=A1=E5=BC=8F=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=9B=AE=E5=BD=95=EF=BC=88=E7=9C=9F=E6=9C=BA=E5=9B=9B?= =?UTF-8?q?=E5=9B=A0=E6=A0=B9=E9=99=A4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主人真机复验:① 不能像原版那样自动识别「已安装程序的安装位置」;② 选「为所有用户安装」却装到 %LOCALAPPDATA% 而不是 Program Files。 真机打点(instrumented 副本 + WM_COMMAND/BM_CLICK/WM_SETTEXT 驱动)读出四个根因: 1. 切换安装模式单选钮只改状态位图,**不同步输入框**; 2. setInstallModePerAllUsers/PerUser 会按记忆/默认**重算 $INSTDIR**,把「框里定下的值」 (用户手改、浏览选择、记忆)**无条件冲掉** —— 这是两条症状的共同根因 (打点:after-normalize=[D:\SBCustom\Scoreboard Console] → after-mode=[C:\Program Files (x86)\…]); 3. 记忆读取 HKCU 优先、**不看安装模式** → 一条更早的用户级记忆顶掉刚装好的机器级目录; 4. 无人值守会话里 $LOCALAPPDATA 解析成 C:\ProgramData →「仅为我」默认成了 C:\ProgramData\Programs\…。 改法: - 新增 ModeDefaultDir:「为所有用户」→ $PROGRAMFILES64\${APP_FILENAME}(本包是 x64); 「仅为我」→ SHGetKnownFolderPath(FOLDERID_UserProgramFiles),失败才退回 $LOCALAPPDATA\Programs; - 新增 PathTouched(框里的文本 != 程序设定的值 = 用户手改过)与 SyncPathToMode: 切模式时**只有用户没改过**才把框与 $INSTDIR 同步成该模式默认目录(不冲掉用户刚填的); - PageDirLeave 用 $pathKeep 保住路径,跑完上游两个宏后再恢复 —— 只让宏改模式与 SHELL_CONTEXT,不再改路径; - .onInit 在 customInit 之后**按当前模式**重读自家记忆键(all → HKLM,否则 HKCU)。 真机六条全过(含落盘复核):进 P3 显示记忆目录;点「为所有用户」→ C:\Program Files\Scoreboard_Console; 点「仅为我」→ %LOCALAPPDATA%\Programs\…;点回去再正确切换;手塞自定义路径以它为准;改过后再切模式不被冲掉。 静默无回归:/S 6.6s exit=0 落记忆目录;/S /D= 6.4s exit=0 且 /D= 优先。 wine 回归:人手速度拖动、原生控件隐藏、P3→P5 自动翻页、✕ 退出无 StdUtils 报错、旧版在跑时 /S 关掉旧进程。 --- console/installer/installer.nsi | 101 +++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/console/installer/installer.nsi b/console/installer/installer.nsi index a7aa422..c1272e0 100644 --- a/console/installer/installer.nsi +++ b/console/installer/installer.nsi @@ -162,6 +162,8 @@ Var dragWy Var gDparam ; 命令行 /D= 指定的安装目录(要在 customInit 的记忆目录之后盖回去) Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局) Var dragging +Var pathSet ; 我们最后一次写进「安装位置」输入框的值(用来判断用户有没有手改过) +Var pathKeep ; 计算路径时临时存一份(上游那两个宏会重写 $INSTDIR,用它盖回来) ; ── include ──────────────────────────────────────────────────────────────── @@ -1183,16 +1185,82 @@ Function OnLicNext Call GoNext FunctionEnd +; 当前安装模式对应的「默认目录」(结果放 $R0)。 +; 「为所有用户」= 公共目录:本包是 x64 应用,所以优先 $PROGRAMFILES64(真机期望就是 C:\Program Files\…), +; 32 位系统上退回 $PROGRAMFILES;「仅为我」= %LOCALAPPDATA%\Programs\…(与上游 setInstallModePerUser 一致)。 +Function ModeDefaultDir + ${If} $modeAll == 1 + StrCpy $R0 "$PROGRAMFILES" + !ifdef APP_64 + ${If} ${RunningX64} + StrCpy $R0 "$PROGRAMFILES64" + ${EndIf} + !endif + StrCpy $R0 "$R0\${APP_FILENAME}" + ${Else} + ; 与上游 setInstallModePerUser 同源:优先问 shell 要「用户程序目录」(FOLDERID_UserProgramFiles)。 + ; 不能直接用 $LOCALAPPDATA:在提权/无人值守(session 0,没有加载用户配置)的会话里, + ; NSIS 的 $LOCALAPPDATA 会解析成 C:\ProgramData —— 真机实测就是装到了 + ; 「C:\ProgramData\Programs\Scoreboard_Console」,那不是用户的程序目录。 + StrCpy $R0 "$LOCALAPPDATA\Programs" + Push $0 + Push $1 + Push $2 + StrCpy $2 0 + System::Call 'SHELL32::SHGetKnownFolderPath(g "${FOLDERID_UserProgramFiles}", i ${KF_FLAG_CREATE}, p 0, *p .r2) i .r1' + ${If} $1 == 0 + System::Call 'KERNEL32::lstrcpynW(w .r0, p r2, i ${NSIS_MAX_STRLEN}) p' + StrCpy $R0 "$0" + ${EndIf} + ${If} $2 != 0 + System::Call 'OLE32::CoTaskMemFree(p r2)' + ${EndIf} + Pop $2 + Pop $1 + Pop $0 + StrCpy $R0 "$R0\${APP_FILENAME}" + ${EndIf} +FunctionEnd + +; 用户有没有手改过「安装位置」框?结果放 $R9(1 = 改过)。 +; 判据是「框里的文本 != 我们最后一次写进去的值」——不需要额外的控件通知,浏览/手输都盖得住。 +Function PathTouched + ${NSD_GetText} $hPath $R1 + StrCpy $R9 0 + ${If} $R1 != $pathSet + StrCpy $R9 1 + ${EndIf} +FunctionEnd + +; 切模式时把路径与输入框同步到该模式的默认目录。 +; 策略(说得通的理由):**用户没手改过**才跟着模式变;用户改过就保持他填的, +; 免得「改完路径再点一下单选」把用户刚填的东西悄悄冲掉。 +; 之前这里只改单选状态位图、不动框 —— 于是「选了为所有用户,框里还是用户目录」, +; 真机复验的两条症状(记忆目录看不出、选公共安装却像在装用户目录)都是它引起的。 +Function SyncPathToMode + Call PathTouched + ${If} $R9 == 0 + Call ModeDefaultDir + StrCpy $INSTDIR $R0 + ${If} $hPath != 0 + ${NSD_SetText} $hPath "$INSTDIR" + StrCpy $pathSet "$INSTDIR" + ${EndIf} + ${EndIf} +FunctionEnd + Function OnModeUser StrCpy $modeAll 0 !insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16 !insertmacro SetBmpBase $hRadio2 "radio-off.bmp" 16 16 + Call SyncPathToMode FunctionEnd Function OnModeAll StrCpy $modeAll 1 !insertmacro SetBmpBase $hRadio1 "radio-off.bmp" 16 16 !insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16 + Call SyncPathToMode FunctionEnd ; 「浏览…」:系统文件夹选择框没有 NSIS 包装,用 System::Call 直接调 shell32。 @@ -1213,6 +1281,8 @@ Function OnBrowse ${If} $R2 != "" StrCpy $tmpTxt $R2 ${NSD_SetText} $hPath "$tmpTxt" + ; 浏览选出来的目录也算用户改过:别让切模式把它冲掉 + StrCpy $pathSet "" ${EndIf} ${EndIf} System::Free $R7 @@ -1365,7 +1435,11 @@ Function PageDir Call Chrome !insertmacro NewEdit "${ES_AUTOHSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${PATH_EDIT_X} ${PATH_EDIT_Y} ${PATH_EDIT_W} ${PATH_EDIT_H} $hPath + ; 进页面时框里显示的就是当前 $INSTDIR:initMultiUser + customInit 已经把「上次装到哪」 + ; (我们自己的记忆键,HKCU 优先、再 HKLM)读进来了,所以这里就是「记忆目录」。 + ; $pathSet 记下我们写的这份值,之后 SyncPathToMode / PageDirLeave 靠它判断用户有没有手改过。 ${NSD_SetText} $hPath "$INSTDIR" + StrCpy $pathSet "$INSTDIR" !insertmacro NewBmpBtn ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H} $hBrowse "btn-browse-normal.bmp" ${NSD_OnClick} $hBrowse OnBrowse @@ -1414,12 +1488,17 @@ Function PageDirLeave StrCpy $INSTDIR "$INSTDIR\${APP_FILENAME}" ${EndIf} - ; 兜底:万一路径是空的(记忆为空 + 默认值也没算出来),给一个合法的按用户目录, + ; 兜底:万一路径是空的(记忆为空 + 默认值也没算出来),按当前模式给一个合法默认目录, ; 绝不把空 $INSTDIR 带进安装流程(空路径是「点了没反应 / 装到莫名其妙的地方」的经典来源)。 + ; 走 ModeDefaultDir 而不是直接拼 $LOCALAPPDATA —— 后者在无人值守会话里会解析成 C:\ProgramData。 ${If} $INSTDIR == "" - StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${APP_FILENAME}" + Call ModeDefaultDir + StrCpy $INSTDIR "$R0" ${EndIf} + ; 先把定下来的路径存一份:下面这两个上游宏会按注册表记忆/模式默认**重新算 $INSTDIR**, + ; 而这一页的规矩是「框里是什么就装什么」(记忆、模式默认、用户手改、/D= 都已经体现在框里了)。 + StrCpy $pathKeep "$INSTDIR" ${If} $modeAll == 1 ; 「为所有用户安装」。执行级别是 highest,所以**管理员账户的机器上启动时就提过权了**, ; 这里不需要(也不该)再走一次 UAC_RunElevated:中途提权会把窗口藏起来、让用户看见 @@ -1436,6 +1515,8 @@ Function PageDirLeave ${Else} !insertmacro setInstallModePerUser ${EndIf} + ; 盖回框里定下的路径($installMode / SHELL_CONTEXT 已经在宏里设好了,这里只保路径) + StrCpy $INSTDIR "$pathKeep" FunctionEnd ; ── P4 正在安装 ──────────────────────────────────────────────────────────── @@ -1572,6 +1653,22 @@ Function .onInit !ifmacrodef customInit !insertmacro customInit !endif + ; 记忆目录要跟**安装模式**对齐,这才是「原版安装器」的行为:initMultiUser 读的就是该模式对应的键 + ; (为所有用户 → HKLM,仅为我 → HKCU)。构建期生成的 customInit 是「HKCU 的自家记忆优先」, + ; 于是一条更早的用户级记忆会把刚装好的机器级目录顶掉 —— 真机实测:机器上最后一次装在 + ; C:\Program Files\Scoreboard_Console,安装位置页却填着更早的 D:\Program Files\Scoreboard_Console。 + ; 这里按当前模式再读一遍自家记忆键,有值就盖回去;两个 hive 都空时保留 initMultiUser/customInit 的结果。 + ${If} $installMode == "all" + ReadRegStr $R9 HKLM "${SCOREBOARD_INSTALL_KEY}" "InstallLocation" + ${If} $R9 != "" + StrCpy $INSTDIR $R9 + ${EndIf} + ${Else} + ReadRegStr $R9 HKCU "${SCOREBOARD_INSTALL_KEY}" "InstallLocation" + ${If} $R9 != "" + StrCpy $INSTDIR $R9 + ${EndIf} + ${EndIf} ${If} $gDparam != "" StrCpy $INSTDIR $gDparam ${EndIf}