Compare commits

..

No commits in common. "28295f59122e70102ec5e228535dd2d29b6c857b" and "67511e8b8c52a1a35bdeaa541914f9e20ea66475" have entirely different histories.

5 changed files with 21 additions and 194 deletions

View File

@ -18,14 +18,6 @@
!define APP_SOURCE_DIR "app" !define APP_SOURCE_DIR "app"
!endif !endif
!ifndef APP_FILE_LIST_GENERATOR
!define APP_FILE_LIST_GENERATOR "..\..\scripts\generate-nsis-file-list.ps1"
!endif
; Enumerate APP_SOURCE_DIR now, while compiling, so the generated installer
; knows the exact unpacked byte total and can log each extracted file.
!insertmacro compileProgressFileCopy "${APP_SOURCE_DIR}" "${APP_FILE_LIST_GENERATOR}"
Name "Farm Control" Name "Farm Control"
OutFile "${OUTFILE}" OutFile "${OUTFILE}"
InstallDir "$LOCALAPPDATA\Programs\Farm Control" InstallDir "$LOCALAPPDATA\Programs\Farm Control"
@ -66,17 +58,18 @@ Function .onInit
!insertmacro progressPhase "Starting installation" !insertmacro progressPhase "Starting installation"
!insertmacro progressStatus "Starting Farm Control installation..." !insertmacro progressStatus "Starting Farm Control installation..."
!insertmacro progressCopyTotal "${APP_COPY_TOTAL_BYTES}" !insertmacro progressPercent 5
!insertmacro progressPercent 0
; In-app updates install into Farm Control.new while the running process ; In-app updates install into Farm Control.new while the running process
; keeps using Farm Control; the folders are swapped after exit (/RESTARTFC). ; 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 prepareSideBySideUpdate !insertmacro prepareSideBySideUpdate
!insertmacro progressPercent 15
${Else} ${Else}
StrCpy $FinalInstDir $INSTDIR StrCpy $FinalInstDir $INSTDIR
!insertmacro uninstallPreviousFarmControl !insertmacro uninstallPreviousFarmControl
!insertmacro progressPercent 20
${EndIf} ${EndIf}
FunctionEnd FunctionEnd
@ -93,10 +86,11 @@ Section "Farm Control" SecMain
!insertmacro progressPhase "Copying application files" !insertmacro progressPhase "Copying application files"
!insertmacro progressStatus "Copying application files..." !insertmacro progressStatus "Copying application files..."
!insertmacro progressPercent 0 !insertmacro progressPercent 35
SetOutPath $INSTDIR
SetOverwrite try SetOverwrite try
!insertmacro copyApplicationFilesWithProgress File /r "${APP_SOURCE_DIR}\*.*"
!insertmacro progressPercent 75 !insertmacro progressPercent 75
!insertmacro customInstall !insertmacro customInstall

View File

