Add support for automatic restart after successful installation on Windows
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
- Implemented a new function to restart Farm Control after a successful installation, triggered by the `/RESTARTFC` option. - Enhanced the installer script to wait for the application to close before relaunching, improving user experience during updates. - Updated the app update process to conditionally schedule a restart based on the platform, ensuring compatibility with macOS.
This commit is contained in:
parent
6f2f7ed5c2
commit
ee62045416
@ -71,6 +71,10 @@ Function .onInstFailed
|
||||
!insertmacro progressFailure "Installation failed."
|
||||
FunctionEnd
|
||||
|
||||
Function .onInstSuccess
|
||||
Call restartFarmControlAfterUpdate
|
||||
FunctionEnd
|
||||
|
||||
Section "Farm Control" SecMain
|
||||
SectionIn RO
|
||||
|
||||
|
||||
@ -4,10 +4,12 @@
|
||||
|
||||
Var ProgressLogFile
|
||||
Var IsInAppUpdate
|
||||
Var RestartAfterInstall
|
||||
|
||||
!macro initProgressLog
|
||||
StrCpy $ProgressLogFile ""
|
||||
StrCpy $IsInAppUpdate "0"
|
||||
StrCpy $RestartAfterInstall "0"
|
||||
|
||||
${GetParameters} $R9
|
||||
|
||||
@ -17,6 +19,12 @@ Var IsInAppUpdate
|
||||
StrCpy $IsInAppUpdate "1"
|
||||
${EndIf}
|
||||
|
||||
ClearErrors
|
||||
${GetOptions} $R9 "/RESTARTFC" $R8
|
||||
${IfNot} ${Errors}
|
||||
StrCpy $RestartAfterInstall "1"
|
||||
${EndIf}
|
||||
|
||||
; Prefer env var so paths with spaces are reliable; /LOG= remains supported.
|
||||
ReadEnvStr $ProgressLogFile "FARMCONTROL_INSTALL_LOG"
|
||||
${If} $ProgressLogFile == ""
|
||||
@ -229,3 +237,24 @@ Var IsInAppUpdate
|
||||
!insertmacro removeStartMenuShortcut
|
||||
DeleteRegKey HKCU "Software\Classes\farmcontrol"
|
||||
!macroend
|
||||
|
||||
; Wait for the running app to exit, then relaunch. Only used when /RESTARTFC is passed.
|
||||
Function restartFarmControlAfterUpdate
|
||||
${If} $RestartAfterInstall != "1"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
!insertmacro progressStatus "Waiting for Farm Control to close..."
|
||||
Sleep 1000
|
||||
|
||||
restart_wait_loop:
|
||||
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq launcher.exe" 2>nul | find /I "launcher.exe"' $R0
|
||||
${If} $R0 == 0
|
||||
Sleep 1000
|
||||
Goto restart_wait_loop
|
||||
${EndIf}
|
||||
|
||||
!insertmacro progressStatus "Starting Farm Control..."
|
||||
SetOutPath "$INSTDIR\bin"
|
||||
Exec "$INSTDIR\bin\launcher.exe"
|
||||
FunctionEnd
|
||||
|
||||
@ -206,8 +206,11 @@ const launchInstallerAndRestart = async (
|
||||
throw new Error(`App updates are not supported on ${process.platform}.`);
|
||||
}
|
||||
|
||||
scheduleAppRestart();
|
||||
// Give the detached restart watcher a moment to start, and let the UI show 100%.
|
||||
if (process.platform === "darwin") {
|
||||
scheduleAppRestart();
|
||||
}
|
||||
|
||||
// Give the UI a moment to show completion before the app exits.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
Utils.quit();
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { execSync, spawn } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
@ -131,57 +131,6 @@ const spawnDetachedMacRestart = ({ appLaunchPath, parentPid }) => {
|
||||
child.unref()
|
||||
}
|
||||
|
||||
const spawnDetachedWindowsRestart = ({ appLaunchPath, parentPid }) => {
|
||||
const updateDir = path.join(
|
||||
os.homedir(),
|
||||
'AppData',
|
||||
'Local',
|
||||
'FarmControl',
|
||||
'Updates'
|
||||
)
|
||||
const scriptPath = path.join(updateDir, `restart-${parentPid}.cmd`)
|
||||
const appLaunchWin = appLaunchPath.replaceAll('/', '\\')
|
||||
const appDirWin = path.dirname(appLaunchWin)
|
||||
|
||||
// Separate process: wait until Farm Control's PID exits, then relaunch launcher.exe.
|
||||
const script = `@echo off
|
||||
setlocal EnableExtensions
|
||||
|
||||
set "PARENT_PID=${parentPid}"
|
||||
set "APP_LAUNCHER=${appLaunchWin}"
|
||||
set "APP_DIR=${appDirWin}"
|
||||
|
||||
:waitparent
|
||||
tasklist /FI "PID eq %PARENT_PID%" 2>NUL | findstr /I /C:"%PARENT_PID%" >NUL
|
||||
if not errorlevel 1 (
|
||||
timeout /t 1 /nobreak >NUL
|
||||
goto waitparent
|
||||
)
|
||||
|
||||
timeout /t 1 /nobreak >NUL
|
||||
cd /d "%APP_DIR%"
|
||||
start "" "%APP_LAUNCHER%"
|
||||
|
||||
timeout /t 1 /nobreak >NUL
|
||||
del "%~f0" >NUL 2>&1
|
||||
`
|
||||
|
||||
mkdirSync(updateDir, { recursive: true })
|
||||
writeFileSync(scriptPath, script, 'utf8')
|
||||
|
||||
const child = spawn(
|
||||
process.env.ComSpec || 'cmd.exe',
|
||||
['/d', '/c', scriptPath],
|
||||
{
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
cwd: updateDir
|
||||
}
|
||||
)
|
||||
child.unref()
|
||||
}
|
||||
|
||||
export const scheduleAppRestart = ({ parentPid = process.pid } = {}) => {
|
||||
const appLaunchPath = resolveAppLaunchPath()
|
||||
|
||||
@ -190,10 +139,5 @@ export const scheduleAppRestart = ({ parentPid = process.pid } = {}) => {
|
||||
return { appLaunchPath }
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
spawnDetachedWindowsRestart({ appLaunchPath, parentPid })
|
||||
return { appLaunchPath }
|
||||
}
|
||||
|
||||
throw new Error(`App updates are not supported on ${process.platform}.`)
|
||||
throw new Error(`App restart scheduling is not supported on ${process.platform}.`)
|
||||
}
|
||||
|
||||
@ -84,11 +84,16 @@ export const prepareInstallerPath = async (installerPath) => {
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
const startWindowsInstallerProgressWatch = (logPath, sendProgress) => {
|
||||
const startWindowsInstallerProgressWatch = (
|
||||
logPath,
|
||||
sendProgress,
|
||||
onInstallSuccessful
|
||||
) => {
|
||||
let installerOutput = ''
|
||||
let offset = 0
|
||||
let lastPercent = null
|
||||
let lastMessage = null
|
||||
let installSuccessNotified = false
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
@ -114,6 +119,15 @@ const startWindowsInstallerProgressWatch = (logPath, sendProgress) => {
|
||||
message: resolvedMessage
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
!installSuccessNotified &&
|
||||
isWindowsInstallSuccessful(installerOutput) &&
|
||||
!isWindowsInstallFailed(installerOutput)
|
||||
) {
|
||||
installSuccessNotified = true
|
||||
onInstallSuccessful?.(installerOutput)
|
||||
}
|
||||
} finally {
|
||||
await handle.close()
|
||||
}
|
||||
@ -162,81 +176,12 @@ export const launchWindowsInstaller = async (
|
||||
mainWindow.show?.()
|
||||
}
|
||||
|
||||
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
||||
logPath,
|
||||
sendProgress
|
||||
)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Silent NSIS install in a child process (not a batch file).
|
||||
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
|
||||
// /UPDATE = in-app update (skip killing this process; overwrite in place)
|
||||
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
|
||||
const installerArgs = ['/S', '/UPDATE', `/LOG=${logPath}`]
|
||||
let settled = false
|
||||
|
||||
const installerProcess = spawn(resolvedPath, installerArgs, {
|
||||
detached: false,
|
||||
env: {
|
||||
...process.env,
|
||||
FARMCONTROL_INSTALL_LOG: logPath
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
let processOutput = ''
|
||||
|
||||
installerProcess.stdout?.on('data', (data) => {
|
||||
processOutput += data.toString('utf8')
|
||||
})
|
||||
|
||||
installerProcess.stderr?.on('data', (data) => {
|
||||
processOutput += data.toString('utf8')
|
||||
})
|
||||
|
||||
installerProcess.on('error', async (error) => {
|
||||
const watchedOutput = await stopProgressWatch()
|
||||
const output = watchedOutput || processOutput
|
||||
console.error('[app-update] installer error:', error)
|
||||
const message = getInstallErrorMessage(error, output)
|
||||
sendProgress({
|
||||
phase: 'error',
|
||||
percent: null,
|
||||
message
|
||||
})
|
||||
reject(new Error(message))
|
||||
})
|
||||
|
||||
installerProcess.on('exit', async (code) => {
|
||||
const watchedOutput = await stopProgressWatch()
|
||||
const output = watchedOutput || processOutput
|
||||
|
||||
await fs.unlink(logPath).catch(() => {})
|
||||
|
||||
if (code !== 0) {
|
||||
const message = getInstallErrorMessage(
|
||||
new Error(`Installer exited with code ${code}.`),
|
||||
output
|
||||
)
|
||||
sendProgress({
|
||||
phase: 'error',
|
||||
percent: null,
|
||||
message
|
||||
})
|
||||
reject(new Error(message))
|
||||
return
|
||||
}
|
||||
|
||||
if (isWindowsInstallFailed(output) || !isWindowsInstallSuccessful(output)) {
|
||||
const message = getInstallErrorMessage(null, output)
|
||||
sendProgress({
|
||||
phase: 'error',
|
||||
percent: null,
|
||||
message
|
||||
})
|
||||
reject(new Error(message))
|
||||
return
|
||||
}
|
||||
const settleSuccess = (output) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
|
||||
const { percent, message } = parseWindowsInstallerProgress(output)
|
||||
|
||||
@ -247,6 +192,85 @@ export const launchWindowsInstaller = async (
|
||||
})
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
const settleFailure = (message) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
|
||||
sendProgress({
|
||||
phase: 'error',
|
||||
percent: null,
|
||||
message
|
||||
})
|
||||
reject(new Error(message))
|
||||
}
|
||||
|
||||
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
||||
logPath,
|
||||
sendProgress,
|
||||
(output) => {
|
||||
settleSuccess(output)
|
||||
}
|
||||
)
|
||||
|
||||
const refreshInstallerOutput = async () => {
|
||||
return await stopProgressWatch()
|
||||
}
|
||||
|
||||
// Silent NSIS install in a detached child process (not a batch file).
|
||||
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
|
||||
// /UPDATE = in-app update (skip killing this process; overwrite in place)
|
||||
// /RESTARTFC = installer waits for this process to exit and relaunches the app
|
||||
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
|
||||
const installerArgs = [
|
||||
'/S',
|
||||
'/UPDATE',
|
||||
'/RESTARTFC',
|
||||
`/LOG=${logPath}`
|
||||
]
|
||||
|
||||
const installerProcess = spawn(resolvedPath, installerArgs, {
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
FARMCONTROL_INSTALL_LOG: logPath
|
||||
},
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
installerProcess.unref()
|
||||
|
||||
installerProcess.on('error', async (error) => {
|
||||
const output = await refreshInstallerOutput()
|
||||
console.error('[app-update] installer error:', error)
|
||||
settleFailure(getInstallErrorMessage(error, output))
|
||||
})
|
||||
|
||||
installerProcess.on('exit', async (code) => {
|
||||
const output = await refreshInstallerOutput()
|
||||
|
||||
await fs.unlink(logPath).catch(() => {})
|
||||
|
||||
if (settled) return
|
||||
|
||||
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