Update Windows installer to use FarmControl.exe and enhance process handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- 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.
This commit is contained in:
Tom Butcher 2026-08-04 01:29:31 +01:00
parent 893804a17c
commit eb1ee1e82b
7 changed files with 81 additions and 92 deletions

View File

@ -100,6 +100,7 @@ Var RestartAfterInstall
!insertmacro progressPhase "Stopping Farm Control" !insertmacro progressPhase "Stopping Farm Control"
!insertmacro progressStatus "Stopping running Farm Control processes..." !insertmacro progressStatus "Stopping running Farm Control processes..."
DetailPrint "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 ExecWait 'taskkill /F /IM launcher.exe /T' $R0
!macroend !macroend
@ -126,6 +127,7 @@ Var RestartAfterInstall
ClearErrors ClearErrors
Rename "$INSTDIR\bin" "$INSTDIR\bin.old" Rename "$INSTDIR\bin" "$INSTDIR\bin.old"
${If} ${Errors} ${If} ${Errors}
!insertmacro moveAsideIfPresent "$INSTDIR\bin\FarmControl.exe"
!insertmacro moveAsideIfPresent "$INSTDIR\bin\launcher.exe" !insertmacro moveAsideIfPresent "$INSTDIR\bin\launcher.exe"
!insertmacro moveAsideIfPresent "$INSTDIR\bin\bun.exe" !insertmacro moveAsideIfPresent "$INSTDIR\bin\bun.exe"
!insertmacro moveAsideIfPresent "$INSTDIR\bin\bspatch.exe" !insertmacro moveAsideIfPresent "$INSTDIR\bin\bspatch.exe"
@ -179,14 +181,14 @@ Var RestartAfterInstall
!macro createDesktopShortcut !macro createDesktopShortcut
SetShellVarContext current SetShellVarContext current
SetOutPath "$INSTDIR\bin" 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 !macroend
!macro createStartMenuShortcut !macro createStartMenuShortcut
SetShellVarContext current SetShellVarContext current
CreateDirectory "$SMPROGRAMS\Farm Control" CreateDirectory "$SMPROGRAMS\Farm Control"
SetOutPath "$INSTDIR\bin" 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 !macroend
!macro removeDesktopShortcut !macro removeDesktopShortcut
@ -203,6 +205,7 @@ Var RestartAfterInstall
!macro cleanupUpdateBackups !macro cleanupUpdateBackups
; Best-effort; may fail while the previous process still holds handles. ; Best-effort; may fail while the previous process still holds handles.
RMDir /r "$INSTDIR\bin.old" RMDir /r "$INSTDIR\bin.old"
Delete "$INSTDIR\bin\FarmControl.exe.old"
Delete "$INSTDIR\bin\launcher.exe.old" Delete "$INSTDIR\bin\launcher.exe.old"
Delete "$INSTDIR\bin\bun.exe.old" Delete "$INSTDIR\bin\bun.exe.old"
Delete "$INSTDIR\bin\bspatch.exe.old" Delete "$INSTDIR\bin\bspatch.exe.old"
@ -216,7 +219,7 @@ Var RestartAfterInstall
DeleteRegKey HKCU "Software\Classes\farmcontrol" DeleteRegKey HKCU "Software\Classes\farmcontrol"
WriteRegStr HKCU "Software\Classes\farmcontrol" "" "URL:farmcontrol" WriteRegStr HKCU "Software\Classes\farmcontrol" "" "URL:farmcontrol"
WriteRegStr HKCU "Software\Classes\farmcontrol" "URL Protocol" "" 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" "" ""
WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open" "" "" WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open" "" ""
WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open\command" "" '"$INSTDIR\bin\bun.exe" "$INSTDIR\bin\deeplink.js" "%1"' 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 Sleep 1000
restart_wait_loop: 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 ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq launcher.exe" 2>nul | find /I "launcher.exe"' $R0
${If} $R0 == 0 ${If} $R0 == 0
Sleep 1000 Sleep 1000
@ -256,5 +264,5 @@ Function restartFarmControlAfterUpdate
!insertmacro progressStatus "Starting Farm Control..." !insertmacro progressStatus "Starting Farm Control..."
SetOutPath "$INSTDIR\bin" SetOutPath "$INSTDIR\bin"
Exec "$INSTDIR\bin\launcher.exe" Exec "$INSTDIR\bin\FarmControl.exe"
FunctionEnd FunctionEnd

View File

@ -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." 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" $requiredDeeplinkScript = Join-Path $stagingAppDir "bin\deeplink.js"
if (-not (Test-Path $requiredExe)) { 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)) { if (-not (Test-Path $requiredDeeplinkScript)) {
throw "Staged application is missing bin\deeplink.js" throw "Staged application is missing bin\deeplink.js"

View File

@ -619,7 +619,7 @@ async function main() {
platformDir platformDir
) )
stageWindowsDeeplinkScript(appDir) stageWindowsDeeplinkScript(appDir)
await patchWindowsBinaries(appDir) patchWindowsBinaries(appDir)
let published let published
try { try {

View File

@ -1,79 +1,37 @@
import { copyFileSync, existsSync, unlinkSync } from 'node:fs' import { existsSync, renameSync } from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import rcedit from 'rcedit'
const PATCH_TARGETS = new Set(['launcher.exe']) export const WINDOWS_LAUNCHER_EXE = 'FarmControl.exe'
const MAX_ATTEMPTS = 6 const SOURCE_LAUNCHER_EXE = 'launcher.exe'
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 function patchWindowsBinaries(appDir) {
const binDir = path.join(appDir, 'bin') const binDir = path.join(appDir, 'bin')
if (!existsSync(binDir)) { if (!existsSync(binDir)) {
throw new Error(`patch-windows-binaries: bin directory not found at ${binDir}`) throw new Error(`patch-windows-binaries: bin directory not found at ${binDir}`)
} }
for (const executable of PATCH_TARGETS) { const sourcePath = path.join(binDir, SOURCE_LAUNCHER_EXE)
const executablePath = path.join(binDir, executable) const destPath = path.join(binDir, WINDOWS_LAUNCHER_EXE)
if (!existsSync(executablePath)) {
console.warn(`patch-windows-binaries: skipping missing ${executablePath}`)
continue
}
try { if (existsSync(destPath) && !existsSync(sourcePath)) {
await patchExecutable(executablePath) console.log(`patch-windows-binaries: ${WINDOWS_LAUNCHER_EXE} already present`)
console.log(`patch-windows-binaries: set asInvoker on ${executablePath}`) return
} catch (error) {
console.warn(
`patch-windows-binaries: failed to patch ${executablePath}:`,
error instanceof Error ? error.message : error
)
}
} }
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)) { if (process.argv[1] === fileURLToPath(import.meta.url)) {
@ -83,5 +41,5 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
process.exit(1) process.exit(1)
} }
await patchWindowsBinaries(path.resolve(appDirArg)) patchWindowsBinaries(path.resolve(appDirArg))
} }

