From eb1ee1e82b34d7bc26d87f1ea587112ca49f6fdc Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Tue, 4 Aug 2026 01:29:31 +0100 Subject: [PATCH] Update Windows installer to use FarmControl.exe and enhance process handling - Replaced references to launcher.exe with FarmControl.exe in the installer script, ensuring consistency in application execution. - Added taskkill command to stop running FarmControl processes during installation. - Updated shortcut creation to point to FarmControl.exe for both desktop and start menu. - Enhanced cleanup process to remove old executable backups and ensure proper application state after updates. - Refactored updater logic to check for the presence of FarmControl.exe, improving application launch reliability. --- packaging/windows/installer.nsh | 16 +++-- scripts/build-windows-nsis.ps1 | 4 +- scripts/finalize-desktop-artifacts.mjs | 2 +- scripts/patch-windows-binaries.mjs | 90 +++++++------------------- src/bun/deeplink.js | 10 ++- src/desktop/updater-runner.js | 46 ++++++++----- src/desktop/windows-app-paths.js | 5 +- 7 files changed, 81 insertions(+), 92 deletions(-) diff --git a/packaging/windows/installer.nsh b/packaging/windows/installer.nsh index 2af167b..e40a9d8 100644 --- a/packaging/windows/installer.nsh +++ b/packaging/windows/installer.nsh @@ -100,6 +100,7 @@ Var RestartAfterInstall !insertmacro progressPhase "Stopping Farm Control" !insertmacro progressStatus "Stopping running Farm Control processes..." DetailPrint "Stopping running Farm Control processes..." + ExecWait 'taskkill /F /IM FarmControl.exe /T' $R0 ExecWait 'taskkill /F /IM launcher.exe /T' $R0 !macroend @@ -126,6 +127,7 @@ Var RestartAfterInstall ClearErrors Rename "$INSTDIR\bin" "$INSTDIR\bin.old" ${If} ${Errors} + !insertmacro moveAsideIfPresent "$INSTDIR\bin\FarmControl.exe" !insertmacro moveAsideIfPresent "$INSTDIR\bin\launcher.exe" !insertmacro moveAsideIfPresent "$INSTDIR\bin\bun.exe" !insertmacro moveAsideIfPresent "$INSTDIR\bin\bspatch.exe" @@ -179,14 +181,14 @@ Var RestartAfterInstall !macro createDesktopShortcut SetShellVarContext current SetOutPath "$INSTDIR\bin" - CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe" "" "$INSTDIR\bin\launcher.exe" 0 SW_SHOWNORMAL "" "Farm Control" + CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\FarmControl.exe" "" "$INSTDIR\bin\FarmControl.exe" 0 SW_SHOWNORMAL "" "Farm Control" !macroend !macro createStartMenuShortcut SetShellVarContext current CreateDirectory "$SMPROGRAMS\Farm Control" SetOutPath "$INSTDIR\bin" - CreateShortCut "$SMPROGRAMS\Farm Control\Farm Control.lnk" "$INSTDIR\bin\launcher.exe" "" "$INSTDIR\bin\launcher.exe" 0 SW_SHOWNORMAL "" "Farm Control" + CreateShortCut "$SMPROGRAMS\Farm Control\Farm Control.lnk" "$INSTDIR\bin\FarmControl.exe" "" "$INSTDIR\bin\FarmControl.exe" 0 SW_SHOWNORMAL "" "Farm Control" !macroend !macro removeDesktopShortcut @@ -203,6 +205,7 @@ Var RestartAfterInstall !macro cleanupUpdateBackups ; Best-effort; may fail while the previous process still holds handles. RMDir /r "$INSTDIR\bin.old" + Delete "$INSTDIR\bin\FarmControl.exe.old" Delete "$INSTDIR\bin\launcher.exe.old" Delete "$INSTDIR\bin\bun.exe.old" Delete "$INSTDIR\bin\bspatch.exe.old" @@ -216,7 +219,7 @@ Var RestartAfterInstall DeleteRegKey HKCU "Software\Classes\farmcontrol" WriteRegStr HKCU "Software\Classes\farmcontrol" "" "URL:farmcontrol" WriteRegStr HKCU "Software\Classes\farmcontrol" "URL Protocol" "" - WriteRegStr HKCU "Software\Classes\farmcontrol\DefaultIcon" "" "$INSTDIR\bin\launcher.exe" + WriteRegStr HKCU "Software\Classes\farmcontrol\DefaultIcon" "" "$INSTDIR\bin\FarmControl.exe" WriteRegStr HKCU "Software\Classes\farmcontrol\shell" "" "" WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open" "" "" WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open\command" "" '"$INSTDIR\bin\bun.exe" "$INSTDIR\bin\deeplink.js" "%1"' @@ -248,6 +251,11 @@ Function restartFarmControlAfterUpdate Sleep 1000 restart_wait_loop: + ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq FarmControl.exe" 2>nul | find /I "FarmControl.exe"' $R0 + ${If} $R0 == 0 + Sleep 1000 + Goto restart_wait_loop + ${EndIf} ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq launcher.exe" 2>nul | find /I "launcher.exe"' $R0 ${If} $R0 == 0 Sleep 1000 @@ -256,5 +264,5 @@ Function restartFarmControlAfterUpdate !insertmacro progressStatus "Starting Farm Control..." SetOutPath "$INSTDIR\bin" - Exec "$INSTDIR\bin\launcher.exe" + Exec "$INSTDIR\bin\FarmControl.exe" FunctionEnd diff --git a/scripts/build-windows-nsis.ps1 b/scripts/build-windows-nsis.ps1 index 259a449..b60ee9f 100644 --- a/scripts/build-windows-nsis.ps1 +++ b/scripts/build-windows-nsis.ps1 @@ -69,10 +69,10 @@ if ($stagedAppBytes -lt 5242880) { throw "Staged application is only $([math]::Round($stagedAppBytes / 1KB, 1)) KiB. Expected a full Electrobun app bundle before building the installer." } -$requiredExe = Join-Path $stagingAppDir "bin\launcher.exe" +$requiredExe = Join-Path $stagingAppDir "bin\FarmControl.exe" $requiredDeeplinkScript = Join-Path $stagingAppDir "bin\deeplink.js" if (-not (Test-Path $requiredExe)) { - throw "Staged application is missing bin\launcher.exe" + throw "Staged application is missing bin\FarmControl.exe" } if (-not (Test-Path $requiredDeeplinkScript)) { throw "Staged application is missing bin\deeplink.js" diff --git a/scripts/finalize-desktop-artifacts.mjs b/scripts/finalize-desktop-artifacts.mjs index 2a970ec..d33ee6c 100644 --- a/scripts/finalize-desktop-artifacts.mjs +++ b/scripts/finalize-desktop-artifacts.mjs @@ -619,7 +619,7 @@ async function main() { platformDir ) stageWindowsDeeplinkScript(appDir) - await patchWindowsBinaries(appDir) + patchWindowsBinaries(appDir) let published try { diff --git a/scripts/patch-windows-binaries.mjs b/scripts/patch-windows-binaries.mjs index d29e3d0..cc24550 100644 --- a/scripts/patch-windows-binaries.mjs +++ b/scripts/patch-windows-binaries.mjs @@ -1,79 +1,37 @@ -import { copyFileSync, existsSync, unlinkSync } from 'node:fs' +import { existsSync, renameSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import rcedit from 'rcedit' -const PATCH_TARGETS = new Set(['launcher.exe']) -const MAX_ATTEMPTS = 6 -const BASE_DELAY_MS = 500 - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -async function patchExecutable(executablePath) { - let lastError - - for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { - const tempPath = `${executablePath}.rcedit-${process.pid}-${attempt}.tmp` - - try { - // Patch a copy then replace — avoids AV/indexer locks on the live PE. - copyFileSync(executablePath, tempPath) - await rcedit(tempPath, { - 'requested-execution-level': 'asInvoker' - }) - copyFileSync(tempPath, executablePath) - return - } catch (error) { - lastError = error - const message = error instanceof Error ? error.message : String(error) - console.warn( - `patch-windows-binaries: attempt ${attempt}/${MAX_ATTEMPTS} failed for ${executablePath}: ${message}` - ) - - if (attempt < MAX_ATTEMPTS) { - await sleep(BASE_DELAY_MS * 2 ** (attempt - 1)) - } - } finally { - try { - unlinkSync(tempPath) - } catch { - // Ignore cleanup failures. - } - } - } - - throw lastError -} - -export async function patchWindowsBinaries(appDir) { - if (process.platform !== 'win32') { - return - } +export const WINDOWS_LAUNCHER_EXE = 'FarmControl.exe' +const SOURCE_LAUNCHER_EXE = 'launcher.exe' +export function patchWindowsBinaries(appDir) { const binDir = path.join(appDir, 'bin') if (!existsSync(binDir)) { throw new Error(`patch-windows-binaries: bin directory not found at ${binDir}`) } - for (const executable of PATCH_TARGETS) { - const executablePath = path.join(binDir, executable) - if (!existsSync(executablePath)) { - console.warn(`patch-windows-binaries: skipping missing ${executablePath}`) - continue - } + const sourcePath = path.join(binDir, SOURCE_LAUNCHER_EXE) + const destPath = path.join(binDir, WINDOWS_LAUNCHER_EXE) - try { - await patchExecutable(executablePath) - console.log(`patch-windows-binaries: set asInvoker on ${executablePath}`) - } catch (error) { - console.warn( - `patch-windows-binaries: failed to patch ${executablePath}:`, - error instanceof Error ? error.message : error - ) - } + if (existsSync(destPath) && !existsSync(sourcePath)) { + console.log(`patch-windows-binaries: ${WINDOWS_LAUNCHER_EXE} already present`) + return } + + if (!existsSync(sourcePath)) { + console.warn(`patch-windows-binaries: skipping missing ${sourcePath}`) + return + } + + if (existsSync(destPath)) { + renameSync(destPath, `${destPath}.old`) + } + + renameSync(sourcePath, destPath) + console.log( + `patch-windows-binaries: renamed ${SOURCE_LAUNCHER_EXE} -> ${WINDOWS_LAUNCHER_EXE}` + ) } if (process.argv[1] === fileURLToPath(import.meta.url)) { @@ -83,5 +41,5 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { process.exit(1) } - await patchWindowsBinaries(path.resolve(appDirArg)) + patchWindowsBinaries(path.resolve(appDirArg)) } diff --git a/src/bun/deeplink.js b/src/bun/deeplink.js index bfca4e3..4a6dad3 100644 --- a/src/bun/deeplink.js +++ b/src/bun/deeplink.js @@ -29,9 +29,13 @@ console.log('[deeplink] no running instance found, launching Farm Control') writeDeeplinkSignal(payload) -const launcherPath = join(dirname(process.execPath), 'launcher.exe') -if (!existsSync(launcherPath)) { - console.error('[deeplink] launcher.exe not found at', launcherPath) +const binDir = dirname(process.execPath) +const launcherPath = ['FarmControl.exe', 'launcher.exe'] + .map((name) => join(binDir, name)) + .find((candidate) => existsSync(candidate)) + +if (!launcherPath) { + console.error('[deeplink] FarmControl.exe not found in', binDir) process.exit(1) } diff --git a/src/desktop/updater-runner.js b/src/desktop/updater-runner.js index 3c3de5f..8578db1 100644 --- a/src/desktop/updater-runner.js +++ b/src/desktop/updater-runner.js @@ -9,14 +9,18 @@ const WINDOWS_INSTALL_DIR_KEYS = [ 'HKCU\\Software\\Tom Butcher\\Farm Control', 'HKLM\\Software\\Tom Butcher\\Farm Control' ] -const WINDOWS_DEFAULT_LAUNCHER = path.join( +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', - 'launcher.exe' + 'bin' ) +const resolveExistingLauncher = (...candidates) => + candidates.find((candidate) => existsSync(candidate)) || null + const quoteBatchArg = (value) => `"${String(value).replaceAll('"', '""')}"` const findMacAppBundle = (startPath) => { @@ -81,32 +85,44 @@ export const resolveAppLaunchPath = () => { // (in-app updates may leave the running process under bin.old). const installDir = readWindowsInstallDirFromRegistry() if (installDir) { - const launcherFromRegistry = path.join(installDir, 'bin', 'launcher.exe') - if (existsSync(launcherFromRegistry)) { + const launcherFromRegistry = resolveExistingLauncher( + path.join(installDir, 'bin', WINDOWS_LAUNCHER_EXE), + path.join(installDir, 'bin', WINDOWS_LEGACY_LAUNCHER_EXE) + ) + if (launcherFromRegistry) { return launcherFromRegistry } } - if (existsSync(WINDOWS_DEFAULT_LAUNCHER)) { - return WINDOWS_DEFAULT_LAUNCHER + 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 === 'launcher.exe') { + if ( + execBase === WINDOWS_LAUNCHER_EXE.toLowerCase() || + execBase === WINDOWS_LEGACY_LAUNCHER_EXE + ) { return process.execPath } - const launcherBesideExec = path.join(path.dirname(process.execPath), 'launcher.exe') - if (existsSync(launcherBesideExec)) { + 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 = path.join( - path.dirname(process.execPath), - 'bin', - 'launcher.exe' + 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 (existsSync(launcherInBin)) { + if (launcherInBin) { return launcherInBin } diff --git a/src/desktop/windows-app-paths.js b/src/desktop/windows-app-paths.js index 2e4e582..1d93ba5 100644 --- a/src/desktop/windows-app-paths.js +++ b/src/desktop/windows-app-paths.js @@ -2,6 +2,8 @@ import { existsSync } from 'node:fs' import { dirname, join } from 'node:path' const NATIVE_WRAPPER_DLL = 'libNativeWrapper.dll' +export const WINDOWS_LAUNCHER_EXE = 'FarmControl.exe' +const LEGACY_LAUNCHER_EXE = 'launcher.exe' export function resolveWindowsBinDir() { const candidates = [ @@ -13,7 +15,8 @@ export function resolveWindowsBinDir() { for (const candidate of candidates) { if ( - existsSync(join(candidate, 'launcher.exe')) || + existsSync(join(candidate, WINDOWS_LAUNCHER_EXE)) || + existsSync(join(candidate, LEGACY_LAUNCHER_EXE)) || existsSync(join(candidate, NATIVE_WRAPPER_DLL)) ) { return candidate