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 progressStatus "Starting Farm Control installation..."
!insertmacro progressPercent 5 !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. ; Interactive / silent fresh installs still remove the previous version first.
${If} $IsInAppUpdate == "1" ${If} $IsInAppUpdate == "1"
!insertmacro prepareInPlaceUpdate !insertmacro prepareSideBySideUpdate
!insertmacro progressPercent 15 !insertmacro progressPercent 15
${Else} ${Else}
StrCpy $FinalInstDir $INSTDIR
!insertmacro uninstallPreviousFarmControl !insertmacro uninstallPreviousFarmControl
!insertmacro progressPercent 20 !insertmacro progressPercent 20
${EndIf} ${EndIf}
@ -97,7 +99,7 @@ Section "Farm Control" SecMain
!insertmacro progressStatus "Writing uninstall information..." !insertmacro progressStatus "Writing uninstall information..."
!insertmacro progressPercent 90 !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" \ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
"DisplayName" "Farm Control" "DisplayName" "Farm Control"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\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" \ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
"Publisher" "Tom Butcher" "Publisher" "Tom Butcher"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \ 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" \ WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \
"NoModify" 1 "NoModify" 1
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \ WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" \

View File

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

View File

@ -81,8 +81,7 @@ export const resolveAppLaunchPath = () => {
} }
if (process.platform === 'win32') { if (process.platform === 'win32') {
// Prefer the installed launcher so post-update restart targets the new binary // Prefer the installed launcher so restarts target the registered install dir.
// (in-app updates may leave the running process under bin.old).
const installDir = readWindowsInstallDirFromRegistry() const installDir = readWindowsInstallDirFromRegistry()
if (installDir) { if (installDir) {
const launcherFromRegistry = resolveExistingLauncher( 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). // Silent NSIS install in a detached child process (not a batch file).
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall) // /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
// /UPDATE = in-app update (skip killing this process; overwrite in place) // /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
// /RESTARTFC = installer waits for this process to exit and relaunches the app // /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS) // /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
const installerArgs = [ const installerArgs = [
'/S', '/S',