@ -6,25 +6,12 @@ Var ProgressLogFile
Var IsInAppUpdate Var IsInAppUpdate
Var RestartAfterInstall Var RestartAfterInstall
Var FinalInstDir Var FinalInstDir
Var UpdateParentPid
; Generate an include at compile time that defines APP_COPY_TOTAL_BYTES and
; copyApplicationFilesWithProgress. The generated macro contains one File
; command and one byte-progress log entry for every file in the source tree.
!macro compileProgressFileCopy sourceDir generator
!tempfile GENERATED_FILE_COPY_INCLUDE
!system 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${generator}" -SourceDir "${sourceDir}" -OutputPath "${GENERATED_FILE_COPY_INCLUDE}"' = 0
!include "${GENERATED_FILE_COPY_INCLUDE}"
!delfile "${GENERATED_FILE_COPY_INCLUDE}"
!undef GENERATED_FILE_COPY_INCLUDE
!macroend
!macro initProgressLog !macro initProgressLog
StrCpy $ProgressLogFile "" StrCpy $ProgressLogFile ""
StrCpy $IsInAppUpdate "0" StrCpy $IsInAppUpdate "0"
StrCpy $RestartAfterInstall "0" StrCpy $RestartAfterInstall "0"
StrCpy $FinalInstDir "" StrCpy $FinalInstDir ""
StrCpy $UpdateParentPid "0"
${GetParameters} $R9 ${GetParameters} $R9
@ -40,13 +27,6 @@ Var UpdateParentPid
StrCpy $RestartAfterInstall "1" StrCpy $RestartAfterInstall "1"
${EndIf} ${EndIf}
ClearErrors
${GetOptions} $R9 "/PARENTPID=" $R8
${IfNot} ${Errors}
; Convert to an integer before it is used in Win32 calls or taskkill.
IntOp $UpdateParentPid $R8 + 0
${EndIf}
; Prefer env var so paths with spaces are reliable; /LOG= remains supported. ; Prefer env var so paths with spaces are reliable; /LOG= remains supported.
ReadEnvStr $ProgressLogFile "FARMCONTROL_INSTALL_LOG" ReadEnvStr $ProgressLogFile "FARMCONTROL_INSTALL_LOG"
${If} $ProgressLogFile == "" ${If} $ProgressLogFile == ""
@ -107,14 +87,6 @@ Var UpdateParentPid
!insertmacro progressLog "installer:STATUS:${status}" !insertmacro progressLog "installer:STATUS:${status}"
!macroend !macroend
!macro progressCopyTotal bytes
!insertmacro progressLog "installer:COPY_TOTAL:${bytes}"
!macroend
!macro progressCopyFile bytes relativePath
!insertmacro progressLog "installer:COPY_FILE:${bytes}:${relativePath}"
!macroend
!macro progressSuccess !macro progressSuccess
!insertmacro progressPercent 100 !insertmacro progressPercent 100
!insertmacro progressStatus "Installation complete. Restarting Farm Control..." !insertmacro progressStatus "Installation complete. Restarting Farm Control..."
@ -339,49 +311,22 @@ Function restartFarmControlAfterUpdate
!insertmacro progressSuccess !insertmacro progressSuccess
!insertmacro progressStatus "Waiting for Farm Control to close..." !insertmacro progressStatus "Waiting for Farm Control to close..."
Sleep 2000
${If} $UpdateParentPid > 0
; Wait for the exact updater process rather than an executable name. CEF
; builds can keep bun/renderer processes alive after the launcher exits.
System::Call 'kernel32::OpenProcess(i 0x00100000, i 0, i $UpdateParentPid) p .r0'
${If} $0 != 0
; Allow two minutes for a graceful CEF shutdown.
System::Call 'kernel32::WaitForSingleObject(p r0, i 120000) i .r1'
System::Call 'kernel32::CloseHandle(p r0)'
${If} $1 == 258
!insertmacro progressStatus "Farm Control is taking too long to close; forcing shutdown..."
DetailPrint "Farm Control process $UpdateParentPid did not exit; terminating its process tree..."
ExecWait 'taskkill /F /T /PID $UpdateParentPid' $R0
${EndIf}
${EndIf}
${Else}
; Compatibility fallback for manually launched older installers.
StrCpy $R7 0
restart_wait_loop: restart_wait_loop:
ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq FarmControl.exe" 2>nul | find /I "FarmControl.exe"' $R0 ExecWait 'cmd.exe /c tasklist /FI "IMAGENAME eq FarmControl.exe" 2>nul | find /I "FarmControl.exe"' $R0
${If} $R0 != 0 ${If} $R0 == 0
Sleep 1500
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
${EndIf} ${If} $R0 == 0
${If} $R0 != 0
Goto restart_wait_done
${EndIf}
IntOp $R7 $R7 + 1
${If} $R7 < 80
Sleep 1500 Sleep 1500
Goto restart_wait_loop Goto restart_wait_loop
${EndIf} ${EndIf}
!insertmacro progressStatus "Farm Control is taking too long to close; forcing shutdown..."
!insertmacro quitFarmControl
${EndIf}
restart_wait_done:
; Give CEF descendants a moment to release DLLs before renaming the tree.
Sleep 3000
${If} $IsInAppUpdate == "1" ${If} $IsInAppUpdate == "1"
Sleep 1000
!insertmacro swapUpdateStaging !insertmacro swapUpdateStaging
${EndIf} ${EndIf}

