Refactor Windows installer for side-by-side updates and improved directory handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Updated the NSIS installer to support side-by-side updates, allowing the running application to remain active while a new version is staged in a separate directory.
- Modified the installation directory handling to ensure the correct paths are used for shortcuts and registry entries.
- Enhanced the update process to include a retry mechanism for folder swaps, improving reliability during updates.
- Adjusted comments and documentation for clarity on the update process and its implications.
This commit is contained in:
Tom Butcher 2026-08-08 22:06:53 +01:00
parent 8d689617f5
commit d2d64ef45d
4 changed files with 61 additions and 56 deletions

View File

@ -60,12 +60,14 @@ Function .onInit
!insertmacro progressStatus "Starting Farm Control installation..."
!insertmacro progressPercent 5
; In-app updates keep the running process alive for progress UI.
; In-app updates install into Farm Control.new while the running process
; keeps using Farm Control; the folders are swapped after exit (/RESTARTFC).
; Interactive / silent fresh installs still remove the previous version first.
${If} $IsInAppUpdate == "1"
!insertmacro prepareInPlaceUpdate
!insertmacro prepareSideBySideUpdate
!insertmacro progressPercent 15
${Else}
StrCpy $FinalInstDir $INSTDIR
!insertmacro uninstallPreviousFarmControl
!insertmacro progressPercent 20
${EndIf}
@ -97,7 +99,7 @@ Section "Farm Control" SecMain
!insertmacro progressStatus "Writing uninstall information..."
!insertmacro progressPercent 90
WriteRegStr HKCU "Software\Tom Butcher\Farm Control" "InstallDir" $INSTDIR
WriteRegStr HKCU "Software\Tom Butcher\Farm Control" "InstallDir" $FinalInstDir
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
"DisplayName" "Farm Control"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
@ -105,7 +107,7 @@ Section "Farm Control" SecMain
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
"Publisher" "Tom Butcher"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
"UninstallString" "$INSTDIR\Uninstall.exe"
"UninstallString" "$FinalInstDir\Uninstall.exe"
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
"NoModify" 1
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \

View File

