Refactor macOS and Windows installer handling to improve progress tracking and error reporting. Introduce functions for parsing installer output and update UI to conditionally display progress based on installation phase.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
This commit is contained in:
parent
6fe30f680f
commit
478010ea5b
@ -166,45 +166,53 @@ const downloadArtifact = async (artifact, destinationPath, webContents) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getMacAppPath = (app) => {
|
||||
const executablePath = app.getPath('exe')
|
||||
const appPathIndex = executablePath.indexOf('.app/')
|
||||
|
||||
if (appPathIndex === -1) return null
|
||||
return executablePath.slice(0, appPathIndex + 4)
|
||||
const restartApp = (app) => {
|
||||
console.log('[app-update] restarting app')
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
}
|
||||
|
||||
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`
|
||||
|
||||
const writeInstallerLog = async (app, { installerPath, stdout, stderr, error }) => {
|
||||
const downloadsDir = app.getPath('downloads')
|
||||
const timestamp = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-')
|
||||
const logPath = path.join(
|
||||
downloadsDir,
|
||||
`farmcontrol-update-install-${timestamp}.log`
|
||||
)
|
||||
const parseMacInstallerProgress = (output) => {
|
||||
const lines = String(output || '').split('\n')
|
||||
let percent = null
|
||||
let message = 'Installing update...'
|
||||
|
||||
const sections = [
|
||||
`Timestamp: ${new Date().toISOString()}`,
|
||||
`Installer: ${installerPath}`,
|
||||
`Status: ${error ? 'failed' : 'success'}`,
|
||||
'',
|
||||
'--- stdout ---',
|
||||
stdout || '(empty)',
|
||||
'',
|
||||
'--- stderr ---',
|
||||
stderr || '(empty)'
|
||||
]
|
||||
|
||||
if (error) {
|
||||
sections.push('', '--- error ---', error.stack || error.message || String(error))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(logPath, `${sections.join('\n')}\n`, 'utf8')
|
||||
console.log('[app-update] installer log written to:', logPath)
|
||||
return logPath
|
||||
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 getInstallErrorMessage = (error, output = '') => {
|
||||
const combined = `${output}\n${error?.message || ''}`.trim()
|
||||
|
||||
@ -223,31 +231,23 @@ const getInstallErrorMessage = (error, output = '') => {
|
||||
return combined || 'Failed to install update.'
|
||||
}
|
||||
|
||||
const buildMacInstallScript = (app, installerPath) => {
|
||||
const appPath = getMacAppPath(app)
|
||||
const relaunchCommand = appPath
|
||||
? `open ${quoteShellArg(appPath)}`
|
||||
: `open ${quoteShellArg(app.getPath('exe'))}`
|
||||
|
||||
return `sleep 2 && /usr/sbin/installer -pkg ${quoteShellArg(
|
||||
const buildMacInstallScript = (installerPath) =>
|
||||
`sleep 2 && /usr/sbin/installer -pkg ${quoteShellArg(
|
||||
installerPath
|
||||
)} -target / && ${relaunchCommand}`
|
||||
}
|
||||
)} -target / -verboseR`
|
||||
|
||||
const buildWindowsInstallCommand = (app, installerPath) => ({
|
||||
const buildWindowsInstallCommand = (installerPath) => ({
|
||||
command: 'cmd.exe',
|
||||
args: [
|
||||
'/d',
|
||||
'/s',
|
||||
'/c',
|
||||
`timeout /t 2 /nobreak >NUL && msiexec.exe /i "${installerPath}" /qn /norestart && start "" "${app.getPath(
|
||||
'exe'
|
||||
)}"`
|
||||
`timeout /t 2 /nobreak >NUL && msiexec.exe /i "${installerPath}" /qn /norestart`
|
||||
]
|
||||
})
|
||||
|
||||
const launchMacInstaller = (app, installerPath, webContents) => {
|
||||
const installScript = buildMacInstallScript(app, installerPath)
|
||||
const installScript = buildMacInstallScript(installerPath)
|
||||
const promptName = 'farmcontrol'
|
||||
|
||||
console.log('[app-update] launching macOS installer:', {
|
||||
@ -258,7 +258,7 @@ const launchMacInstaller = (app, installerPath, webContents) => {
|
||||
|
||||
sendProgress(webContents, {
|
||||
phase: 'installing',
|
||||
percent: 100,
|
||||
percent: 0,
|
||||
message:
|
||||
'Installing update. Enter your Mac password when prompted, then Farm Control will restart automatically.'
|
||||
})
|
||||
@ -273,15 +273,6 @@ const launchMacInstaller = (app, installerPath, webContents) => {
|
||||
if (stdout) console.log('[app-update] installer stdout:', stdout)
|
||||
if (stderr) console.error('[app-update] installer stderr:', stderr)
|
||||
|
||||
void writeInstallerLog(app, {
|
||||
installerPath,
|
||||
stdout,
|
||||
stderr,
|
||||
error
|
||||
}).catch((logError) => {
|
||||
console.error('[app-update] failed to write installer log:', logError)
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('[app-update] installer error:', error)
|
||||
const message = getInstallErrorMessage(error, output)
|
||||
@ -294,6 +285,24 @@ const launchMacInstaller = (app, installerPath, webContents) => {
|
||||
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()
|
||||
})
|
||||
@ -301,7 +310,7 @@ const launchMacInstaller = (app, installerPath, webContents) => {
|
||||
}
|
||||
|
||||
const launchWindowsInstaller = (app, installerPath, webContents) => {
|
||||
const { command, args } = buildWindowsInstallCommand(app, installerPath)
|
||||
const { command, args } = buildWindowsInstallCommand(installerPath)
|
||||
|
||||
console.log('[app-update] launching installer:', {
|
||||
installerPath,
|
||||
@ -317,66 +326,81 @@ const launchWindowsInstaller = (app, installerPath, webContents) => {
|
||||
message: 'Installing update. Farm Control will restart automatically.'
|
||||
})
|
||||
|
||||
let installerOutput = ''
|
||||
return new Promise((resolve, reject) => {
|
||||
let installerOutput = ''
|
||||
|
||||
const installerProcess = spawn(command, args, {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
installerProcess.stdout?.on('data', (data) => {
|
||||
const text = data.toString()
|
||||
installerOutput += text
|
||||
console.log('[app-update] installer stdout:', text)
|
||||
})
|
||||
|
||||
installerProcess.stderr?.on('data', (data) => {
|
||||
const text = data.toString()
|
||||
installerOutput += text
|
||||
console.error('[app-update] installer stderr:', text)
|
||||
})
|
||||
|
||||
installerProcess.on('spawn', () => {
|
||||
console.log('[app-update] installer spawned, pid:', installerProcess.pid)
|
||||
})
|
||||
|
||||
installerProcess.on('error', (error) => {
|
||||
console.error('[app-update] installer spawn error:', error)
|
||||
sendProgress(webContents, {
|
||||
phase: 'error',
|
||||
percent: null,
|
||||
message: error?.message || 'Failed to start update installer.'
|
||||
})
|
||||
})
|
||||
|
||||
installerProcess.on('exit', (code, signal) => {
|
||||
console.log('[app-update] installer exited:', {
|
||||
code,
|
||||
signal,
|
||||
output: installerOutput
|
||||
const installerProcess = spawn(command, args, {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
if (code !== 0) {
|
||||
installerProcess.stdout?.on('data', (data) => {
|
||||
const text = data.toString()
|
||||
installerOutput += text
|
||||
console.log('[app-update] installer stdout:', text)
|
||||
})
|
||||
|
||||
installerProcess.stderr?.on('data', (data) => {
|
||||
const text = data.toString()
|
||||
installerOutput += text
|
||||
console.error('[app-update] installer stderr:', text)
|
||||
})
|
||||
|
||||
installerProcess.on('spawn', () => {
|
||||
console.log('[app-update] installer spawned, pid:', installerProcess.pid)
|
||||
})
|
||||
|
||||
installerProcess.on('error', (error) => {
|
||||
console.error('[app-update] installer spawn error:', error)
|
||||
sendProgress(webContents, {
|
||||
phase: 'error',
|
||||
percent: null,
|
||||
message: getInstallErrorMessage(null, installerOutput)
|
||||
message: error?.message || 'Failed to start update installer.'
|
||||
})
|
||||
}
|
||||
})
|
||||
reject(error)
|
||||
})
|
||||
|
||||
installerProcess.unref()
|
||||
installerProcess.on('exit', (code, signal) => {
|
||||
console.log('[app-update] installer exited:', {
|
||||
code,
|
||||
signal,
|
||||
output: installerOutput
|
||||
})
|
||||
|
||||
if (code !== 0) {
|
||||
const message = getInstallErrorMessage(null, installerOutput)
|
||||
sendProgress(webContents, {
|
||||
phase: 'error',
|
||||
percent: null,
|
||||
message
|
||||
})
|
||||
reject(new Error(message))
|
||||
return
|
||||
}
|
||||
|
||||
sendProgress(webContents, {
|
||||
phase: 'installing',
|
||||
percent: 100,
|
||||
message: 'Installation complete. Restarting Farm Control...'
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
|
||||
installerProcess.unref()
|
||||
})
|
||||
}
|
||||
|
||||
const launchInstallerAndQuit = async (app, installerPath, webContents) => {
|
||||
if (process.platform === 'darwin') {
|
||||
await launchMacInstaller(app, installerPath, webContents)
|
||||
restartApp(app)
|
||||
return
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
launchWindowsInstaller(app, installerPath, webContents)
|
||||
await launchWindowsInstaller(app, installerPath, webContents)
|
||||
restartApp(app)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,8 @@ const AppUpdateProgress = ({ progress, update }) => {
|
||||
const message = progress?.message || 'Preparing update'
|
||||
const isInstalling = phase === 'installing'
|
||||
const isError = phase === 'error'
|
||||
const showProgress =
|
||||
!isError && (!isInstalling || typeof progress?.percent === 'number')
|
||||
|
||||
return (
|
||||
<Flex vertical gap='middle'>
|
||||
@ -59,13 +61,13 @@ const AppUpdateProgress = ({ progress, update }) => {
|
||||
icon={isInstalling ? undefined : <LoadingOutlined />}
|
||||
message={
|
||||
isInstalling
|
||||
? 'The app will close while the installer runs, then reopen automatically.'
|
||||
? message
|
||||
: `Downloading ${artifactName}`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isInstalling && !isError && (
|
||||
{showProgress && (
|
||||
<Progress
|
||||
percent={percent}
|
||||
status={getProgressStatus(phase)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user