View File

@ -48,7 +48,6 @@ function Get-DirectoryBytes {
$rootDir = Split-Path -Parent $PSScriptRoot $rootDir = Split-Path -Parent $PSScriptRoot
$nsiPath = Join-Path $rootDir "packaging/windows/farmcontrol.nsi" $nsiPath = Join-Path $rootDir "packaging/windows/farmcontrol.nsi"
$installerInclude = Join-Path $rootDir "packaging/windows/installer.nsh" $installerInclude = Join-Path $rootDir "packaging/windows/installer.nsh"
$fileListGenerator = Join-Path $rootDir "scripts/generate-nsis-file-list.ps1"
$workDir = Join-Path $env:TEMP "farmcontrol-nsis" $workDir = Join-Path $env:TEMP "farmcontrol-nsis"
$localInstallerName = "farmcontrol-installer.exe" $localInstallerName = "farmcontrol-installer.exe"
@ -81,7 +80,6 @@ if (-not (Test-Path $requiredDeeplinkScript)) {
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol.nsi") Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol.nsi")
Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "installer.nsh") Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "installer.nsh")
Copy-Item -LiteralPath $fileListGenerator -Destination (Join-Path $workDir "generate-nsis-file-list.ps1")
$iconPath = Join-Path $rootDir "assets\installer.ico" $iconPath = Join-Path $rootDir "assets\installer.ico"
if (Test-Path $iconPath) { if (Test-Path $iconPath) {
@ -109,7 +107,6 @@ $makensisArgs = @(
"/DVERSION=$Version" "/DVERSION=$Version"
"/DBUILD_NUMBER=$BuildNumber" "/DBUILD_NUMBER=$BuildNumber"
"/DAPP_SOURCE_DIR=app" "/DAPP_SOURCE_DIR=app"
"/DAPP_FILE_LIST_GENERATOR=generate-nsis-file-list.ps1"
) )
if (Test-Path (Join-Path $workDir "icon.ico")) { if (Test-Path (Join-Path $workDir "icon.ico")) {

View File

@ -1,62 +0,0 @@
param(
[Parameter(Mandatory = $true)]
[string]$SourceDir,
[Parameter(Mandatory = $true)]
[string]$OutputPath
)
$ErrorActionPreference = "Stop"
function ConvertTo-NsisString {
param([string]$Value)
return $Value.Replace('$', '$$').Replace('"', '$\"')
}
$sourceRoot = (Resolve-Path -LiteralPath $SourceDir).Path.TrimEnd('\')
$sourcePrefix = "$sourceRoot\"
$files = @(
Get-ChildItem -LiteralPath $sourceRoot -Recurse -File -Force |
Sort-Object FullName
)
if ($files.Count -eq 0) {
throw "No files were found in NSIS application source directory: $sourceRoot"
}
[long]$totalBytes = ($files | Measure-Object -Property Length -Sum).Sum
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("!define APP_COPY_TOTAL_BYTES `"$totalBytes`"")
$lines.Add("!macro copyApplicationFilesWithProgress")
$lastRelativeDirectory = $null
foreach ($file in $files) {
$relativePath = $file.FullName.Substring($sourcePrefix.Length)
$relativeDirectory = Split-Path -Parent $relativePath
$escapedRelativePath = ConvertTo-NsisString $relativePath
$escapedSourcePath = ConvertTo-NsisString $file.FullName
$escapedFileName = ConvertTo-NsisString $file.Name
if ($relativeDirectory -ne $lastRelativeDirectory) {
if ([string]::IsNullOrEmpty($relativeDirectory)) {
$lines.Add(' SetOutPath "$INSTDIR"')
} else {
$escapedDirectory = ConvertTo-NsisString $relativeDirectory
$lines.Add(" SetOutPath `"`$INSTDIR\$escapedDirectory`"")
}
$lastRelativeDirectory = $relativeDirectory
}
$lines.Add(" File `"/oname=$escapedFileName`" `"$escapedSourcePath`"")
$lines.Add(
" !insertmacro progressCopyFile `"$($file.Length)`" `"$escapedRelativePath`""
)
}
$lines.Add("!macroend")
$encoding = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllLines($OutputPath, $lines, $encoding)
Write-Host "Generated NSIS commands for $($files.Count) files ($totalBytes bytes)."

View File

@ -4,22 +4,13 @@ import os from 'os'
import path from 'path' import path from 'path'
const PE_MZ_HEADER = Buffer.from([0x4d, 0x5a]) // "MZ" const PE_MZ_HEADER = Buffer.from([0x4d, 0x5a]) // "MZ"
const COPY_PROGRESS_PERCENT = 75
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const formatBytes = (bytes) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`
}
const parseWindowsInstallerProgress = (output) => { const parseWindowsInstallerProgress = (output) => {
const lines = String(output || '').split(/\r?\n/) const lines = String(output || '').split(/\r?\n/)
let percent = null let percent = null
let message = 'Installing update...' let message = 'Installing update...'
let copyTotalBytes = 0
let copiedBytes = 0
for (const line of lines) { for (const line of lines) {
if (line.startsWith('installer:PHASE:')) { if (line.startsWith('installer:PHASE:')) {
@ -32,41 +23,11 @@ const parseWindowsInstallerProgress = (output) => {
if (Number.isFinite(value)) { if (Number.isFinite(value)) {
percent = Math.min(100, Math.round(value <= 1 ? value * 100 : value)) percent = Math.min(100, Math.round(value <= 1 ? value * 100 : value))
} }
} else if (line.startsWith('installer:COPY_TOTAL:')) {
const value = Number.parseInt(
line.slice('installer:COPY_TOTAL:'.length),
10
)
if (Number.isFinite(value) && value > 0) {
copyTotalBytes = value
copiedBytes = 0
}
} else if (line.startsWith('installer:COPY_FILE:')) {
const copyFile = line.match(/^installer:COPY_FILE:(\d+):(.*)$/)
if (copyFile) {
const fileBytes = Number.parseInt(copyFile[1], 10)
const relativePath = copyFile[2].trim()
if (Number.isFinite(fileBytes)) {
copiedBytes += fileBytes
if (copyTotalBytes > 0) {
percent = Math.min(
COPY_PROGRESS_PERCENT,
Math.round((copiedBytes / copyTotalBytes) * COPY_PROGRESS_PERCENT)
)
}
}
if (relativePath) {
message = `Copying ${relativePath} (${formatBytes(fileBytes)})`
}
}
} else if ( } else if (
line.startsWith('installer: ') && line.startsWith('installer: ') &&
!line.startsWith('installer:PHASE:') && !line.startsWith('installer:PHASE:') &&
!line.startsWith('installer:STATUS:') && !line.startsWith('installer:STATUS:') &&
!line.startsWith('installer:%') && !line.startsWith('installer:%')
!line.startsWith('installer:COPY_')
) { ) {
const text = line.slice('installer: '.length).trim() const text = line.slice('installer: '.length).trim()
if (text) message = text if (text) message = text
@ -262,16 +223,8 @@ export const launchWindowsInstaller = async (
// /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall) // /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
// /UPDATE = in-app update (stage into Farm Control.new; swap after exit) // /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
// /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches // /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
// /PARENTPID = exact process to wait for (CEF may outlive the launcher) // /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log const installerArgs = ['/S', '/UPDATE', '/RESTARTFC', `/LOG=${logPath}`]
// (installer:% / PHASE / STATUS / COPY_TOTAL / COPY_FILE)
const installerArgs = [
'/S',
'/UPDATE',
'/RESTARTFC',
`/PARENTPID=${process.pid}`,
`/LOG=${logPath}`
]
const installerProcess = spawn(resolvedPath, installerArgs, { const installerProcess = spawn(resolvedPath, installerArgs, {
detached: true, detached: true,