Enhance Windows installer progress monitoring with improved outcome handling
- Refactored the `startWindowsInstallerProgressWatch` function to notify on installation success, failure, and stalling more reliably. - Introduced a timeout mechanism to detect stalled installers, improving user feedback during the update process. - Updated the `launchWindowsInstaller` function to pass new callback parameters for handling different installation outcomes, enhancing overall robustness.
This commit is contained in:
parent
bb2b8148c7
commit
84a6a86bf3
@ -126,13 +126,14 @@ export const prepareInstallerPath = async (installerPath) => {
|
|||||||
const startWindowsInstallerProgressWatch = (
|
const startWindowsInstallerProgressWatch = (
|
||||||
logPath,
|
logPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
onInstallSuccessful
|
{ onInstallSuccessful, onInstallFailed, onStalled, stallTimeoutMs }
|
||||||
) => {
|
) => {
|
||||||
let installerOutput = ''
|
let installerOutput = ''
|
||||||
let offset = 0
|
let offset = 0
|
||||||
let lastPercent = null
|
let lastPercent = null
|
||||||
let lastMessage = null
|
let lastMessage = null
|
||||||
let installSuccessNotified = false
|
let outcomeNotified = false
|
||||||
|
let lastActivityAt = Date.now()
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
@ -145,6 +146,7 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
await handle.read(buffer, 0, buffer.length, offset)
|
await handle.read(buffer, 0, buffer.length, offset)
|
||||||
offset = stat.size
|
offset = stat.size
|
||||||
installerOutput += buffer.toString('utf8')
|
installerOutput += buffer.toString('utf8')
|
||||||
|
lastActivityAt = Date.now()
|
||||||
|
|
||||||
const { percent, message } =
|
const { percent, message } =
|
||||||
parseWindowsInstallerProgress(installerOutput)
|
parseWindowsInstallerProgress(installerOutput)
|
||||||
@ -160,13 +162,14 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!outcomeNotified) {
|
||||||
!installSuccessNotified &&
|
if (isWindowsInstallFailed(installerOutput)) {
|
||||||
isWindowsInstallSuccessful(installerOutput) &&
|
outcomeNotified = true
|
||||||
!isWindowsInstallFailed(installerOutput)
|
onInstallFailed?.(installerOutput)
|
||||||
) {
|
} else if (isWindowsInstallSuccessful(installerOutput)) {
|
||||||
installSuccessNotified = true
|
outcomeNotified = true
|
||||||
onInstallSuccessful?.(installerOutput)
|
onInstallSuccessful?.(installerOutput)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await handle.close()
|
await handle.close()
|
||||||
@ -179,6 +182,18 @@ const startWindowsInstallerProgressWatch = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
|
// The installer runs detached (outside our job object), so a dead
|
||||||
|
// installer only shows up as the log going quiet before any outcome.
|
||||||
|
if (
|
||||||
|
!outcomeNotified &&
|
||||||
|
stallTimeoutMs > 0 &&
|
||||||
|
Date.now() - lastActivityAt > stallTimeoutMs
|
||||||
|
) {
|
||||||
|
outcomeNotified = true
|
||||||
|
onStalled?.(installerOutput)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
poll().catch((error) => {
|
poll().catch((error) => {
|
||||||
console.error('[app-update] installer log poll error:', error)
|
console.error('[app-update] installer log poll error:', error)
|
||||||
})
|
})
|
||||||
@ -249,8 +264,22 @@ export const launchWindowsInstaller = async (
|
|||||||
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
||||||
logPath,
|
logPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
(output) => {
|
{
|
||||||
settleSuccess(output)
|
stallTimeoutMs: 3 * 60 * 1000,
|
||||||
|
onInstallSuccessful: (output) => {
|
||||||
|
settleSuccess(output)
|
||||||
|
},
|
||||||
|
onInstallFailed: (output) => {
|
||||||
|
settleFailure(getInstallErrorMessage(null, output))
|
||||||
|
},
|
||||||
|
onStalled: (output) => {
|
||||||
|
settleFailure(
|
||||||
|
getInstallErrorMessage(
|
||||||
|
new Error('The update installer stopped responding.'),
|
||||||
|
output
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -258,65 +287,63 @@ export const launchWindowsInstaller = async (
|
|||||||
return await stopProgressWatch()
|
return await stopProgressWatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Silent NSIS install in a detached child process (not a batch file).
|
// Silent NSIS install.
|
||||||
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
|
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
|
||||||
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
|
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
|
||||||
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
|
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
|
||||||
// /PARENTPID = exact process to wait for (CEF may outlive the launcher)
|
// /PARENTPID = exact process to wait for (CEF may outlive the launcher)
|
||||||
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log
|
// /LOG= = progress log (installer:% / PHASE / STATUS / COPY_TOTAL / COPY_FILE)
|
||||||
// (installer:% / PHASE / STATUS / COPY_TOTAL / COPY_FILE)
|
const installerCommandLine = [
|
||||||
const installerArgs = [
|
`"${resolvedPath}"`,
|
||||||
'/S',
|
'/S',
|
||||||
'/UPDATE',
|
'/UPDATE',
|
||||||
'/RESTARTFC',
|
'/RESTARTFC',
|
||||||
`/PARENTPID=${process.pid}`,
|
`/PARENTPID=${process.pid}`,
|
||||||
`/LOG=${logPath}`
|
`/LOG="${logPath}"`
|
||||||
]
|
].join(' ')
|
||||||
|
|
||||||
const installerProcess = spawn(resolvedPath, installerArgs, {
|
// This process runs inside a Windows job object (CEF/launcher) that kills
|
||||||
detached: true,
|
// every child process when the app exits — a directly spawned installer
|
||||||
env: {
|
// (even with detached: true) inherits the job and dies mid folder-swap the
|
||||||
...process.env,
|
// moment the app quits. Creating the process via WMI (Win32_Process.Create)
|
||||||
FARMCONTROL_INSTALL_LOG: logPath
|
// parents it to the WMI provider host, outside our job, so it survives.
|
||||||
},
|
const quoteForPowershell = (value) => `'${value.replace(/'/g, "''")}'`
|
||||||
stdio: 'ignore',
|
|
||||||
windowsHide: true
|
|
||||||
})
|
|
||||||
|
|
||||||
installerProcess.unref()
|
const spawnerProcess = spawn(
|
||||||
|
'powershell.exe',
|
||||||
|
[
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-WindowStyle',
|
||||||
|
'Hidden',
|
||||||
|
'-Command',
|
||||||
|
`$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ${quoteForPowershell(installerCommandLine)} }; exit $result.ReturnValue`
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
installerProcess.on('error', async (error) => {
|
spawnerProcess.on('error', async (error) => {
|
||||||
const output = await refreshInstallerOutput()
|
const output = await refreshInstallerOutput()
|
||||||
console.error('[app-update] installer error:', error)
|
console.error('[app-update] installer launch error:', error)
|
||||||
settleFailure(getInstallErrorMessage(error, output))
|
settleFailure(getInstallErrorMessage(error, output))
|
||||||
})
|
})
|
||||||
|
|
||||||
installerProcess.on('exit', async (code) => {
|
spawnerProcess.on('exit', async (code) => {
|
||||||
|
// PowerShell exits as soon as the installer is created; from here the
|
||||||
|
// log watcher owns success/failure. Only a non-zero code (WMI create
|
||||||
|
// failed) means the installer never started.
|
||||||
|
if (code === 0 || settled) return
|
||||||
|
|
||||||
const output = await refreshInstallerOutput()
|
const output = await refreshInstallerOutput()
|
||||||
|
settleFailure(
|
||||||
await fs.unlink(logPath).catch(() => {})
|
getInstallErrorMessage(
|
||||||
|
new Error(`Failed to launch the update installer (code ${code}).`),
|
||||||
if (settled) return
|
output
|
||||||
|
|
||||||
if (code !== 0) {
|
|
||||||
settleFailure(
|
|
||||||
getInstallErrorMessage(
|
|
||||||
new Error(`Installer exited with code ${code}.`),
|
|
||||||
output
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return
|
)
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isWindowsInstallFailed(output) ||
|
|
||||||
!isWindowsInstallSuccessful(output)
|
|
||||||
) {
|
|
||||||
settleFailure(getInstallErrorMessage(null, output))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
settleSuccess(output)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user