@ -5,11 +5,13 @@
Var ProgressLogFile
Var IsInAppUpdate
Var RestartAfterInstall
Var FinalInstDir
!macro initProgressLog
StrCpy $ProgressLogFile ""
StrCpy $IsInAppUpdate "0"
StrCpy $RestartAfterInstall "0"
StrCpy $FinalInstDir ""
${GetParameters} $R9
@ -104,36 +106,48 @@ Var RestartAfterInstall
ExecWait 'taskkill /F /IM launcher.exe /T' $R0
!macroend
; Windows allows renaming open executables; move locked binaries aside so File can replace them.
!macro moveAsideIfPresent filePath
${If} ${FileExists} "${filePath}"
Delete "${filePath}.old"
ClearErrors
Rename "${filePath}" "${filePath}.old"
ClearErrors
; Side-by-side update: install into Farm Control.new while the running app
; keeps using Farm Control, then swap folders after the process exits.
!macro prepareSideBySideUpdate
!insertmacro progressPhase "Preparing update"
!insertmacro progressStatus "Preparing staging folder for update..."
StrCpy $FinalInstDir $INSTDIR
StrCpy $INSTDIR "$FinalInstDir.new"
${If} ${FileExists} "$INSTDIR"
DetailPrint "Removing leftover staging folder..."
RMDir /r "$INSTDIR"
${EndIf}
!macroend
; Prefer renaming the whole bin dir (works even with open files inside).
!macro prepareInPlaceUpdate
!insertmacro progressPhase "Preparing update"
!insertmacro progressStatus "Preparing files for update..."
!macro swapUpdateStaging
!insertmacro progressPhase "Applying update"
!insertmacro progressStatus "Replacing Farm Control with the new version..."
DetailPrint "Replacing Farm Control with staged update..."
${If} ${FileExists} "$INSTDIR\bin.old"
RMDir /r "$INSTDIR\bin.old"
StrCpy $R7 0
swap_delete_retry:
RMDir /r "$FinalInstDir"
${If} ${FileExists} "$FinalInstDir"
IntOp $R7 $R7 + 1
${If} $R7 < 20
Sleep 500
Goto swap_delete_retry
${EndIf}
!insertmacro progressFailure "Could not remove the previous Farm Control installation."
Abort
${EndIf}
${If} ${FileExists} "$INSTDIR\bin"
ClearErrors
Rename "$INSTDIR\bin" "$INSTDIR\bin.old"
Rename "$INSTDIR" "$FinalInstDir"
${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"
!insertmacro moveAsideIfPresent "$INSTDIR\bin\zig-zstd.exe"
${EndIf}
!insertmacro progressFailure "Could not activate the new Farm Control installation."
Abort
${EndIf}
StrCpy $INSTDIR $FinalInstDir
!insertmacro progressLog "installer:STATUS:Update files activated"
!macroend
!macro uninstallPreviousFarmControl
@ -180,15 +194,15 @@ Var RestartAfterInstall
!macro createDesktopShortcut
SetShellVarContext current
SetOutPath "$INSTDIR\bin"
CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\FarmControl.exe" "" "$INSTDIR\bin\FarmControl.exe" 0 SW_SHOWNORMAL "" "Farm Control"
SetOutPath "$FinalInstDir\bin"
CreateShortCut "$DESKTOP\Farm Control.lnk" "$FinalInstDir\bin\FarmControl.exe" "" "$FinalInstDir\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\FarmControl.exe" "" "$INSTDIR\bin\FarmControl.exe" 0 SW_SHOWNORMAL "" "Farm Control"
SetOutPath "$FinalInstDir\bin"
CreateShortCut "$SMPROGRAMS\Farm Control\Farm Control.lnk" "$FinalInstDir\bin\FarmControl.exe" "" "$FinalInstDir\bin\FarmControl.exe" 0 SW_SHOWNORMAL "" "Farm Control"
!macroend
!macro removeDesktopShortcut
@ -202,16 +216,6 @@ Var RestartAfterInstall
RMDir "$SMPROGRAMS\Farm Control"
!macroend
!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"
Delete "$INSTDIR\bin\zig-zstd.exe.old"
!macroend
!macro customInstall
!insertmacro progressPhase "Configuring Farm Control"
!insertmacro progressStatus "Registering farmcontrol URI handler..."
@ -219,20 +223,15 @@ 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\FarmControl.exe"
WriteRegStr HKCU "Software\Classes\farmcontrol\DefaultIcon" "" "$FinalInstDir\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"'
WriteRegStr HKCU "Software\Classes\farmcontrol\shell\Open\command" "" '"$FinalInstDir\bin\bun.exe" "$FinalInstDir\bin\deeplink.js" "%1"'
!insertmacro progressStatus "Creating shortcuts..."
DetailPrint "Creating shortcuts"
!insertmacro createDesktopShortcut
!insertmacro createStartMenuShortcut
; Don't remove bin.old during an in-app update the running process still uses it.
${If} $IsInAppUpdate != "1"
!insertmacro cleanupUpdateBackups
${EndIf}
!macroend
!macro customUnInstall
@ -241,7 +240,8 @@ Var RestartAfterInstall
DeleteRegKey HKCU "Software\Classes\farmcontrol"
!macroend
; Wait for the running app to exit, then relaunch. Only used when /RESTARTFC is passed.
; Wait for the running app to exit, swap staged update into place, then relaunch.
; Only used when /RESTARTFC is passed.
Function restartFarmControlAfterUpdate
${If} $RestartAfterInstall != "1"
Return
@ -262,7 +262,11 @@ Function restartFarmControlAfterUpdate
Goto restart_wait_loop
${EndIf}
${If} $IsInAppUpdate == "1"
!insertmacro swapUpdateStaging
${EndIf}
!insertmacro progressStatus "Starting Farm Control..."
SetOutPath "$INSTDIR\bin"
Exec "$INSTDIR\bin\FarmControl.exe"
SetOutPath "$FinalInstDir\bin"
Exec "$FinalInstDir\bin\FarmControl.exe"
FunctionEnd

View File

@ -81,8 +81,7 @@ export const resolveAppLaunchPath = () => {
}
if (process.platform === 'win32') {
// Prefer the installed launcher so post-update restart targets the new binary
// (in-app updates may leave the running process under bin.old).
// Prefer the installed launcher so restarts target the registered install dir.
const installDir = readWindowsInstallDirFromRegistry()
if (installDir) {
const launcherFromRegistry = resolveExistingLauncher(

View File

@ -220,8 +220,8 @@ export const launchWindowsInstaller = async (
// Silent NSIS install in a detached child process (not a batch file).
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
// /UPDATE = in-app update (skip killing this process; overwrite in place)
// /RESTARTFC = installer waits for this process to exit and relaunches the app
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
const installerArgs = [
'/S',