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}.`) }