fix(windows): elevate installer when update destination is protected

This commit is contained in:
zipg
2026-10-08 11:02:13 +08:00
committed by GitHub
parent 934d731d00
commit 33f7c19675
3 changed files with 434 additions and 1 deletions
+184
View File
@@ -0,0 +1,184 @@
// Opt-in Windows integration test. Requires NSIS, gh, and handlebars installed
// in an isolated fixture directory (npm install --prefix <directory> handlebars).
// Run: node scripts/windows-installer.integration.mjs <directory>
// Uses real runas elevation; approve UAC if Windows prompts. The full production
// installer template installs a disposable test app, never the user's DBX.
import fs from 'node:fs';
import path from 'node:path';
import {execFileSync, spawn} from 'node:child_process';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {fileURLToPath} from 'node:url';
assert.equal(process.platform, 'win32', 'This integration test requires Windows');
assert.ok(process.argv[2], 'Pass an isolated fixture directory containing handlebars');
const root = path.resolve(process.argv[2]);
const hb = createRequire(path.join(root, 'package.json'))('handlebars');
const repo = fileURLToPath(new URL('..', import.meta.url));
const compiler = path.join(process.env.LOCALAPPDATA, 'tauri/NSIS/makensis.exe');
const product = `DBX Issue10895 Test ${Date.now()}`;
const manufacturer = 'DBXInstallerIntegrationTest';
const target = path.join(root, 'protected DBX folder');
const report = path.join(root, 'report.ini');
const mainName = 'dbx-10895-test-app';
const registry = `Software\\${manufacturer}\\${product}`;
const uninstallRegistry = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${product}`;
const ref = '9b17a7aeae9a83222ffe829aa4e2d8a5ba6bed8c';
// Refuse to overwrite existing fixture installs. Cleanup only these exact paths
// and the unique test registry keys, leaving source files and reports for review.
assert.equal(fs.existsSync(target), false, 'Fixture destination already exists');
assert.equal(fs.existsSync(path.join(root, 'writable DBX folder')), false, 'Writable fixture destination already exists');
fs.rmSync(report, {force:true});
function compile(name, source) {
const file = path.join(root, `${name}.nsi`);
fs.writeFileSync(file, source);
const out = execFileSync(compiler, ['/V1', '/INPUTCHARSET', 'UTF8', file], {cwd:root, encoding:'utf8', timeout:30000});
if(out.trim()) console.log(out.trim());
}
function run(name, args = []) {
const exe = path.join(root, `${name}.exe`);
execFileSync(exe,args,{cwd:root,argv0:`"${exe}"`,windowsVerbatimArguments:true,timeout:30000});
}
function base(name, level='user') {
return `Unicode true\n!include MUI2.nsh\n!include FileFunc.nsh\nName "${name}"\nOutFile "${root}\\${name}.exe"\nRequestExecutionLevel ${level}\nSilentInstall silent\n`;
}
async function waitFor(check) {
const end = Date.now()+30000;
while(Date.now()<end) {
if(check()) return;
await new Promise(r=>setTimeout(r,200));
}
throw new Error('Timed out waiting for elevated installer: '+(fs.existsSync(report)?fs.readFileSync(report,'utf8'):''));
}
async function main() {
for(const file of ['utils.nsh','FileAssociation.nsh','languages/English.nsh','languages/SimpChinese.nsh','languages/TradChinese.nsh']) {
const data=JSON.parse(execFileSync('gh',['api',`repos/tauri-apps/tauri/contents/crates/tauri-bundler/src/bundle/windows/nsis/${file}?ref=${ref}`],{encoding:'utf8'}));
fs.writeFileSync(path.join(root,path.basename(file)),Buffer.from(data.content,'base64'));
}
for(const version of ['old','new']) {
compile(`${mainName}-${version}`,base(`${mainName}-${version}`)+`
Function .onInit
WriteINIStr "${report}" "app" "version" "${version}"
WriteINIStr "${report}" "app" "cmdline" "$CMDLINE"
System::Call 'shell32::IsUserAnAdmin() i .r0'
WriteINIStr "${report}" "app" "admin" "$0"
\u0024{GetOptions} $CMDLINE "/HOLD" $0
\u0024{IfNot} \u0024{Errors}
WriteINIStr "${report}" "app" "holding" "1"
Sleep 120000
\u0024{EndIf}
SetErrorLevel 0
Quit
FunctionEnd
Section
SectionEnd
`);
}
fs.copyFileSync(path.join(root,`${mainName}-new.exe`),path.join(root,`${mainName}.exe`));
fs.writeFileSync(path.join(root,'payload.txt'),'updated resource payload');
fs.writeFileSync(path.join(root,'hooks.nsh'),`
!macro NSIS_HOOK_PREINSTALL
System::Call 'shell32::IsUserAnAdmin() i .r0'
WriteINIStr "${report}" "installer" "admin" "$0"
WriteINIStr "${report}" "installer" "cmdline" "$CMDLINE"
WriteINIStr "${report}" "installer" "language" "$LANGUAGE"
WriteINIStr "${report}" "installer" "update" "$UpdateMode"
WriteINIStr "${report}" "installer" "passive" "$PassiveMode"
WriteINIStr "${report}" "installer" "noShortcut" "$NoShortcutMode"
!macroend
!macro NSIS_HOOK_POSTINSTALL
WriteINIStr "${report}" "installer" "complete" "1"
!macroend
`);
const data={compression:'lzma',installer_hooks:path.join(root,'hooks.nsh'),manufacturer,product_name:product,version:'1.0.1',version_with_build:'1.0.1.0',install_mode:'currentUser',main_binary_name:mainName,main_binary_path:path.join(root,`${mainName}.exe`),bundle_id:'com.dbx.integration10895',copyright:'DBX installer test',out_file:path.join(root,'installer.exe'),arch:'x64',additional_plugins_path:path.join(process.env.LOCALAPPDATA,'tauri/NSIS/Plugins/x86-unicode/additional'),allow_downgrades:'true',display_language_selector:'false',install_webview2_mode:'',estimated_size:1024,start_menu_folder:'',languages:['English','SimpChinese','TradChinese'],language_files:['English.nsh','SimpChinese.nsh','TradChinese.nsh'],resources:{[path.join(root,'payload.txt')]:[path.join(root,'payload.txt'),'payload.txt']}};
hb.registerHelper('no-escape',value=>new hb.SafeString(value));
const template = fs.readFileSync(path.join(repo,'src-tauri/windows/nsis/installer.nsi'),'utf8');
compile('installer',hb.compile(template,{noEscape:true})(data));
compile('seed',base('seed','admin')+`
Section
CreateDirectory "${target}"
SetOutPath "${target}"
File /oname=${mainName}.exe "${root}\\${mainName}-old.exe"
FileOpen $0 "${target}\\payload.txt" w
FileWrite $0 "old resource payload"
FileClose $0
WriteRegStr HKCU "${registry}" "" "${target}"
WriteRegStr HKCU "${uninstallRegistry}" "" "${target}"
WriteRegStr HKCU "${uninstallRegistry}" "DisplayVersion" "1.0.0"
WriteRegStr HKCU "${uninstallRegistry}" "UninstallString" '"${target}\\uninstall.exe"'
ExecWait '"$SYSDIR\\icacls.exe" "${target}" /inheritance:r /grant:r "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-545:(OI)(CI)RX"' $0
WriteINIStr "${report}" "seed" "aclExit" "$0"
System::Call 'shell32::IsUserAnAdmin() i .r0'
WriteINIStr "${report}" "seed" "admin" "$0"
SectionEnd
`);
compile('driver',base('driver')+`
Function .onInit
System::Call 'shell32::IsUserAnAdmin() i .r0'
WriteINIStr "${report}" "driver" "admin" "$0"
ExecShellWait "runas" "${root}\\seed.exe"
SetErrorLevel 0
Quit
FunctionEnd
Section
SectionEnd
`);
compile('cleanup',base('cleanup','admin')+`
Section
RMDir /r "${target}"
RMDir /r "${root}\\writable DBX folder"
DeleteRegKey HKCU "${registry}"
DeleteRegKey /ifempty HKCU "Software\\${manufacturer}"
DeleteRegKey HKCU "${uninstallRegistry}"
SectionEnd
`);
compile('cleanup-driver',base('cleanup-driver')+`
Function .onInit
ExecShellWait "runas" "${root}\\cleanup.exe"
SetErrorLevel 0
Quit
FunctionEnd
Section
SectionEnd
`);
run('driver');
console.log(fs.readFileSync(report,'utf8'));
assert.throws(()=>fs.openSync(path.join(target,`${mainName}.exe`),'r+'),/EACCES|EPERM/);
const oldApp=spawn(path.join(target,`${mainName}.exe`),['/HOLD'],{windowsHide:true,stdio:'ignore'});
await waitFor(()=>fs.readFileSync(report,'utf8').includes('holding=1'));
assert.equal(oldApp.exitCode,null);
run('installer',['/UPDATE','/P','/NS','/R','/ARGS','"argument with spaces"']);
await waitFor(()=>fs.existsSync(report)&&fs.readFileSync(report,'utf8').includes('version=new'));
const output=fs.readFileSync(report,'utf8');
console.log(output);
assert.match(output,/\[driver\]\r?\nadmin=0/);
assert.match(output,/\[installer\]\r?\nadmin=1/);
assert.match(output,/complete=1/);
assert.match(output,/update=1/);
assert.match(output,/passive=1/);
assert.match(output,/noShortcut=1/);
assert.match(output,/\/DBX_ELEVATED/);
assert.match(output,/\[app\][\s\S]*version=new[\s\S]*argument with spaces/);
assert.deepEqual(fs.readFileSync(path.join(target,`${mainName}.exe`)),fs.readFileSync(path.join(root,`${mainName}.exe`)));
assert.equal(fs.readFileSync(path.join(target,'payload.txt'),'utf8'),'updated resource payload');
assert.ok(fs.existsSync(path.join(target,'uninstall.exe')));
assert.notEqual(oldApp.exitCode,null,'old application was stopped before overwrite');
console.log('REAL ELEVATION AND FULL TEMPLATE UPDATE WITH RUNNING OLD APP: PASS');
fs.copyFileSync(report,path.join(root,'protected-update-report.ini'));
fs.unlinkSync(report);
const writableTarget=path.join(root,'writable DBX folder');
run('installer',['/S','/UPDATE','/NS','/R','/ARGS','"writable update argument"',`/D=${writableTarget}`]);
await waitFor(()=>fs.existsSync(report)&&fs.readFileSync(report,'utf8').includes('version=new'));
const writableOutput=fs.readFileSync(report,'utf8');
console.log(writableOutput);
assert.match(writableOutput,/\[installer\]\r?\nadmin=0/);
assert.doesNotMatch(writableOutput,/\/DBX_ELEVATED/);
assert.equal(fs.readFileSync(path.join(writableTarget,'payload.txt'),'utf8'),'updated resource payload');
console.log('FULL TEMPLATE INSTALL TO WRITABLE FOLDER WITHOUT ELEVATION: PASS');
}
main().catch(e=>{console.error(e);process.exitCode=1;}).finally(()=>{
if(fs.existsSync(path.join(root,'cleanup-driver.exe'))) {
run('cleanup-driver');
assert.equal(fs.existsSync(target),false,'Protected fixture was not removed');
assert.equal(fs.existsSync(path.join(root,'writable DBX folder')),false,'Writable fixture was not removed');
}
});
+151
View File
@@ -0,0 +1,151 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { test } from 'node:test'
const compiler = process.env.NSIS_MAKENSIS || path.join(process.env.LOCALAPPDATA || '', 'tauri', 'NSIS', 'makensis.exe')
const available = process.platform === 'win32' && existsSync(compiler)
test('Windows installer access checks and elevation handoff', { skip: !available }, () => {
const dir = mkdtempSync(path.join(tmpdir(), 'dbx installer test '))
const template = readFileSync(new URL('../src-tauri/windows/nsis/installer.nsi', import.meta.url), 'utf8')
const helper = template.match(/Function DbxEnsureInstallAccess\r?\n[\s\S]*?FunctionEnd/)[0]
const elevationInit = template.slice(template.indexOf('Function .onInit\n') + 'Function .onInit\n'.length, template.indexOf(' \u0024{GetOptions} $CMDLINE "/P" $PassiveMode'))
assert.ok(elevationInit.includes('/DBX_PROFILE='))
const launch = `ExecShell "runas" "$EXEPATH" '/DBX_ELEVATED /DBX_PROFILE="$3" /DBX_LANG=$LANGUAGE $2 /D=$INSTDIR'`
assert.ok(helper.includes(launch))
// Execute the production function, replacing only the OS UAC launch with a
// recorder. Tests must never trigger an interactive prompt or install DBX.
const fixtureHelper = helper.replace(launch, `WriteINIStr "\u0024EXEDIR\\result.ini" "handoff" "args" '/DBX_ELEVATED /DBX_PROFILE="$3" /DBX_LANG=$LANGUAGE $2 /D=$INSTDIR'
\u0024{If} \u0024DbxTestCancel = 1
SetErrors
\u0024{EndIf}`)
const exe = path.join(dir, 'probe.exe')
const result = path.join(dir, 'result.ini')
const destination = path.join(dir, 'existing DBX')
mkdirSync(destination)
const binary = path.join(destination, 'dbx.exe')
writeFileSync(binary, 'original executable contents')
const script = `Unicode true
!include MUI2.nsh
!include FileFunc.nsh
!define MAINBINARYNAME "dbx"
Name "DBX access probe"
OutFile "${exe}"
RequestExecutionLevel user
SilentInstall silent
Var DbxElevated
Var DbxTestCancel
Var DbxTestHandle
!insertmacro MUI_LANGUAGE "English"
LangString dbxElevationFailed \u0024{LANG_ENGLISH} "Elevation cancelled"
LangString dbxElevationUserMismatch \u0024{LANG_ENGLISH} "Different user"
${fixtureHelper}
Function .onInit
${elevationInit}
\u0024{If} $DbxElevated <> 1
StrCpy $LANGUAGE 1033
\u0024{EndIf}
\u0024{GetOptions} $CMDLINE "/TEST_ELEVATED" $0
\u0024{IfNot} \u0024{Errors}
StrCpy $DbxElevated 1
\u0024{EndIf}
\u0024{GetOptions} $CMDLINE "/TEST_LOCK" $0
\u0024{IfNot} \u0024{Errors}
System::Call 'kernel32::CreateFileW(w "$INSTDIR\\dbx.exe", i 0x80000000, i 0, p 0, i 3, i 0, p 0) p .r0'
StrCpy $DbxTestHandle $0
\u0024{EndIf}
\u0024{GetOptions} $CMDLINE "/TEST_CANCEL" $0
\u0024{IfNot} \u0024{Errors}
StrCpy $DbxTestCancel 1
\u0024{EndIf}
StrCpy $0 "register zero"
StrCpy $1 "register one"
StrCpy $2 "register two"
StrCpy $3 "register three"
Call DbxEnsureInstallAccess
WriteINIStr "$EXEDIR\\result.ini" "probe" "registers" "$0|$1|$2|$3"
WriteINIStr "$EXEDIR\\result.ini" "probe" "destination" "$INSTDIR"
WriteINIStr "$EXEDIR\\result.ini" "probe" "language" "$LANGUAGE"
SetErrorLevel 0
Quit
FunctionEnd
Section
SectionEnd
`
const source = path.join(dir, 'probe.nsi')
writeFileSync(source, script)
try {
// Also compile the unmodified production launch instruction and init code.
writeFileSync(source, script.replace(fixtureHelper, helper))
execFileSync(compiler, ['/V2', source], { encoding: 'utf8', timeout: 30_000 })
writeFileSync(source, script)
execFileSync(compiler, ['/V2', source], { encoding: 'utf8', timeout: 30_000 })
const run = (args, target = destination) => {
rmSync(result, { force: true })
let status = 0
try {
execFileSync(exe, [...args, ...(target === null ? [] : [`/D=${target}`])], { timeout: 15_000, windowsVerbatimArguments: true, argv0: `"${exe}"` })
} catch (error) {
if (typeof error.status !== 'number') throw error
status = error.status
}
return { status, output: existsSync(result) ? readFileSync(result, 'utf8') : '' }
}
const writable = run([])
assert.equal(writable.status, 0, writable.output)
assert.match(writable.output, /register zero\|register one\|register two\|register three/)
assert.doesNotMatch(writable.output, /handoff/)
assert.equal(readFileSync(binary, 'utf8'), 'original executable contents')
const locked = run(['/TEST_LOCK'])
assert.equal(locked.status, 0)
assert.doesNotMatch(locked.output, /handoff/)
assert.equal(readFileSync(binary, 'utf8'), 'original executable contents')
const nested = run([], path.join(destination, 'new', 'nested DBX'))
assert.equal(nested.status, 0)
assert.doesNotMatch(nested.output, /handoff/)
assert.equal(existsSync(path.join(destination, 'new')), false)
// The read-only attribute yields ERROR_ACCESS_DENIED without changing ACLs.
execFileSync('attrib.exe', ['+R', binary])
const update = run(['/UPDATE', '/P', '/NS', '/R', '/ARGS', '"argument with spaces"'])
assert.equal(update.status, 0)
assert.match(update.output, /\/DBX_ELEVATED \/DBX_PROFILE="[^"]+" \/DBX_LANG=1033 \/UPDATE \/P \/NS \/R \/ARGS "argument with spaces" \/D=/)
assert.ok(update.output.includes(`/D=${destination}`))
assert.doesNotMatch(update.output, /registers=/)
assert.equal(readFileSync(binary, 'utf8'), 'original executable contents')
const resumed = run([update.output.match(/args=(.*)/)[1]], null)
assert.equal(resumed.status, 0)
assert.doesNotMatch(resumed.output, /handoff/)
assert.ok(resumed.output.includes(`destination=${destination}`))
assert.match(resumed.output, /language=1033/)
const cancelled = run(['/TEST_CANCEL'])
assert.equal(cancelled.status, 740)
assert.doesNotMatch(cancelled.output, /registers=/)
const elevated = run(['/TEST_ELEVATED'])
assert.equal(elevated.status, 0)
assert.doesNotMatch(elevated.output, /handoff/)
assert.match(elevated.output, /register zero\|register one\|register two\|register three/)
const wrongUser = run(['/DBX_ELEVATED', '/DBX_LANG=1033', '/DBX_PROFILE="different Windows account"'])
assert.equal(wrongUser.status, 740)
assert.equal(wrongUser.output, '')
// A machine-protected directory also requests elevation, including a
// destination that does not exist yet. No files are installed there.
const protectedPath = path.join(process.env.ProgramFiles, 'DBX access probe', 'nested')
const protectedFolder = run(['/UPDATE'], protectedPath)
assert.match(protectedFolder.output, /handoff/)
assert.equal(existsSync(protectedPath), false)
} finally {
if (existsSync(binary)) execFileSync('attrib.exe', ['-R', binary])
rmSync(dir, { recursive: true, force: true })
}
})
+99 -1
View File
@@ -95,6 +95,7 @@ Var UpdateMode
Var NoShortcutMode
Var WixMode
Var OldMainBinaryName
Var DbxElevated
Name "${PRODUCTNAME}"
BrandingText "${COPYRIGHT}"
@@ -409,6 +410,7 @@ FunctionEnd
; 5. Choose install directory page
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE DbxEnsureInstallAccess
!insertmacro MUI_PAGE_DIRECTORY
; 6. Start menu shortcut page
@@ -510,7 +512,99 @@ LangString dbxWin7InstallerRequired ${LANG_SIMPCHINESE} "此安装包不支持 W
LangString dbxWin7InstallerRequired ${LANG_TRADCHINESE} "此安裝套件不支援 Windows 7 或 Windows Server 2012 R2。$\r$\n$\r$\n請改用 Windows 7 / Server 2012 R2 專用套件。$\r$\n$\r$\n是否立即開啟下載網址?"
FileErrorText "$(dbxFileWriteError)" "$(dbxFileWriteErrorNoIgnore)"
LangString dbxElevationFailed ${LANG_ENGLISH} "Administrator permission is required to install DBX in this folder. Permission was denied or the elevated installer could not be started."
LangString dbxElevationFailed ${LANG_SIMPCHINESE} "安装到此目录需要管理员权限。授权已被拒绝,或无法启动提权后的安装程序。"
LangString dbxElevationFailed ${LANG_TRADCHINESE} "安裝到此目錄需要系統管理員權限。授權已被拒絕,或無法啟動提升權限後的安裝程式。"
LangString dbxElevationUserMismatch ${LANG_ENGLISH} "Please approve elevation using the same Windows account that started this installer, so DBX keeps its installation and shortcuts in the correct user profile."
LangString dbxElevationUserMismatch ${LANG_SIMPCHINESE} "请使用启动安装程序的同一个 Windows 账户授权提权,以确保 DBX 的安装信息和快捷方式保留在正确的用户配置中。"
LangString dbxElevationUserMismatch ${LANG_TRADCHINESE} "請使用啟動安裝程式的同一個 Windows 帳戶授權提升權限,以確保 DBX 的安裝資訊和捷徑保留在正確的使用者設定中。"
; Probe without truncating the old executable. Only ACCESS_DENIED requests UAC;
; sharing violations still go through CheckIfAppIsRunning and normal retry UI.
Function DbxEnsureInstallAccess
Push $0
Push $1
Push $2
Push $3
${If} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe"
System::Call 'kernel32::CreateFileW(w "$INSTDIR\${MAINBINARYNAME}.exe", i 0x40000000, i 7, p 0, i 3, i 0, p 0) p .r0 ?e'
Pop $1
${If} $0 != -1
System::Call 'kernel32::CloseHandle(p r0)'
${ElseIf} $1 = 5
Goto dbx_elevate
${EndIf}
${EndIf}
; For a new destination, test the nearest existing parent directory.
StrCpy $2 $INSTDIR
dbx_find_parent:
${IfNot} ${FileExists} "$2\*.*"
${GetParent} "$2" $3
${If} $3 == ""
${OrIf} $3 == $2
Goto dbx_access_done
${EndIf}
StrCpy $2 $3
Goto dbx_find_parent
${EndIf}
System::Call 'kernel32::GetTempFileNameW(w r2, w "dbx", i 0, w .r3) i .r0 ?e'
Pop $1
${If} $0 != 0
Delete "$3"
${ElseIf} $1 = 5
Goto dbx_elevate
${EndIf}
Goto dbx_access_done
dbx_elevate:
; Never loop when elevation has already been attempted or cannot help.
System::Call 'shell32::IsUserAnAdmin() i .r0'
${If} $0 != 0
${OrIf} $DbxElevated = 1
Goto dbx_access_done
${EndIf}
${GetParameters} $2
StrCpy $3 $PROFILE
; /D must be last and unquoted (NSIS consumes the rest of the command).
; Internal flags precede /ARGS so application arguments stay intact.
ClearErrors
ExecShell "runas" "$EXEPATH" '/DBX_ELEVATED /DBX_PROFILE="$3" /DBX_LANG=$LANGUAGE $2 /D=$INSTDIR'
${If} ${Errors}
MessageBox MB_OK|MB_ICONSTOP "$(dbxElevationFailed)" /SD IDOK
SetErrorLevel 740
Pop $3
Pop $2
Pop $1
Pop $0
Abort
${EndIf}
SetErrorLevel 0
Quit
dbx_access_done:
Pop $3
Pop $2
Pop $1
Pop $0
FunctionEnd
Function .onInit
${GetOptions} $CMDLINE "/DBX_ELEVATED" $DbxElevated
${IfNot} ${Errors}
StrCpy $DbxElevated 1
${GetOptions} $CMDLINE "/DBX_LANG=" $0
StrCpy $LANGUAGE $0
; currentUser installs must not migrate registry entries to an account
; entered in the UAC credential dialog.
${GetOptions} $CMDLINE "/DBX_PROFILE=" $0
${If} $0 != $PROFILE
MessageBox MB_OK|MB_ICONSTOP "$(dbxElevationUserMismatch)" /SD IDOK
SetErrorLevel 740
Quit
${EndIf}
${EndIf}
${GetOptions} $CMDLINE "/P" $PassiveMode
${IfNot} ${Errors}
StrCpy $PassiveMode 1
@@ -527,7 +621,9 @@ Function .onInit
${EndIf}
!if "${DISPLAYLANGUAGESELECTOR}" == "true"
!insertmacro MUI_LANGDLL_DISPLAY
${If} $DbxElevated <> 1
!insertmacro MUI_LANGDLL_DISPLAY
${EndIf}
!endif
; Tauri renders fixedRuntime (and skip) as an empty install mode in NSIS;
@@ -580,6 +676,8 @@ Function .onInit
!if "${INSTALLMODE}" == "both"
!insertmacro MULTIUSER_INIT
!endif
; Updaters skip the directory page. Check before any uninstall or extraction.
Call DbxEnsureInstallAccess
FunctionEnd