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