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."
|
!insertmacro progressFailure "Installation failed."
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function .onInstSuccess
|
||||||
|
Call restartFarmControlAfterUpdate
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
Section "Farm Control" SecMain
|
Section "Farm Control" SecMain
|
||||||
SectionIn RO
|
SectionIn RO
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,12 @@
|
|||||||
|
|
||||||
Var ProgressLogFile
|
Var ProgressLogFile
|
||||||
Var IsInAppUpdate
|
Var IsInAppUpdate
|
||||||
|
Var RestartAfterInstall
|
||||||
|
|
||||||
!macro initProgressLog
|
!macro initProgressLog
|
||||||
StrCpy $ProgressLogFile ""
|
StrCpy $ProgressLogFile ""
|
||||||
StrCpy $IsInAppUpdate "0"
|
StrCpy $IsInAppUpdate "0"
|
||||||
|
StrCpy $RestartAfterInstall "0"
|
||||||
|
|
||||||
${GetParameters} $R9
|
${GetParameters} $R9
|
||||||
|
|
||||||
@ -17,6 +19,12 @@ Var IsInAppUpdate
|
|||||||
StrCpy $IsInAppUpdate "1"
|
StrCpy $IsInAppUpdate "1"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
|
ClearErrors
|
||||||
|
${GetOptions} $R9 "/RESTARTFC" $R8
|
||||||
|
${IfNot} ${Errors}
|
||||||
|
StrCpy $RestartAfterInstall "1"
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
; Prefer env var so paths with spaces are reliable; /LOG= remains supported.
|
; Prefer env var so paths with spaces are reliable; /LOG= remains supported.
|
||||||
ReadEnvStr $ProgressLogFile "FARMCONTROL_INSTALL_LOG"
|
ReadEnvStr $ProgressLogFile "FARMCONTROL_INSTALL_LOG"
|
||||||
${If} $ProgressLogFile == ""
|
${If} $ProgressLogFile == ""
|
||||||
@ -229,3 +237,24 @@ Var IsInAppUpdate
|
|||||||
!insertmacro removeStartMenuShortcut
|
!insertmacro removeStartMenuShortcut
|
||||||
DeleteRegKey HKCU "Software\Classes\farmcontrol"
|
DeleteRegKey HKCU "Software\Classes\farmcontrol"
|
||||||
!macroend
|
!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}.`);
|
throw new Error(`App updates are not supported on ${process.platform}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleAppRestart();
|
if (process.platform === "darwin") {
|
||||||
// Give the detached restart watcher a moment to start, and let the UI show 100%.
|
scheduleAppRestart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the UI a moment to show completion before the app exits.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
Utils.quit();
|
Utils.quit();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { execSync, spawn } from 'node:child_process'
|
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 os from 'node:os'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
@ -131,57 +131,6 @@ const spawnDetachedMacRestart = ({ appLaunchPath, parentPid }) => {
|
|||||||
child.unref()
|
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 } = {}) => {
|
export const scheduleAppRestart = ({ parentPid = process.pid } = {}) => {
|
||||||
const appLaunchPath = resolveAppLaunchPath()
|
const appLaunchPath = resolveAppLaunchPath()
|
||||||
|
|
||||||
@ -190,10 +139,5 @@ export const scheduleAppRestart = ({ parentPid = process.pid } = {}) => {
|
|||||||
return { appLaunchPath }
|
return { appLaunchPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
throw new Error(`App restart scheduling is not supported on ${process.platform}.`)
|
||||||
spawnDetachedWindowsRestart({ appLaunchPath, parentPid })
|
|
||||||
return { appLaunchPath }
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`App updates are not supported on ${process.platform}.`)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,11 +84,16 @@ export const prepareInstallerPath = async (installerPath) => {
|
|||||||
return resolvedPath
|
return resolvedPath
|
||||||
}
|
}
|
||||||
|
|
||||||
const startWindowsInstallerProgressWatch = (logPath, sendProgress) => {
|
const startWindowsInstallerProgressWatch = (
|
||||||
|
logPath,
|
||||||
|
sendProgress,
|
||||||
|
onInstallSuccessful
|
||||||
|
) => {
|
||||||
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
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
@ -114,6 +119,15 @@ const startWindowsInstallerProgressWatch = (logPath, sendProgress) => {
|
|||||||
message: resolvedMessage
|
message: resolvedMessage
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!installSuccessNotified &&
|
||||||
|
isWindowsInstallSuccessful(installerOutput) &&
|
||||||
|
!isWindowsInstallFailed(installerOutput)
|
||||||
|
) {
|
||||||
|
installSuccessNotified = true
|
||||||
|
onInstallSuccessful?.(installerOutput)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await handle.close()
|
await handle.close()
|
||||||
}
|
}
|
||||||
@ -162,81 +176,12 @@ export const launchWindowsInstaller = async (
|
|||||||
mainWindow.show?.()
|
mainWindow.show?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopProgressWatch = startWindowsInstallerProgressWatch(
|
|
||||||
logPath,
|
|
||||||
sendProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Silent NSIS install in a child process (not a batch file).
|
let settled = false
|
||||||
// /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}`]
|
|
||||||
|
|
||||||
const installerProcess = spawn(resolvedPath, installerArgs, {
|
const settleSuccess = (output) => {
|
||||||
detached: false,
|
if (settled) return
|
||||||
env: {
|
settled = true
|
||||||
...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 { percent, message } = parseWindowsInstallerProgress(output)
|
const { percent, message } = parseWindowsInstallerProgress(output)
|
||||||
|
|
||||||
@ -247,6 +192,85 @@ export const launchWindowsInstaller = async (
|
|||||||
})
|
})
|
||||||
|
|
||||||
resolve()
|
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