All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Updated the NSIS installer to support side-by-side updates, allowing the running application to remain active while a new version is staged in a separate directory. - Modified the installation directory handling to ensure the correct paths are used for shortcuts and registry entries. - Enhanced the update process to include a retry mechanism for folder swaps, improving reliability during updates. - Adjusted comments and documentation for clarity on the update process and its implications.
159 lines
4.4 KiB
JavaScript
159 lines
4.4 KiB
JavaScript
import { execSync, spawn } from 'node:child_process'
|
|
import { existsSync } 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_KEYS = [
|
|
'HKCU\\Software\\Tom Butcher\\Farm Control',
|
|
'HKLM\\Software\\Tom Butcher\\Farm Control'
|
|
]
|
|
const WINDOWS_LAUNCHER_EXE = 'FarmControl.exe'
|
|
const WINDOWS_LEGACY_LAUNCHER_EXE = 'launcher.exe'
|
|
const WINDOWS_DEFAULT_BIN_DIR = path.join(
|
|
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
|
'Programs',
|
|
'Farm Control',
|
|
'bin'
|
|
)
|
|
|
|
const resolveExistingLauncher = (...candidates) =>
|
|
candidates.find((candidate) => existsSync(candidate)) || null
|
|
|
|
const quoteBatchArg = (value) => `"${String(value).replaceAll('"', '""')}"`
|
|
|
|
export 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 = () => {
|
|
for (const registryKey of WINDOWS_INSTALL_DIR_KEYS) {
|
|
try {
|
|
const output = execSync(
|
|
`reg query ${quoteBatchArg(registryKey)} /v InstallDir`,
|
|
{ encoding: 'utf8', windowsHide: true }
|
|
)
|
|
const match = output.match(/InstallDir\s+REG_\w+\s+(.+)/i)
|
|
const installDir = match?.[1]?.trim()
|
|
if (installDir) {
|
|
return installDir
|
|
}
|
|
} catch {
|
|
// Try the next registry key.
|
|
}
|
|
}
|
|
|
|
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') {
|
|
// Prefer the installed launcher so restarts target the registered install dir.
|
|
const installDir = readWindowsInstallDirFromRegistry()
|
|
if (installDir) {
|
|
const launcherFromRegistry = resolveExistingLauncher(
|
|
path.join(installDir, 'bin', WINDOWS_LAUNCHER_EXE),
|
|
path.join(installDir, 'bin', WINDOWS_LEGACY_LAUNCHER_EXE)
|
|
)
|
|
if (launcherFromRegistry) {
|
|
return launcherFromRegistry
|
|
}
|
|
}
|
|
|
|
const defaultLauncher = resolveExistingLauncher(
|
|
path.join(WINDOWS_DEFAULT_BIN_DIR, WINDOWS_LAUNCHER_EXE),
|
|
path.join(WINDOWS_DEFAULT_BIN_DIR, WINDOWS_LEGACY_LAUNCHER_EXE)
|
|
)
|
|
if (defaultLauncher) {
|
|
return defaultLauncher
|
|
}
|
|
|
|
const execBase = path.basename(process.execPath).toLowerCase()
|
|
if (
|
|
execBase === WINDOWS_LAUNCHER_EXE.toLowerCase() ||
|
|
execBase === WINDOWS_LEGACY_LAUNCHER_EXE
|
|
) {
|
|
return process.execPath
|
|
}
|
|
|
|
const launcherBesideExec = resolveExistingLauncher(
|
|
path.join(path.dirname(process.execPath), WINDOWS_LAUNCHER_EXE),
|
|
path.join(path.dirname(process.execPath), WINDOWS_LEGACY_LAUNCHER_EXE)
|
|
)
|
|
if (launcherBesideExec) {
|
|
return launcherBesideExec
|
|
}
|
|
|
|
const launcherInBin = resolveExistingLauncher(
|
|
path.join(path.dirname(process.execPath), 'bin', WINDOWS_LAUNCHER_EXE),
|
|
path.join(path.dirname(process.execPath), 'bin', WINDOWS_LEGACY_LAUNCHER_EXE)
|
|
)
|
|
if (launcherInBin) {
|
|
return launcherInBin
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
export const scheduleAppRestart = ({ parentPid = process.pid } = {}) => {
|
|
const appLaunchPath = resolveAppLaunchPath()
|
|
|
|
if (process.platform === 'darwin') {
|
|
spawnDetachedMacRestart({ appLaunchPath, parentPid })
|
|
return { appLaunchPath }
|
|
}
|
|
|
|
throw new Error(`App restart scheduling is not supported on ${process.platform}.`)
|
|
}
|