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 restartApp = (app) => {
|
||||||
const executablePath = app.getPath('exe')
|
console.log('[app-update] restarting app')
|
||||||
const appPathIndex = executablePath.indexOf('.app/')
|
app.relaunch()
|
||||||
|
app.exit(0)
|
||||||
if (appPathIndex === -1) return null
|
|
||||||
return executablePath.slice(0, appPathIndex + 4)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`
|
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`
|
||||||
|
|
||||||
const writeInstallerLog = async (app, { installerPath, stdout, stderr, error }) => {
|
const parseMacInstallerProgress = (output) => {
|
||||||
const downloadsDir = app.getPath('downloads')
|
const lines = String(output || '').split('\n')
|
||||||
const timestamp = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-')
|
let percent = null
|
||||||
const logPath = path.join(
|
let message = 'Installing update...'
|
||||||
downloadsDir,
|
|
||||||
`farmcontrol-update-install-${timestamp}.log`
|
|
||||||
)
|
|
||||||
|
|
||||||
const sections = [
|
for (const line of lines) {
|
||||||
`Timestamp: ${new Date().toISOString()}`,
|
if (line.startsWith('installer:PHASE:')) {
|
||||||
`Installer: ${installerPath}`,
|
message = line.slice('installer:PHASE:'.length).trim() || message
|
||||||
`Status: ${error ? 'failed' : 'success'}`,
|
} else if (line.startsWith('installer:STATUS:')) {
|
||||||
'',
|
const status = line.slice('installer:STATUS:'.length).trim()
|
||||||
'--- stdout ---',
|
if (status) message = status
|
||||||
stdout || '(empty)',
|
} else if (line.startsWith('installer:%')) {
|
||||||
'',
|
const value = Number.parseFloat(line.slice('installer:%'.length))
|
||||||
'--- stderr ---',
|
if (Number.isFinite(value)) {
|
||||||
stderr || '(empty)'
|
percent = Math.min(
|
||||||
]
|
100,
|
||||||
|
Math.round(value <= 1 ? value * 100 : value)
|
||||||
if (error) {
|
)
|
||||||
sections.push('', '--- error ---', error.stack || error.message || String(error))
|
}
|
||||||
|
} 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')
|
return { percent, message }
|
||||||
console.log('[app-update] installer log written to:', logPath)
|
|
||||||
return logPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 getInstallErrorMessage = (error, output = '') => {
|
||||||
const combined = `${output}\n${error?.message || ''}`.trim()
|
const combined = `${output}\n${error?.message || ''}`.trim()
|
||||||
|
|
||||||
@ -223,31 +231,23 @@ const getInstallErrorMessage = (error, output = '') => {
|
|||||||
return combined || 'Failed to install update.'
|
return combined || 'Failed to install update.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildMacInstallScript = (app, installerPath) => {
|
const buildMacInstallScript = (installerPath) =>
|
||||||
const appPath = getMacAppPath(app)
|
`sleep 2 && /usr/sbin/installer -pkg ${quoteShellArg(
|
||||||
const relaunchCommand = appPath
|
|
||||||
? `open ${quoteShellArg(appPath)}`
|
|
||||||
: `open ${quoteShellArg(app.getPath('exe'))}`
|
|
||||||
|
|
||||||
return `sleep 2 && /usr/sbin/installer -pkg ${quoteShellArg(
|
|
||||||
installerPath
|
installerPath
|
||||||
)} -target / && ${relaunchCommand}`
|
)} -target / -verboseR`
|
||||||
}
|
|
||||||
|
|
||||||
const buildWindowsInstallCommand = (app, installerPath) => ({
|
const buildWindowsInstallCommand = (installerPath) => ({
|
||||||
command: 'cmd.exe',
|
command: 'cmd.exe',
|
||||||
args: [
|
args: [
|
||||||
'/d',
|
'/d',
|
||||||
'/s',
|
'/s',
|
||||||
'/c',
|
'/c',
|
||||||
`timeout /t 2 /nobreak >NUL && msiexec.exe /i "${installerPath}" /qn /norestart && start "" "${app.getPath(
|
`timeout /t 2 /nobreak >NUL && msiexec.exe /i "${installerPath}" /qn /norestart`
|
||||||
'exe'
|
|
||||||
)}"`
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const launchMacInstaller = (app, installerPath, webContents) => {
|
const launchMacInstaller = (app, installerPath, webContents) => {
|
||||||
const installScript = buildMacInstallScript(app, installerPath)
|
const installScript = buildMacInstallScript(installerPath)
|
||||||
const promptName = 'farmcontrol'
|
const promptName = 'farmcontrol'
|
||||||
|
|
||||||
console.log('[app-update] launching macOS installer:', {
|
console.log('[app-update] launching macOS installer:', {
|
||||||
@ -258,7 +258,7 @@ const launchMacInstaller = (app, installerPath, webContents) => {
|
|||||||
|
|
||||||
sendProgress(webContents, {
|
sendProgress(webContents, {
|
||||||
phase: 'installing',
|
phase: 'installing',
|
||||||
percent: 100,
|
percent: 0,
|
||||||
message:
|
message:
|
||||||
'Installing update. Enter your Mac password when prompted, then Farm Control will restart automatically.'
|
'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 (stdout) console.log('[app-update] installer stdout:', stdout)
|
||||||
if (stderr) console.error('[app-update] installer stderr:', stderr)
|
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) {
|
if (error) {
|
||||||
console.error('[app-update] installer error:', error)
|
console.error('[app-update] installer error:', error)
|
||||||
const message = getInstallErrorMessage(error, output)
|
const message = getInstallErrorMessage(error, output)
|
||||||
@ -294,6 +285,24 @@ const launchMacInstaller = (app, installerPath, webContents) => {
|
|||||||
return
|
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')
|
console.log('[app-update] installer completed successfully')
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
@ -301,7 +310,7 @@ const launchMacInstaller = (app, installerPath, webContents) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const launchWindowsInstaller = (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:', {
|
console.log('[app-update] launching installer:', {
|
||||||
installerPath,
|
installerPath,
|
||||||
@ -317,66 +326,81 @@ const launchWindowsInstaller = (app, installerPath, webContents) => {
|
|||||||
message: 'Installing update. Farm Control will restart automatically.'
|
message: 'Installing update. Farm Control will restart automatically.'
|
||||||
})
|
})
|
||||||
|
|
||||||
let installerOutput = ''
|
return new Promise((resolve, reject) => {
|
||||||
|
let installerOutput = ''
|
||||||
|
|
||||||
const installerProcess = spawn(command, args, {
|
const installerProcess = spawn(command, args, {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true
|
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
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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, {
|
sendProgress(webContents, {
|
||||||
phase: 'error',
|
phase: 'error',
|
||||||
percent: null,
|
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) => {
|
const launchInstallerAndQuit = async (app, installerPath, webContents) => {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
await launchMacInstaller(app, installerPath, webContents)
|
await launchMacInstaller(app, installerPath, webContents)
|
||||||
|
restartApp(app)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
launchWindowsInstaller(app, installerPath, webContents)
|
await launchWindowsInstaller(app, installerPath, webContents)
|
||||||
|
restartApp(app)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,8 @@ const AppUpdateProgress = ({ progress, update }) => {
|
|||||||
const message = progress?.message || 'Preparing update'
|
const message = progress?.message || 'Preparing update'
|
||||||
const isInstalling = phase === 'installing'
|
const isInstalling = phase === 'installing'
|
||||||
const isError = phase === 'error'
|
const isError = phase === 'error'
|
||||||
|
const showProgress =
|
||||||
|
!isError && (!isInstalling || typeof progress?.percent === 'number')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical gap='middle'>
|
<Flex vertical gap='middle'>
|
||||||
@ -59,13 +61,13 @@ const AppUpdateProgress = ({ progress, update }) => {
|
|||||||
icon={isInstalling ? undefined : <LoadingOutlined />}
|
icon={isInstalling ? undefined : <LoadingOutlined />}
|
||||||
message={
|
message={
|
||||||
isInstalling
|
isInstalling
|
||||||
? 'The app will close while the installer runs, then reopen automatically.'
|
? message
|
||||||
: `Downloading ${artifactName}`
|
: `Downloading ${artifactName}`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isInstalling && !isError && (
|
{showProgress && (
|
||||||
<Progress
|
<Progress
|
||||||
percent={percent}
|
percent={percent}
|
||||||
status={getProgressStatus(phase)}
|
status={getProgressStatus(phase)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user