import { promises as fs } from 'fs' import { createRequire } from 'module' import path from 'path' const require = createRequire(import.meta.url) const sudo = require('@vscode/sudo-prompt') const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'` const parseMacInstallerProgress = (output) => { const lines = String(output || '').split('\n') let percent = null let message = 'Installing update...' for (const line of lines) { 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 } } return { percent, message } } const isMacInstallSuccessful = (output) => /installer: The (install|upgrade) was successful\./i.test(output) const isMacInstallFailed = (output) => /installer: The install failed/i.test(output) const buildMacInstallScript = (installerPath, logPath) => `sleep 2 && /usr/sbin/installer -pkg ${quoteShellArg( installerPath )} -target / -verboseR 2>&1 | /usr/bin/tee ${quoteShellArg(logPath)}` const startMacInstallerProgressWatch = (logPath, webContents, sendProgress) => { let installerOutput = '' let offset = 0 let lastPercent = null let lastMessage = null const poll = async () => { try { const stat = await fs.stat(logPath) if (stat.size <= offset) return const handle = await fs.open(logPath, 'r') try { 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 } = parseMacInstallerProgress(installerOutput) const resolvedMessage = message || 'Installing update...' if (percent !== lastPercent || resolvedMessage !== lastMessage) { lastPercent = percent lastMessage = resolvedMessage sendProgress(webContents, { phase: 'installing', percent, message: resolvedMessage }) } } finally { await handle.close() } } catch (error) { if (error?.code !== 'ENOENT') { console.error('[app-update] installer log poll error:', error) } } } const intervalId = setInterval(() => { poll().catch((error) => { console.error('[app-update] installer log poll error:', error) }) }, 300) return async () => { clearInterval(intervalId) await poll() return installerOutput } } export const launchMacInstaller = ( app, installerPath, webContents, { sendProgress, getInstallErrorMessage } ) => { const logPath = path.join(path.dirname(installerPath), 'install.log') const installScript = buildMacInstallScript(installerPath, logPath) const promptName = 'farmcontrol' console.log('[app-update] launching macOS installer:', { installerPath, installScript, logPath, promptName }) sendProgress(webContents, { phase: 'installing', percent: 0, message: 'Enter your Mac password when prompted.' }) app.focus({ steal: true }) app.dock?.show() const stopProgressWatch = startMacInstallerProgressWatch( logPath, webContents, sendProgress ) return new Promise((resolve, reject) => { sudo.exec(installScript, { name: promptName }, async (error, stdout, stderr) => { const watchedOutput = await stopProgressWatch() const output = `${stdout || ''}${stderr || ''}` || watchedOutput await fs.unlink(logPath).catch(() => {}) if (stdout) console.log('[app-update] installer stdout:', stdout) if (stderr) console.error('[app-update] installer stderr:', stderr) if (error) { console.error('[app-update] installer error:', error) const message = getInstallErrorMessage(error, output) sendProgress(webContents, { phase: 'error', percent: null, message }) reject(new Error(message)) return } if (isMacInstallFailed(output) || !isMacInstallSuccessful(output)) { const message = getInstallErrorMessage(null, output) sendProgress(webContents, { phase: 'error', percent: null, message }) reject(new Error(message)) return } const { percent, message } = parseMacInstallerProgress(output) sendProgress(webContents, { phase: 'installing', percent: percent ?? 100, message: message || 'Installation complete. Restarting Farm Control...' }) console.log('[app-update] installer completed successfully') resolve() }) }) }