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 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

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."
}
$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"

View File

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

View File

@ -1,80 +1,38 @@
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)
if (existsSync(destPath) && !existsSync(sourcePath)) {
console.log(`patch-windows-binaries: ${WINDOWS_LAUNCHER_EXE} already present`)
return
}
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(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)) {
const appDirArg = process.argv[2]
@ -83,5 +41,5 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
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)
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)
}

View File

@ -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
}

View File

@ -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