All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced macappupdate.js for macOS installer management, including progress tracking and error handling. - Added winappupdate.js for Windows installer execution with process monitoring. - Refactored appupdate.js to utilize new installer modules, enhancing code organization and maintainability. - Improved error messaging and progress reporting during installation processes.
182 lines
5.2 KiB
JavaScript
182 lines
5.2 KiB
JavaScript
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()
|
|
})
|
|
})
|
|
}
|