From 4fdf3a233da1544202b14282a92a9f3abc94aba8 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 3 Aug 2026 23:54:23 +0100 Subject: [PATCH] Enhance Windows installer with improved logging and installation progress tracking - Added support for silent installation mode. - Implemented detailed progress logging during installation phases, including status updates and percentage completion. - Refactored the handling of previous installations to ensure smoother updates and in-app upgrade processes. - Updated error handling for installation failures, providing clearer feedback to users. --- packaging/windows/farmcontrol.nsi | 35 ++- packaging/windows/installer.nsh | 147 +++++++++++- src/desktop/appupdate.js | 4 +- src/desktop/winappupdate.js | 364 +++++++----------------------- 4 files changed, 268 insertions(+), 282 deletions(-) diff --git a/packaging/windows/farmcontrol.nsi b/packaging/windows/farmcontrol.nsi index f3e27fc..49884d5 100644 --- a/packaging/windows/farmcontrol.nsi +++ b/packaging/windows/farmcontrol.nsi @@ -23,6 +23,7 @@ OutFile "${OUTFILE}" InstallDir "$LOCALAPPDATA\Programs\Farm Control" InstallDirRegKey HKCU "Software\Tom Butcher\Farm Control" "InstallDir" RequestExecutionLevel user +SilentInstall normal !define MUI_ABORTWARNING @@ -45,19 +46,49 @@ BrandingText "Farm Control v${VERSION}-b${BUILD_NUMBER} Installer" !insertmacro MUI_LANGUAGE "English" Function .onInit + !insertmacro initProgressLog + ${If} ${Silent} SetAutoClose true ${EndIf} - !insertmacro uninstallPreviousFarmControl + + !insertmacro progressPhase "Starting installation" + !insertmacro progressStatus "Starting Farm Control installation..." + !insertmacro progressPercent 5 + + ; In-app updates keep the running process alive for progress UI. + ; Interactive / silent fresh installs still remove the previous version first. + ${If} $IsInAppUpdate == "1" + !insertmacro prepareInPlaceUpdate + !insertmacro progressPercent 15 + ${Else} + !insertmacro uninstallPreviousFarmControl + !insertmacro progressPercent 20 + ${EndIf} +FunctionEnd + +Function .onInstFailed + !insertmacro progressFailure "Installation failed." FunctionEnd Section "Farm Control" SecMain SectionIn RO + + !insertmacro progressPhase "Copying application files" + !insertmacro progressStatus "Copying application files..." + !insertmacro progressPercent 35 + SetOutPath $INSTDIR + SetOverwrite try File /r "${APP_SOURCE_DIR}\*.*" + !insertmacro progressPercent 75 !insertmacro customInstall + !insertmacro progressPhase "Finalizing installation" + !insertmacro progressStatus "Writing uninstall information..." + !insertmacro progressPercent 90 + WriteRegStr HKCU "Software\Tom Butcher\Farm Control" "InstallDir" $INSTDIR WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \ "DisplayName" "Farm Control" @@ -73,6 +104,8 @@ Section "Farm Control" SecMain "NoRepair" 1 WriteUninstaller "$INSTDIR\Uninstall.exe" + + !insertmacro progressSuccess SectionEnd Section "Uninstall" diff --git a/packaging/windows/installer.nsh b/packaging/windows/installer.nsh index 6756d0b..3701bc9 100644 --- a/packaging/windows/installer.nsh +++ b/packaging/windows/installer.nsh @@ -1,8 +1,131 @@ -!macro quitFarmControl +!include "FileFunc.nsh" +!insertmacro GetParameters +!insertmacro GetOptions + +Var ProgressLogFile +Var IsInAppUpdate + +!macro initProgressLog + StrCpy $ProgressLogFile "" + StrCpy $IsInAppUpdate "0" + + ${GetParameters} $R9 + + ClearErrors + ${GetOptions} $R9 "/UPDATE" $R8 + ${IfNot} ${Errors} + StrCpy $IsInAppUpdate "1" + ${EndIf} + + ; Prefer env var so paths with spaces are reliable; /LOG= remains supported. + ReadEnvStr $ProgressLogFile "FARMCONTROL_INSTALL_LOG" + ${If} $ProgressLogFile == "" + ClearErrors + ${GetOptions} $R9 "/LOG=" $ProgressLogFile + ${If} ${Errors} + StrCpy $ProgressLogFile "" + ${EndIf} + ${EndIf} + + ${If} $ProgressLogFile != "" + ; Strip surrounding quotes from /LOG="C:\path with spaces\log.log" + StrCpy $R8 $ProgressLogFile 1 + ${If} $R8 == '"' + StrCpy $ProgressLogFile $ProgressLogFile "" 1 + StrLen $R8 $ProgressLogFile + IntOp $R8 $R8 - 1 + ${If} $R8 > 0 + StrCpy $ProgressLogFile $ProgressLogFile $R8 + ${EndIf} + ${EndIf} + + ; Truncate any previous log so progress starts clean. + Push $0 + FileOpen $0 "$ProgressLogFile" w + ${If} $0 != "" + FileClose $0 + ${EndIf} + Pop $0 + ${EndIf} +!macroend + +; Open/append/close each line so the updater can read the log concurrently +; (avoids an exclusive lock for the whole install; LogEx SHARE_READ is an alternative). +!macro progressLog line + DetailPrint "${line}" + ${If} $ProgressLogFile != "" + Push $0 + FileOpen $0 "$ProgressLogFile" a + ${If} $0 != "" + FileSeek $0 0 END + FileWrite $0 "${line}$\r$\n" + FileClose $0 + ${EndIf} + Pop $0 + ${EndIf} +!macroend + +!macro progressPercent percent + !insertmacro progressLog "installer:%${percent}" +!macroend + +!macro progressPhase phase + !insertmacro progressLog "installer:PHASE:${phase}" +!macroend + +!macro progressStatus status + !insertmacro progressLog "installer:STATUS:${status}" +!macroend + +!macro progressSuccess + !insertmacro progressPercent 100 + !insertmacro progressStatus "Installation complete. Restarting Farm Control..." + !insertmacro progressLog "installer: The install was successful." +!macroend + +!macro progressFailure message + !insertmacro progressLog "installer:STATUS:${message}" + !insertmacro progressLog "installer: The install failed." +!macroend + +!macro quitFarmControl + !insertmacro progressPhase "Stopping Farm Control" + !insertmacro progressStatus "Stopping running Farm Control processes..." DetailPrint "Stopping running Farm Control processes..." ExecWait 'taskkill /F /IM launcher.exe /T' $R0 !macroend +; Windows allows renaming open executables; move locked binaries aside so File can replace them. +!macro moveAsideIfPresent filePath + ${If} ${FileExists} "${filePath}" + Delete "${filePath}.old" + ClearErrors + Rename "${filePath}" "${filePath}.old" + ClearErrors + ${EndIf} +!macroend + +; Prefer renaming the whole bin dir (works even with open files inside). +!macro prepareInPlaceUpdate + !insertmacro progressPhase "Preparing update" + !insertmacro progressStatus "Preparing files for update..." + + ${If} ${FileExists} "$INSTDIR\bin.old" + RMDir /r "$INSTDIR\bin.old" + ${EndIf} + + ${If} ${FileExists} "$INSTDIR\bin" + ClearErrors + Rename "$INSTDIR\bin" "$INSTDIR\bin.old" + ${If} ${Errors} + !insertmacro moveAsideIfPresent "$INSTDIR\bin\launcher.exe" + !insertmacro moveAsideIfPresent "$INSTDIR\bin\bun.exe" + !insertmacro moveAsideIfPresent "$INSTDIR\bin\bspatch.exe" + !insertmacro moveAsideIfPresent "$INSTDIR\bin\zig-zstd.exe" + ${EndIf} + ${EndIf} +!macroend + !macro uninstallPreviousFarmControl ClearErrors ReadRegStr $R0 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" "UninstallString" @@ -34,10 +157,13 @@ run_uninstall: IfFileExists $R0 0 done_uninstall + !insertmacro progressPhase "Removing previous version" + !insertmacro progressStatus "Removing previous Farm Control installation..." DetailPrint "Removing previous Farm Control installation..." !insertmacro quitFarmControl - ExecWait '$R0 /S' $R1 + ExecWait '"$R0" /S' $R1 DetailPrint "Previous installation removed (exit code: $R1)" + !insertmacro progressLog "installer:STATUS:Previous installation removed" done_uninstall: !macroend @@ -66,7 +192,18 @@ RMDir "$SMPROGRAMS\Farm Control" !macroend +!macro cleanupUpdateBackups + ; Best-effort; may fail while the previous process still holds handles. + RMDir /r "$INSTDIR\bin.old" + Delete "$INSTDIR\bin\launcher.exe.old" + Delete "$INSTDIR\bin\bun.exe.old" + Delete "$INSTDIR\bin\bspatch.exe.old" + Delete "$INSTDIR\bin\zig-zstd.exe.old" +!macroend + !macro customInstall + !insertmacro progressPhase "Configuring Farm Control" + !insertmacro progressStatus "Registering farmcontrol URI handler..." DetailPrint "Register farmcontrol URI Handler" DeleteRegKey HKCU "Software\Classes\farmcontrol" WriteRegStr HKCU "Software\Classes\farmcontrol" "" "URL:farmcontrol" @@ -76,9 +213,15 @@ WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open" "" "" WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open\command" "" '"$INSTDIR\bin\bun.exe" "$INSTDIR\bin\deeplink.js" "%1"' + !insertmacro progressStatus "Creating shortcuts..." DetailPrint "Creating shortcuts" !insertmacro createDesktopShortcut !insertmacro createStartMenuShortcut + + ; Don't remove bin.old during an in-app update — the running process still uses it. + ${If} $IsInAppUpdate != "1" + !insertmacro cleanupUpdateBackups + ${EndIf} !macroend !macro customUnInstall diff --git a/src/desktop/appupdate.js b/src/desktop/appupdate.js index 214115b..93084a1 100644 --- a/src/desktop/appupdate.js +++ b/src/desktop/appupdate.js @@ -15,7 +15,7 @@ const SUPPORTED_TARGETS = { osMatchers: ["darwin", "mac", "macos", "osx"], }, win32: { - extension: ".msi", + extension: ".exe", osMatchers: ["win32", "win", "windows"], }, }; @@ -107,7 +107,7 @@ const getInstallErrorMessage = (error, output = "") => { /forbidden by system policy/i.test(combined) || /Non-assigned apps are disabled/i.test(combined) ) { - return "Update installation was blocked by Windows Installer policy."; + return "Update installation was blocked by system policy."; } return combined || "Failed to install update."; diff --git a/src/desktop/winappupdate.js b/src/desktop/winappupdate.js index 8b7e908..c6bebac 100644 --- a/src/desktop/winappupdate.js +++ b/src/desktop/winappupdate.js @@ -2,143 +2,53 @@ import { spawn } from 'child_process' import { promises as fs } from 'fs' import os from 'os' import path from 'path' -import process from 'process' -const MSI_OLE_HEADER = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]) -const DEBUG_PREFIX = '[app-update][win-progress]' +const PE_MZ_HEADER = Buffer.from([0x4d, 0x5a]) // "MZ" const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) -const debugLog = () => {} - -const decodeMsiLogBuffer = (buffer) => { - if (!buffer?.length) return '' - - if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) { - debugLog('decoded MSI log as UTF-16 LE (BOM)') - return buffer.subarray(2).toString('utf16le') - } - - const sample = buffer.subarray(0, Math.min(buffer.length, 64)) - const looksUtf16 = - sample.length >= 4 && - sample.filter((byte) => byte === 0).length > sample.length / 4 - - if (looksUtf16) { - debugLog('decoded MSI log as UTF-16 LE (heuristic)') - return buffer.toString('utf16le') - } - - debugLog('decoded MSI log as UTF-8') - return buffer.toString('utf8') -} - -const formatMsiActionName = (actionName) => { - const humanized = String(actionName) - .replace(/([a-z])([A-Z])/g, '$1 $2') - .replace(/_/g, ' ') - .toLowerCase() - .trim() - - if (!humanized) return 'Installing update...' - - return `${humanized.charAt(0).toUpperCase()}${humanized.slice(1)}...` -} - const parseWindowsInstallerProgress = (output) => { const lines = String(output || '').split(/\r?\n/) let percent = null let message = 'Installing update...' - let totalTicks = 0 - let currentTicks = 0 - let actionStarts = 0 - let actionEnds = 0 - const matchedLines = [] for (const line of lines) { - const actionStart = line.match(/^Action start \d{2}:\d{2}:\d{2}: (.+?)\./) - if (actionStart) { - actionStarts += 1 - message = formatMsiActionName(actionStart[1]) - matchedLines.push(`action-start:${actionStart[1]}`) - } - - const doingAction = line.match(/Doing action:\s*(.+)$/) - if (doingAction && !actionStart) { - message = formatMsiActionName(doingAction[1]) - matchedLines.push(`doing-action:${doingAction[1]}`) - } - - if (/^Action ended \d{2}:\d{2}:\d{2}: .+?\. Return value \d+\./.test(line)) { - actionEnds += 1 - matchedLines.push('action-ended') - } - - const progressReset = line.match(/^\s*0\s+(\d+)\s+0(?:\s+\d+)?\s*$/) - if (progressReset) { - totalTicks = Number.parseInt(progressReset[1], 10) || 0 - currentTicks = 0 - matchedLines.push(`progress-reset:${totalTicks}`) - } - - const progressIncrement = line.match(/^\s*2\s+(\d+)\s*$/) - if (progressIncrement) { - currentTicks += Number.parseInt(progressIncrement[1], 10) || 0 - matchedLines.push(`progress-increment:${progressIncrement[1]}`) - } - - const progressAddition = line.match(/^\s*3\s+(\d+)\s*$/) - if (progressAddition) { - totalTicks += Number.parseInt(progressAddition[1], 10) || 0 - matchedLines.push(`progress-addition:${progressAddition[1]}`) - } - - if (/Installation success or error status:\s*0\b/.test(line)) { - percent = 100 - message = 'Installation complete. Restarting Farm Control...' - matchedLines.push('install-success') + if (line.startsWith('installer:PHASE:')) { + message = line.slice('installer:PHASE:'.length).trim() || message + } else if (line.startsWith('installer:STATUS:')) { + const status = line.slice('installer:STATUS:'.length).trim() + if (status) message = status + } else if (line.startsWith('installer:%')) { + const value = Number.parseFloat(line.slice('installer:%'.length)) + if (Number.isFinite(value)) { + percent = Math.min(100, Math.round(value <= 1 ? value * 100 : value)) + } + } else if ( + line.startsWith('installer: ') && + !line.startsWith('installer:PHASE:') && + !line.startsWith('installer:STATUS:') && + !line.startsWith('installer:%') + ) { + const text = line.slice('installer: '.length).trim() + if (text) message = text } } - if (percent !== 100) { - if (totalTicks > 0) { - percent = Math.min(99, Math.round((currentTicks / totalTicks) * 100)) - } else if (actionStarts > 0) { - percent = Math.min( - 95, - Math.max(5, Math.round((actionEnds / actionStarts) * 90)) - ) - } - } - - return { - percent, - message, - stats: { - lineCount: lines.length, - actionStarts, - actionEnds, - totalTicks, - currentTicks, - matchedLines: matchedLines.slice(-8) - } - } + return { percent, message } } const isWindowsInstallSuccessful = (output) => - /Installation success or error status:\s*0\b/.test(output) || - /MainEngineThread is returning 0\b/.test(output) + /installer: The install was successful\./i.test(output) const isWindowsInstallFailed = (output) => - /Installation success or error status:\s*[1-9]\d*\b/.test(output) || - /MainEngineThread is returning [1-9]\d*\b/.test(output) + /installer: The install failed/i.test(output) -const isValidMsiPackage = async (filePath) => { +const isValidNsisInstaller = async (filePath) => { const handle = await fs.open(filePath, 'r') try { - const header = Buffer.alloc(MSI_OLE_HEADER.length) + const header = Buffer.alloc(PE_MZ_HEADER.length) await handle.read(header, 0, header.length, 0) - return header.equals(MSI_OLE_HEADER) + return header.equals(PE_MZ_HEADER) } finally { await handle.close() } @@ -158,7 +68,6 @@ export const prepareInstallerPath = async (installerPath) => { const stablePath = path.join(updateDir, fileName) await fs.copyFile(installerPath, stablePath) - // Resolve to a canonical long path. Short 8.3 paths (e.g. ADMINI~1) break msiexec. const resolvedPath = await fs.realpath(stablePath) const stats = await fs.stat(resolvedPath) @@ -166,9 +75,9 @@ export const prepareInstallerPath = async (installerPath) => { throw new Error('Update installer file is missing or empty.') } - if (!(await isValidMsiPackage(resolvedPath))) { + if (!(await isValidNsisInstaller(resolvedPath))) { throw new Error( - 'Downloaded update is not a valid Windows Installer package. The file may be corrupted or incomplete.' + 'Downloaded update is not a valid Windows installer. The file may be corrupted or incomplete.' ) } @@ -177,102 +86,53 @@ export const prepareInstallerPath = async (installerPath) => { const startWindowsInstallerProgressWatch = (logPath, sendProgress) => { let installerOutput = '' - let lastLogSize = 0 + let offset = 0 let lastPercent = null let lastMessage = null - let pollCount = 0 const poll = async () => { - pollCount += 1 - try { const stat = await fs.stat(logPath) - if (stat.size === 0) { - debugLog(`poll #${pollCount}: log exists but is empty`, { logPath }) - return - } + if (stat.size <= offset) return - if (stat.size === lastLogSize) { - debugLog(`poll #${pollCount}: no new log data`, { - logPath, - size: stat.size - }) - return - } - - const buffer = Buffer.alloc(stat.size) const handle = await fs.open(logPath, 'r') try { - await handle.read(buffer, 0, stat.size, 0) + const buffer = Buffer.alloc(stat.size - offset) + await handle.read(buffer, 0, buffer.length, offset) + offset = stat.size + installerOutput += buffer.toString('utf8') + + const { percent, message } = parseWindowsInstallerProgress(installerOutput) + const resolvedMessage = message || 'Installing update...' + + if (percent !== lastPercent || resolvedMessage !== lastMessage) { + lastPercent = percent + lastMessage = resolvedMessage + sendProgress({ + phase: 'installing', + percent, + message: resolvedMessage + }) + } } finally { await handle.close() } - - lastLogSize = stat.size - installerOutput = decodeMsiLogBuffer(buffer) - - const { percent, message, stats } = - parseWindowsInstallerProgress(installerOutput) - const resolvedPercent = percent ?? lastPercent ?? 0 - const resolvedMessage = message || 'Installing update...' - - debugLog(`poll #${pollCount}: parsed installer log`, { - logPath, - size: stat.size, - textLength: installerOutput.length, - preview: installerOutput.slice(0, 240).replace(/\s+/g, ' '), - parsed: stats, - resolvedPercent, - resolvedMessage - }) - - if ( - resolvedPercent !== lastPercent || - resolvedMessage !== lastMessage - ) { - debugLog(`poll #${pollCount}: sending progress update`, { - percent: resolvedPercent, - message: resolvedMessage - }) - - lastPercent = resolvedPercent - lastMessage = resolvedMessage - sendProgress( { - phase: 'installing', - percent: resolvedPercent, - message: resolvedMessage - }) - } else { - debugLog(`poll #${pollCount}: progress unchanged, skipping UI update`, { - percent: resolvedPercent, - message: resolvedMessage - }) - } } catch (error) { - if (error?.code === 'ENOENT') { - debugLog(`poll #${pollCount}: log file not created yet`, { logPath }) - return + if (error?.code !== 'ENOENT') { + console.error('[app-update] installer log poll error:', error) } - - console.error(`${DEBUG_PREFIX} installer log poll error:`, error) } } const intervalId = setInterval(() => { poll().catch((error) => { - console.error(`${DEBUG_PREFIX} installer log poll error:`, error) + console.error('[app-update] installer log poll error:', error) }) }, 300) return async () => { clearInterval(intervalId) await poll() - debugLog('stopped progress watch', { - logPath, - finalSize: lastLogSize, - textLength: installerOutput.length, - pollCount - }) return installerOutput } } @@ -286,13 +146,7 @@ export const launchWindowsInstaller = async ( const resolvedPath = await prepareInstallerPath(installerPath) const logPath = path.join(path.dirname(resolvedPath), 'install.log') - debugLog('prepared installer', { - installerPath, - resolvedPath, - logPath - }) - - sendProgress( { + sendProgress({ phase: 'installing', percent: 0, message: 'Installing update...' @@ -300,103 +154,71 @@ export const launchWindowsInstaller = async ( await fs.unlink(logPath).catch(() => {}) - // Allow file handles from the download/copy to settle before msiexec opens the MSI. + // Allow file handles from the download/copy to settle before the installer opens. await sleep(2000) + if (mainWindow && !mainWindow.isDestroyed?.()) { + mainWindow.focus?.() + mainWindow.show?.() + } + const stopProgressWatch = startWindowsInstallerProgressWatch( logPath, sendProgress ) return new Promise((resolve, reject) => { - let processOutput = '' - const startedAt = Date.now() + // Silent NSIS install in a child process (not a batch file). + // /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall) + // /UPDATE = in-app update (skip killing this process; overwrite in place) + // /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS) + const installerArgs = ['/S', '/UPDATE', `/LOG=${logPath}`] - const installerArgs = [ - '/i', - resolvedPath, - '/qn', - '/norestart', - 'ALLUSERS=2', - 'MSIINSTALLPERUSER=1', - 'REBOOT=ReallySuppress', - '/L*v!', - logPath - ] - - debugLog('spawning msiexec', { - args: installerArgs, - elapsedMs: Date.now() - startedAt - }) - - const installerProcess = spawn('msiexec.exe', installerArgs, { + const installerProcess = spawn(resolvedPath, installerArgs, { + detached: false, + env: { + ...process.env, + FARMCONTROL_INSTALL_LOG: logPath + }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) + let processOutput = '' + installerProcess.stdout?.on('data', (data) => { - const text = data.toString('utf16le') - processOutput += text - debugLog('msiexec stdout chunk', { - length: text.length, - preview: text.slice(0, 200) - }) + processOutput += data.toString('utf8') }) installerProcess.stderr?.on('data', (data) => { - const text = data.toString('utf16le') - processOutput += text - debugLog('msiexec stderr chunk', { - length: text.length, - preview: text.slice(0, 200) - }) - }) - - installerProcess.on('spawn', () => { - debugLog('msiexec spawned', { - pid: installerProcess.pid, - elapsedMs: Date.now() - startedAt - }) + processOutput += data.toString('utf8') }) installerProcess.on('error', async (error) => { - console.error(`${DEBUG_PREFIX} installer spawn error:`, error) const watchedOutput = await stopProgressWatch() - - debugLog('installer spawn failed', { - watchedOutputLength: watchedOutput.length, - processOutputLength: processOutput.length - }) - - const message = error?.message || 'Failed to start update installer.' - sendProgress( { + const output = watchedOutput || processOutput + console.error('[app-update] installer error:', error) + const message = getInstallErrorMessage(error, output) + sendProgress({ phase: 'error', percent: null, message }) - reject(error) + reject(new Error(message)) }) - installerProcess.on('exit', async (code, signal) => { + installerProcess.on('exit', async (code) => { const watchedOutput = await stopProgressWatch() const output = watchedOutput || processOutput - const finalParse = parseWindowsInstallerProgress(output) - debugLog('msiexec exited', { - code, - signal, - elapsedMs: Date.now() - startedAt, - watchedOutputLength: watchedOutput.length, - processOutputLength: processOutput.length, - parsed: finalParse.stats, - outputPreview: output.slice(0, 500).replace(/\s+/g, ' ') - }) - - debugLog('keeping install log', { logPath }) + await fs.unlink(logPath).catch(() => {}) if (code !== 0) { - const message = getInstallErrorMessage(null, output) - sendProgress( { + const message = getInstallErrorMessage( + new Error(`Installer exited with code ${code}.`), + output + ) + sendProgress({ phase: 'error', percent: null, message @@ -405,20 +227,9 @@ export const launchWindowsInstaller = async ( return } - const succeeded = - isWindowsInstallSuccessful(output) || - (code === 0 && !isWindowsInstallFailed(output)) - - debugLog('install success evaluation', { - succeeded, - isSuccessful: isWindowsInstallSuccessful(output), - isFailed: isWindowsInstallFailed(output), - exitCode: code - }) - - if (!succeeded) { + if (isWindowsInstallFailed(output) || !isWindowsInstallSuccessful(output)) { const message = getInstallErrorMessage(null, output) - sendProgress( { + sendProgress({ phase: 'error', percent: null, message @@ -427,15 +238,14 @@ export const launchWindowsInstaller = async ( return } - const { percent, message } = finalParse + const { percent, message } = parseWindowsInstallerProgress(output) - sendProgress( { + sendProgress({ phase: 'installing', percent: percent ?? 100, message: message || 'Installation complete. Restarting Farm Control...' }) - debugLog('installer completed successfully') resolve() }) })