From cfb8c6ede10eb0ef1ea67e5eb99dcdf6394b7536 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 3 Aug 2026 12:22:21 +0100 Subject: [PATCH] Update Windows installer to use .exe extension and refactor validation logic - Changed the installer file extension from `.msi` to `.exe` in both `appupdate.js` and `winappupdate.js` to align with the new installation format. - Refactored the validation function to check for Windows executables instead of MSI packages, enhancing compatibility with the updated installer format. --- public/appupdate.js | 2 +- public/winappupdate.js | 331 +++-------------------------------- src/desktop/appupdate.js | 2 +- src/desktop/winappupdate.js | 334 ++++-------------------------------- 4 files changed, 62 insertions(+), 607 deletions(-) diff --git a/public/appupdate.js b/public/appupdate.js index 52201bc..57ed27c 100644 --- a/public/appupdate.js +++ b/public/appupdate.js @@ -15,7 +15,7 @@ const SUPPORTED_TARGETS = { osMatchers: ['darwin', 'mac', 'macos', 'osx'] }, win32: { - extension: '.msi', + extension: '.exe', osMatchers: ['win32', 'win', 'windows'] } } diff --git a/public/winappupdate.js b/public/winappupdate.js index db12610..8a64fc1 100644 --- a/public/winappupdate.js +++ b/public/winappupdate.js @@ -2,143 +2,20 @@ 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 MZ_HEADER = Buffer.from([0x4d, 0x5a]) const DEBUG_PREFIX = '[app-update][win-progress]' 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 (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) - } - } -} - -const isWindowsInstallSuccessful = (output) => - /Installation success or error status:\s*0\b/.test(output) || - /MainEngineThread is returning 0\b/.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) - -const isValidMsiPackage = async (filePath) => { +const isValidWindowsExecutable = async (filePath) => { const handle = await fs.open(filePath, 'r') try { - const header = Buffer.alloc(MSI_OLE_HEADER.length) + const header = Buffer.alloc(MZ_HEADER.length) await handle.read(header, 0, header.length, 0) - return header.equals(MSI_OLE_HEADER) + return header.equals(MZ_HEADER) } finally { await handle.close() } @@ -158,7 +35,6 @@ 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,118 +42,20 @@ const prepareInstallerPath = async (installerPath) => { throw new Error('Update installer file is missing or empty.') } - if (!(await isValidMsiPackage(resolvedPath))) { + if (!(await isValidWindowsExecutable(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.' ) } return resolvedPath } -const startWindowsInstallerProgressWatch = ( - logPath, - webContents, - sendProgress -) => { - let installerOutput = '' - let lastLogSize = 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 === 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) - } 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(webContents, { - 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 - } - - console.error(`${DEBUG_PREFIX} installer log poll error:`, error) - } - } - - const intervalId = setInterval(() => { - poll().catch((error) => { - console.error(`${DEBUG_PREFIX} 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 +const readInstallerLog = async (logPath) => { + try { + return await fs.readFile(logPath, 'utf8') + } catch { + return '' } } @@ -304,71 +82,42 @@ export const launchWindowsInstaller = async ( await fs.unlink(logPath).catch(() => {}) - // Allow file handles from the download/copy to settle before msiexec opens the MSI. await sleep(2000) - const stopProgressWatch = startWindowsInstallerProgressWatch( - logPath, - webContents, - sendProgress - ) - return new Promise((resolve, reject) => { let processOutput = '' const startedAt = Date.now() - const installerArgs = [ - '/i', - resolvedPath, - '/qn', - '/norestart', - '/L*v!', - logPath - ] + const installerArgs = ['/S', `/LOG=${logPath}`] - debugLog('spawning msiexec', { + debugLog('spawning NSIS installer', { + installerPath: resolvedPath, args: installerArgs, elapsedMs: Date.now() - startedAt }) - const installerProcess = spawn('msiexec.exe', installerArgs, { + const installerProcess = spawn(resolvedPath, installerArgs, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) 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) - }) + processOutput += data.toString('utf8') }) installerProcess.on('spawn', () => { - debugLog('msiexec spawned', { + debugLog('installer spawned', { pid: installerProcess.pid, elapsedMs: Date.now() - startedAt }) }) - installerProcess.on('error', async (error) => { + installerProcess.on('error', (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(webContents, { @@ -380,24 +129,24 @@ export const launchWindowsInstaller = async ( }) installerProcess.on('exit', async (code, signal) => { - const watchedOutput = await stopProgressWatch() - const output = watchedOutput || processOutput - const finalParse = parseWindowsInstallerProgress(output) + const logOutput = await readInstallerLog(logPath) + const output = [processOutput, logOutput].filter(Boolean).join('\n') - debugLog('msiexec exited', { + debugLog('installer exited', { code, signal, elapsedMs: Date.now() - startedAt, - watchedOutputLength: watchedOutput.length, + logOutputLength: logOutput.length, processOutputLength: processOutput.length, - parsed: finalParse.stats, outputPreview: output.slice(0, 500).replace(/\s+/g, ' ') }) debugLog('keeping install log', { logPath }) if (code !== 0) { - const message = getInstallErrorMessage(null, output) + const message = + getInstallErrorMessage(null, output) || + `Update installer failed with exit code ${code ?? 'unknown'}.` sendProgress(webContents, { phase: 'error', percent: null, @@ -407,34 +156,10 @@ 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) { - const message = getInstallErrorMessage(null, output) - sendProgress(webContents, { - phase: 'error', - percent: null, - message - }) - reject(new Error(message)) - return - } - - const { percent, message } = finalParse - sendProgress(webContents, { phase: 'installing', - percent: percent ?? 100, - message: message || 'Installation complete. Restarting Farm Control...' + percent: 100, + message: 'Installation complete. Restarting Farm Control...' }) debugLog('installer completed successfully') diff --git a/src/desktop/appupdate.js b/src/desktop/appupdate.js index bd38dff..edf7025 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"], }, }; diff --git a/src/desktop/winappupdate.js b/src/desktop/winappupdate.js index d8b516c..8e8f942 100644 --- a/src/desktop/winappupdate.js +++ b/src/desktop/winappupdate.js @@ -2,143 +2,20 @@ 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 MZ_HEADER = Buffer.from([0x4d, 0x5a]) const DEBUG_PREFIX = '[app-update][win-progress]' 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 (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) - } - } -} - -const isWindowsInstallSuccessful = (output) => - /Installation success or error status:\s*0\b/.test(output) || - /MainEngineThread is returning 0\b/.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) - -const isValidMsiPackage = async (filePath) => { +const isValidWindowsExecutable = async (filePath) => { const handle = await fs.open(filePath, 'r') try { - const header = Buffer.alloc(MSI_OLE_HEADER.length) + const header = Buffer.alloc(MZ_HEADER.length) await handle.read(header, 0, header.length, 0) - return header.equals(MSI_OLE_HEADER) + return header.equals(MZ_HEADER) } finally { await handle.close() } @@ -158,7 +35,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,114 +42,20 @@ export const prepareInstallerPath = async (installerPath) => { throw new Error('Update installer file is missing or empty.') } - if (!(await isValidMsiPackage(resolvedPath))) { + if (!(await isValidWindowsExecutable(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.' ) } return resolvedPath } -const startWindowsInstallerProgressWatch = (logPath, sendProgress) => { - let installerOutput = '' - let lastLogSize = 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 === 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) - } 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 - } - - console.error(`${DEBUG_PREFIX} installer log poll error:`, error) - } - } - - const intervalId = setInterval(() => { - poll().catch((error) => { - console.error(`${DEBUG_PREFIX} 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 +const readInstallerLog = async (logPath) => { + try { + return await fs.readFile(logPath, 'utf8') + } catch { + return '' } } @@ -292,7 +74,7 @@ export const launchWindowsInstaller = async ( logPath }) - sendProgress( { + sendProgress({ phase: 'installing', percent: 0, message: 'Installing update...' @@ -300,73 +82,45 @@ export const launchWindowsInstaller = async ( await fs.unlink(logPath).catch(() => {}) - // Allow file handles from the download/copy to settle before msiexec opens the MSI. await sleep(2000) - const stopProgressWatch = startWindowsInstallerProgressWatch( - logPath, - sendProgress - ) - return new Promise((resolve, reject) => { let processOutput = '' const startedAt = Date.now() - const installerArgs = [ - '/i', - resolvedPath, - '/qn', - '/norestart', - '/L*v!', - logPath - ] + const installerArgs = ['/S', `/LOG=${logPath}`] - debugLog('spawning msiexec', { + debugLog('spawning NSIS installer', { + installerPath: resolvedPath, args: installerArgs, elapsedMs: Date.now() - startedAt }) - const installerProcess = spawn('msiexec.exe', installerArgs, { + const installerProcess = spawn(resolvedPath, installerArgs, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) 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) - }) + processOutput += data.toString('utf8') }) installerProcess.on('spawn', () => { - debugLog('msiexec spawned', { + debugLog('installer spawned', { pid: installerProcess.pid, elapsedMs: Date.now() - startedAt }) }) - installerProcess.on('error', async (error) => { + installerProcess.on('error', (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( { + sendProgress({ phase: 'error', percent: null, message @@ -375,25 +129,25 @@ export const launchWindowsInstaller = async ( }) installerProcess.on('exit', async (code, signal) => { - const watchedOutput = await stopProgressWatch() - const output = watchedOutput || processOutput - const finalParse = parseWindowsInstallerProgress(output) + const logOutput = await readInstallerLog(logPath) + const output = [processOutput, logOutput].filter(Boolean).join('\n') - debugLog('msiexec exited', { + debugLog('installer exited', { code, signal, elapsedMs: Date.now() - startedAt, - watchedOutputLength: watchedOutput.length, + logOutputLength: logOutput.length, processOutputLength: processOutput.length, - parsed: finalParse.stats, outputPreview: output.slice(0, 500).replace(/\s+/g, ' ') }) debugLog('keeping install log', { logPath }) if (code !== 0) { - const message = getInstallErrorMessage(null, output) - sendProgress( { + const message = + getInstallErrorMessage(null, output) || + `Update installer failed with exit code ${code ?? 'unknown'}.` + sendProgress({ phase: 'error', percent: null, message @@ -402,34 +156,10 @@ 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) { - const message = getInstallErrorMessage(null, output) - sendProgress( { - phase: 'error', - percent: null, - message - }) - reject(new Error(message)) - return - } - - const { percent, message } = finalParse - - sendProgress( { + sendProgress({ phase: 'installing', - percent: percent ?? 100, - message: message || 'Installation complete. Restarting Farm Control...' + percent: 100, + message: 'Installation complete. Restarting Farm Control...' }) debugLog('installer completed successfully')