import { spawn } from 'child_process' import { promises as fs } from 'fs' import os from 'os' import path from 'path' 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 isValidWindowsExecutable = async (filePath) => { const handle = await fs.open(filePath, 'r') try { const header = Buffer.alloc(MZ_HEADER.length) await handle.read(header, 0, header.length, 0) return header.equals(MZ_HEADER) } finally { await handle.close() } } export const prepareInstallerPath = async (installerPath) => { const fileName = path.basename(installerPath) const updateDir = path.join( os.homedir(), 'AppData', 'Local', 'FarmControl', 'Updates' ) await fs.mkdir(updateDir, { recursive: true }) const stablePath = path.join(updateDir, fileName) await fs.copyFile(installerPath, stablePath) const resolvedPath = await fs.realpath(stablePath) const stats = await fs.stat(resolvedPath) if (!stats.isFile() || stats.size === 0) { throw new Error('Update installer file is missing or empty.') } if (!(await isValidWindowsExecutable(resolvedPath))) { throw new Error( 'Downloaded update is not a valid Windows installer. The file may be corrupted or incomplete.' ) } return resolvedPath } const readInstallerLog = async (logPath) => { try { return await fs.readFile(logPath, 'utf8') } catch { return '' } } export const launchWindowsInstaller = async ( mainWindow, installerPath, webContents, { sendProgress, getInstallErrorMessage } ) => { const resolvedPath = await prepareInstallerPath(installerPath) const logPath = path.join(path.dirname(resolvedPath), 'install.log') debugLog('prepared installer', { installerPath, resolvedPath, logPath }) sendProgress({ phase: 'installing', percent: 0, message: 'Installing update...' }) await fs.unlink(logPath).catch(() => {}) await sleep(2000) return new Promise((resolve, reject) => { let processOutput = '' const startedAt = Date.now() const installerArgs = ['/S', `/LOG=${logPath}`] debugLog('spawning NSIS installer', { installerPath: resolvedPath, args: installerArgs, elapsedMs: Date.now() - startedAt }) const installerProcess = spawn(resolvedPath, installerArgs, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) installerProcess.stdout?.on('data', (data) => { processOutput += data.toString('utf8') }) installerProcess.stderr?.on('data', (data) => { processOutput += data.toString('utf8') }) installerProcess.on('spawn', () => { debugLog('installer spawned', { pid: installerProcess.pid, elapsedMs: Date.now() - startedAt }) }) installerProcess.on('error', (error) => { console.error(`${DEBUG_PREFIX} installer spawn error:`, error) const message = error?.message || 'Failed to start update installer.' sendProgress({ phase: 'error', percent: null, message }) reject(error) }) installerProcess.on('exit', async (code, signal) => { const logOutput = await readInstallerLog(logPath) const output = [processOutput, logOutput].filter(Boolean).join('\n') debugLog('installer exited', { code, signal, elapsedMs: Date.now() - startedAt, logOutputLength: logOutput.length, processOutputLength: processOutput.length, outputPreview: output.slice(0, 500).replace(/\s+/g, ' ') }) debugLog('keeping install log', { logPath }) if (code !== 0) { const message = getInstallErrorMessage(null, output) || `Update installer failed with exit code ${code ?? 'unknown'}.` sendProgress({ phase: 'error', percent: null, message }) reject(new Error(message)) return } sendProgress({ phase: 'installing', percent: 100, message: 'Installation complete. Restarting Farm Control...' }) debugLog('installer completed successfully') resolve() }) }) }