Refactor app update process and restart handling
- Removed the restart stage from the app update progress component, simplifying the update stages to only include download and install. - Updated the app update logic to schedule a restart after installation instead of handling it directly in the installer functions. - Enhanced the messaging for installation completion by removing references to automatic restarts in the progress messages. - Introduced a new updater-runner module to manage application restarts across platforms, improving the update workflow.
This commit is contained in:
parent
4caa7a4bd1
commit
30f0ac1559
@ -4,7 +4,6 @@ import { Button, Flex, Modal, Progress, Typography, theme, Divider } from 'antd'
|
|||||||
|
|
||||||
import CloudIcon from '../../../Icons/CloudIcon'
|
import CloudIcon from '../../../Icons/CloudIcon'
|
||||||
import HostIcon from '../../../Icons/HostIcon'
|
import HostIcon from '../../../Icons/HostIcon'
|
||||||
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
||||||
|
|
||||||
import CheckCircleIcon from '../../../Icons/CheckCircleIcon'
|
import CheckCircleIcon from '../../../Icons/CheckCircleIcon'
|
||||||
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
|
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
|
||||||
@ -46,15 +45,6 @@ const STAGE_CONFIG = {
|
|||||||
complete: 'Installed',
|
complete: 'Installed',
|
||||||
error: 'Install failed'
|
error: 'Install failed'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
restart: {
|
|
||||||
icon: ReloadIcon,
|
|
||||||
labels: {
|
|
||||||
pending: 'Restart',
|
|
||||||
active: 'Restarting...',
|
|
||||||
complete: 'Restarted',
|
|
||||||
error: 'Restart failed'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +69,7 @@ const isInstallComplete = (phase, message) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
normalized.includes('complete') ||
|
normalized.includes('complete') ||
|
||||||
normalized.includes('successful') ||
|
normalized.includes('successful')
|
||||||
normalized.includes('restarting')
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,12 +80,6 @@ const getInstallStageStatus = (phase, isError, message) => {
|
|||||||
return 'pending'
|
return 'pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRestartStageStatus = (phase, isError, message) => {
|
|
||||||
if (isError && isInstallComplete(phase, message)) return 'error'
|
|
||||||
if (isInstallComplete(phase, message)) return 'active'
|
|
||||||
return 'pending'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getProgressStatus = (stageStatus) => {
|
const getProgressStatus = (stageStatus) => {
|
||||||
if (stageStatus === 'error') return 'exception'
|
if (stageStatus === 'error') return 'exception'
|
||||||
if (stageStatus === 'complete') return 'success'
|
if (stageStatus === 'complete') return 'success'
|
||||||
@ -112,7 +95,7 @@ const UpdateStage = ({ stage, status, percent, detail }) => {
|
|||||||
const resolvedStatus =
|
const resolvedStatus =
|
||||||
status !== 'error' && resolvedPercent === 100 ? 'complete' : status
|
status !== 'error' && resolvedPercent === 100 ? 'complete' : status
|
||||||
const color = getStageColor(resolvedStatus, token)
|
const color = getStageColor(resolvedStatus, token)
|
||||||
const showProgress = resolvedStatus === 'active' && stage !== 'restart'
|
const showProgress = resolvedStatus === 'active'
|
||||||
|
|
||||||
const StatusIcon =
|
const StatusIcon =
|
||||||
resolvedStatus === 'complete'
|
resolvedStatus === 'complete'
|
||||||
@ -143,7 +126,7 @@ const UpdateStage = ({ stage, status, percent, detail }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdateStage.propTypes = {
|
UpdateStage.propTypes = {
|
||||||
stage: PropTypes.oneOf(['download', 'install', 'restart']).isRequired,
|
stage: PropTypes.oneOf(['download', 'install']).isRequired,
|
||||||
status: PropTypes.oneOf(['pending', 'active', 'complete', 'error'])
|
status: PropTypes.oneOf(['pending', 'active', 'complete', 'error'])
|
||||||
.isRequired,
|
.isRequired,
|
||||||
percent: PropTypes.number,
|
percent: PropTypes.number,
|
||||||
@ -165,7 +148,6 @@ const AppUpdateProgress = ({ progress, update, onClose }) => {
|
|||||||
|
|
||||||
const downloadStatus = getDownloadStageStatus(phase, isError)
|
const downloadStatus = getDownloadStageStatus(phase, isError)
|
||||||
const installStatus = getInstallStageStatus(phase, isError, message)
|
const installStatus = getInstallStageStatus(phase, isError, message)
|
||||||
const restartStatus = getRestartStageStatus(phase, isError, message)
|
|
||||||
|
|
||||||
const downloadPercent =
|
const downloadPercent =
|
||||||
downloadStatus === 'active' ? (phase === 'preparing' ? 0 : percent) : null
|
downloadStatus === 'active' ? (phase === 'preparing' ? 0 : percent) : null
|
||||||
@ -202,7 +184,6 @@ const AppUpdateProgress = ({ progress, update, onClose }) => {
|
|||||||
percent={installPercent}
|
percent={installPercent}
|
||||||
detail={installDetail}
|
detail={installDetail}
|
||||||
/>
|
/>
|
||||||
<UpdateStage stage='restart' status={restartStatus} />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import process from "node:process";
|
|||||||
import { Utils } from "electrobun/bun";
|
import { Utils } from "electrobun/bun";
|
||||||
import { launchMacInstaller } from "./macappupdate.js";
|
import { launchMacInstaller } from "./macappupdate.js";
|
||||||
import { launchWindowsInstaller } from "./winappupdate.js";
|
import { launchWindowsInstaller } from "./winappupdate.js";
|
||||||
|
import { scheduleAppRestart } from "./updater-runner.js";
|
||||||
|
|
||||||
const SUPPORTED_TARGETS = {
|
const SUPPORTED_TARGETS = {
|
||||||
darwin: {
|
darwin: {
|
||||||
@ -172,11 +173,7 @@ const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const restartApp = () => {
|
const launchInstallerAndRestart = async (
|
||||||
Utils.quit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const launchInstallerAndQuit = async (
|
|
||||||
mainWindow,
|
mainWindow,
|
||||||
installerPath,
|
installerPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
@ -184,23 +181,25 @@ const launchInstallerAndQuit = async (
|
|||||||
const installerHelpers = { sendProgress, getInstallErrorMessage };
|
const installerHelpers = { sendProgress, getInstallErrorMessage };
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
await launchMacInstaller(mainWindow, installerPath, sendProgress, installerHelpers);
|
await launchMacInstaller(
|
||||||
restartApp();
|
mainWindow,
|
||||||
return;
|
installerPath,
|
||||||
}
|
sendProgress,
|
||||||
|
installerHelpers,
|
||||||
if (process.platform === "win32") {
|
);
|
||||||
|
} else if (process.platform === "win32") {
|
||||||
await launchWindowsInstaller(
|
await launchWindowsInstaller(
|
||||||
mainWindow,
|
mainWindow,
|
||||||
installerPath,
|
installerPath,
|
||||||
sendProgress,
|
sendProgress,
|
||||||
installerHelpers,
|
installerHelpers,
|
||||||
);
|
);
|
||||||
restartApp();
|
} else {
|
||||||
return;
|
throw new Error(`App updates are not supported on ${process.platform}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`App updates are not supported on ${process.platform}.`);
|
scheduleAppRestart();
|
||||||
|
Utils.quit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const runAppUpdate = async (mainWindow, update, sendProgress) => {
|
const runAppUpdate = async (mainWindow, update, sendProgress) => {
|
||||||
@ -229,7 +228,7 @@ const runAppUpdate = async (mainWindow, update, sendProgress) => {
|
|||||||
message: "Update downloaded",
|
message: "Update downloaded",
|
||||||
});
|
});
|
||||||
|
|
||||||
await launchInstallerAndQuit(mainWindow, installerPath, sendProgress);
|
await launchInstallerAndRestart(mainWindow, installerPath, sendProgress);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function startAppUpdate(mainWindow, update, sendProgress) {
|
export function startAppUpdate(mainWindow, update, sendProgress) {
|
||||||
|
|||||||
@ -158,7 +158,7 @@ export const launchMacInstaller = (
|
|||||||
sendProgress( {
|
sendProgress( {
|
||||||
phase: 'installing',
|
phase: 'installing',
|
||||||
percent: percent ?? 100,
|
percent: percent ?? 100,
|
||||||
message: message || 'Installation complete. Restarting Farm Control...'
|
message: message || 'Installation complete.'
|
||||||
})
|
})
|
||||||
|
|
||||||
resolve()
|
resolve()
|
||||||
|
|||||||
193
src/desktop/updater-runner.js
Normal file
193
src/desktop/updater-runner.js
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { execSync, spawn } from 'node:child_process'
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
const MAC_APP_NAME = 'Farm Control.app'
|
||||||
|
const WINDOWS_INSTALL_DIR_KEY =
|
||||||
|
'HKLM\\Software\\Tom Butcher\\Farm Control'
|
||||||
|
const WINDOWS_DEFAULT_LAUNCHER = path.join(
|
||||||
|
process.env.ProgramFiles || 'C:\\Program Files',
|
||||||
|
'Farm Control',
|
||||||
|
'bin',
|
||||||
|
'launcher.exe'
|
||||||
|
)
|
||||||
|
|
||||||
|
const quoteBatchArg = (value) => `"${String(value).replaceAll('"', '""')}"`
|
||||||
|
|
||||||
|
const findMacAppBundle = (startPath) => {
|
||||||
|
let current = path.resolve(startPath)
|
||||||
|
|
||||||
|
while (current !== path.dirname(current)) {
|
||||||
|
if (current.endsWith('.app')) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseName = path.basename(current)
|
||||||
|
if (baseName === 'Contents') {
|
||||||
|
const bundlePath = path.dirname(current)
|
||||||
|
if (bundlePath.endsWith('.app')) {
|
||||||
|
return bundlePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = path.dirname(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const readWindowsInstallDirFromRegistry = () => {
|
||||||
|
try {
|
||||||
|
const output = execSync(
|
||||||
|
`reg query ${quoteBatchArg(WINDOWS_INSTALL_DIR_KEY)} /v InstallDir`,
|
||||||
|
{ encoding: 'utf8', windowsHide: true }
|
||||||
|
)
|
||||||
|
const match = output.match(/InstallDir\s+REG_\w+\s+(.+)/i)
|
||||||
|
return match?.[1]?.trim() || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveAppLaunchPath = () => {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const bundleFromExec = findMacAppBundle(process.execPath)
|
||||||
|
if (bundleFromExec) {
|
||||||
|
return bundleFromExec
|
||||||
|
}
|
||||||
|
|
||||||
|
const applicationsPath = path.join('/Applications', MAC_APP_NAME)
|
||||||
|
if (existsSync(applicationsPath)) {
|
||||||
|
return applicationsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.execPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const execBase = path.basename(process.execPath).toLowerCase()
|
||||||
|
if (execBase === 'launcher.exe') {
|
||||||
|
return process.execPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const launcherBesideExec = path.join(path.dirname(process.execPath), 'launcher.exe')
|
||||||
|
if (existsSync(launcherBesideExec)) {
|
||||||
|
return launcherBesideExec
|
||||||
|
}
|
||||||
|
|
||||||
|
const launcherInBin = path.join(
|
||||||
|
path.dirname(process.execPath),
|
||||||
|
'bin',
|
||||||
|
'launcher.exe'
|
||||||
|
)
|
||||||
|
if (existsSync(launcherInBin)) {
|
||||||
|
return launcherInBin
|
||||||
|
}
|
||||||
|
|
||||||
|
const installDir = readWindowsInstallDirFromRegistry()
|
||||||
|
if (installDir) {
|
||||||
|
const launcherFromRegistry = path.join(installDir, 'bin', 'launcher.exe')
|
||||||
|
if (existsSync(launcherFromRegistry)) {
|
||||||
|
return launcherFromRegistry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(WINDOWS_DEFAULT_LAUNCHER)) {
|
||||||
|
return WINDOWS_DEFAULT_LAUNCHER
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.execPath
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`App updates are not supported on ${process.platform}.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawnDetachedMacRestart = ({ appLaunchPath, parentPid }) => {
|
||||||
|
const child = spawn(
|
||||||
|
'/bin/sh',
|
||||||
|
[
|
||||||
|
'-c',
|
||||||
|
`while kill -0 ${parentPid} 2>/dev/null; do sleep 0.5; done; sleep 1; /usr/bin/open ${JSON.stringify(appLaunchPath)}`
|
||||||
|
],
|
||||||
|
{
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
child.unref()
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawnDetachedWindowsRestart = ({ appLaunchPath, parentPid }) => {
|
||||||
|
const updateDir = path.join(
|
||||||
|
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||||
|
'FarmControl',
|
||||||
|
'Updates'
|
||||||
|
)
|
||||||
|
const scriptPath = path.join(updateDir, `restart-${parentPid}.bat`)
|
||||||
|
const appLaunchWin = appLaunchPath.replaceAll('/', '\\')
|
||||||
|
const scriptWin = scriptPath.replaceAll('/', '\\')
|
||||||
|
|
||||||
|
const script = `@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
set "PARENT_PID=${parentPid}"
|
||||||
|
set "APP_LAUNCHER=${appLaunchWin}"
|
||||||
|
|
||||||
|
:waitparent
|
||||||
|
tasklist /FI "PID eq %PARENT_PID%" 2>NUL | find /I "%PARENT_PID%" >NUL && (
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
goto waitparent
|
||||||
|
)
|
||||||
|
|
||||||
|
:waitprocesses
|
||||||
|
tasklist /FI "IMAGENAME eq launcher.exe" 2>NUL | find /I /N "launcher.exe">NUL && goto waitsleep
|
||||||
|
tasklist /FI "IMAGENAME eq bun.exe" 2>NUL | find /I /N "bun.exe">NUL && goto waitsleep
|
||||||
|
tasklist 2>NUL | find /I "bun Helper">NUL && goto waitsleep
|
||||||
|
goto waitdone
|
||||||
|
:waitsleep
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
goto waitprocesses
|
||||||
|
:waitdone
|
||||||
|
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
start "" "%APP_LAUNCHER%"
|
||||||
|
|
||||||
|
for /f "tokens=1" %%t in ('schtasks /query /fo list ^| findstr /i "FarmControlRestart_"') do (
|
||||||
|
schtasks /delete /tn "%%t" /f >nul 2>&1
|
||||||
|
)
|
||||||
|
|
||||||
|
ping -n 2 127.0.0.1 >nul
|
||||||
|
del "%~f0"
|
||||||
|
`
|
||||||
|
|
||||||
|
mkdirSync(updateDir, { recursive: true })
|
||||||
|
writeFileSync(scriptPath, script, 'utf8')
|
||||||
|
|
||||||
|
const taskName = `FarmControlRestart_${parentPid}_${Date.now()}`
|
||||||
|
execSync(
|
||||||
|
`schtasks /create /tn ${quoteBatchArg(taskName)} /tr ${quoteBatchArg(`cmd /c "${scriptWin}"`)} /sc once /st 00:00 /f`,
|
||||||
|
{ stdio: 'ignore', windowsHide: true }
|
||||||
|
)
|
||||||
|
execSync(`schtasks /run /tn ${quoteBatchArg(taskName)}`, {
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scheduleAppRestart = ({ parentPid = process.pid } = {}) => {
|
||||||
|
const appLaunchPath = resolveAppLaunchPath()
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
spawnDetachedMacRestart({ appLaunchPath, parentPid })
|
||||||
|
return { appLaunchPath }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
spawnDetachedWindowsRestart({ appLaunchPath, parentPid })
|
||||||
|
return { appLaunchPath }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`App updates are not supported on ${process.platform}.`)
|
||||||
|
}
|
||||||
@ -95,7 +95,7 @@ const parseWindowsInstallerProgress = (output) => {
|
|||||||
|
|
||||||
if (/Installation success or error status:\s*0\b/.test(line)) {
|
if (/Installation success or error status:\s*0\b/.test(line)) {
|
||||||
percent = 100
|
percent = 100
|
||||||
message = 'Installation complete. Restarting Farm Control...'
|
message = 'Installation complete.'
|
||||||
matchedLines.push('install-success')
|
matchedLines.push('install-success')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,7 +144,7 @@ const isValidMsiPackage = async (filePath) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const prepareInstallerPath = async (installerPath) => {
|
export const prepareInstallerPath = async (installerPath) => {
|
||||||
const fileName = path.basename(installerPath)
|
const fileName = path.basename(installerPath)
|
||||||
const updateDir = path.join(
|
const updateDir = path.join(
|
||||||
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
||||||
@ -427,7 +427,7 @@ export const launchWindowsInstaller = async (
|
|||||||
sendProgress( {
|
sendProgress( {
|
||||||
phase: 'installing',
|
phase: 'installing',
|
||||||
percent: percent ?? 100,
|
percent: percent ?? 100,
|
||||||
message: message || 'Installation complete. Restarting Farm Control...'
|
message: message || 'Installation complete.'
|
||||||
})
|
})
|
||||||
|
|
||||||
debugLog('installer completed successfully')
|
debugLog('installer completed successfully')
|
||||||
|
|||||||
@ -124,7 +124,7 @@ export function handleDeepLink(url) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
openInternalUrl(path)
|
sendNavigateToRenderer(path)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user