View File

@ -29,9 +29,13 @@ console.log('[deeplink] no running instance found, launching Farm Control')
writeDeeplinkSignal(payload) writeDeeplinkSignal(payload)
const launcherPath = join(dirname(process.execPath), 'launcher.exe') const binDir = dirname(process.execPath)
if (!existsSync(launcherPath)) { const launcherPath = ['FarmControl.exe', 'launcher.exe']
console.error('[deeplink] launcher.exe not found at', launcherPath) .map((name) => join(binDir, name))
.find((candidate) => existsSync(candidate))
if (!launcherPath) {
console.error('[deeplink] FarmControl.exe not found in', binDir)
process.exit(1) process.exit(1)
} }

View File

@ -9,14 +9,18 @@ const WINDOWS_INSTALL_DIR_KEYS = [
'HKCU\\Software\\Tom Butcher\\Farm Control', 'HKCU\\Software\\Tom Butcher\\Farm Control',
'HKLM\\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'), process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
'Programs', 'Programs',
'Farm Control', 'Farm Control',
'bin', 'bin'
'launcher.exe'
) )
const resolveExistingLauncher = (...candidates) =>
candidates.find((candidate) => existsSync(candidate)) || null
const quoteBatchArg = (value) => `"${String(value).replaceAll('"', '""')}"` const quoteBatchArg = (value) => `"${String(value).replaceAll('"', '""')}"`
const findMacAppBundle = (startPath) => { const findMacAppBundle = (startPath) => {
@ -81,32 +85,44 @@ export const resolveAppLaunchPath = () => {
// (in-app updates may leave the running process under bin.old). // (in-app updates may leave the running process under bin.old).
const installDir = readWindowsInstallDirFromRegistry() const installDir = readWindowsInstallDirFromRegistry()
if (installDir) { if (installDir) {
const launcherFromRegistry = path.join(installDir, 'bin', 'launcher.exe') const launcherFromRegistry = resolveExistingLauncher(
if (existsSync(launcherFromRegistry)) { path.join(installDir, 'bin', WINDOWS_LAUNCHER_EXE),
path.join(installDir, 'bin', WINDOWS_LEGACY_LAUNCHER_EXE)
)
if (launcherFromRegistry) {
return launcherFromRegistry return launcherFromRegistry
} }
} }
if (existsSync(WINDOWS_DEFAULT_LAUNCHER)) { const defaultLauncher = resolveExistingLauncher(
return WINDOWS_DEFAULT_LAUNCHER 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() 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 return process.execPath
} }
const launcherBesideExec = path.join(path.dirname(process.execPath), 'launcher.exe') const launcherBesideExec = resolveExistingLauncher(
if (existsSync(launcherBesideExec)) { path.join(path.dirname(process.execPath), WINDOWS_LAUNCHER_EXE),
path.join(path.dirname(process.execPath), WINDOWS_LEGACY_LAUNCHER_EXE)
)
if (launcherBesideExec) {
return launcherBesideExec return launcherBesideExec
} }
const launcherInBin = path.join( const launcherInBin = resolveExistingLauncher(
path.dirname(process.execPath), path.join(path.dirname(process.execPath), 'bin', WINDOWS_LAUNCHER_EXE),
'bin', path.join(path.dirname(process.execPath), 'bin', WINDOWS_LEGACY_LAUNCHER_EXE)
'launcher.exe'
) )
if (existsSync(launcherInBin)) { if (launcherInBin) {
return launcherInBin return launcherInBin
} }

View File

@ -2,6 +2,8 @@ import { existsSync } from 'node:fs'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
const NATIVE_WRAPPER_DLL = 'libNativeWrapper.dll' const NATIVE_WRAPPER_DLL = 'libNativeWrapper.dll'
export const WINDOWS_LAUNCHER_EXE = 'FarmControl.exe'
const LEGACY_LAUNCHER_EXE = 'launcher.exe'
export function resolveWindowsBinDir() { export function resolveWindowsBinDir() {
const candidates = [ const candidates = [
@ -13,7 +15,8 @@ export function resolveWindowsBinDir() {
for (const candidate of candidates) { for (const candidate of candidates) {
if ( if (
existsSync(join(candidate, 'launcher.exe')) || existsSync(join(candidate, WINDOWS_LAUNCHER_EXE)) ||
existsSync(join(candidate, LEGACY_LAUNCHER_EXE)) ||
existsSync(join(candidate, NATIVE_WRAPPER_DLL)) existsSync(join(candidate, NATIVE_WRAPPER_DLL))
) { ) {
return candidate return candidate