Implement file enumeration and progress tracking in Windows installer
- Added a new PowerShell script to generate NSIS file lists, enabling the installer to track file copy progress. - Updated the NSIS scripts to include macros for logging total bytes and individual file copy progress during installation. - Enhanced the `winappupdate.js` to parse and display copy progress information, improving user feedback during updates.
This commit is contained in:
parent
67511e8b8c
commit
2bbecb0d6a
@ -18,6 +18,14 @@
|
|||||||
!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"
|
||||||
@ -58,18 +66,17 @@ Function .onInit
|
|||||||
|
|
||||||
!insertmacro progressPhase "Starting installation"
|
!insertmacro progressPhase "Starting installation"
|
||||||
!insertmacro progressStatus "Starting Farm Control installation..."
|
!insertmacro progressStatus "Starting Farm Control installation..."
|
||||||
!insertmacro progressPercent 5
|
!insertmacro progressCopyTotal "${APP_COPY_TOTAL_BYTES}"
|
||||||
|
!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
|
||||||
|
|
||||||
@ -86,11 +93,10 @@ 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 35
|
!insertmacro progressPercent 0
|
||||||
|
|
||||||
SetOutPath $INSTDIR
|
|
||||||
SetOverwrite try
|
SetOverwrite try
|
||||||
File /r "${APP_SOURCE_DIR}\*.*"
|
!insertmacro copyApplicationFilesWithProgress
|
||||||
|
|
||||||
!insertmacro progressPercent 75
|
!insertmacro progressPercent 75
|
||||||
!insertmacro customInstall
|
!insertmacro customInstall
|
||||||
|
|||||||
@ -7,6 +7,17 @@ Var IsInAppUpdate
|
|||||||
Var RestartAfterInstall
|
Var RestartAfterInstall
|
||||||
Var FinalInstDir
|
Var FinalInstDir
|
||||||
|
|
||||||
|
; 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"
|
||||||
@ -87,6 +98,14 @@ Var FinalInstDir
|
|||||||
!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..."
|
||||||
|
|||||||
@ -48,6 +48,7 @@ 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"
|
||||||
|
|
||||||
@ -80,6 +81,7 @@ 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) {
|
||||||
@ -107,6 +109,7 @@ $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")) {
|
||||||
|
|||||||
62
scripts/generate-nsis-file-list.ps1
Normal file
62
scripts/generate-nsis-file-list.ps1
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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)."
|
||||||
@ -4,13 +4,22 @@ 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:')) {
|
||||||
@ -23,11 +32,41 @@ 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
|
||||||
@ -223,7 +262,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
|
||||||
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log (installer:% / PHASE / STATUS)
|
// /LOG= + FARMCONTROL_INSTALL_LOG = progress log
|
||||||
|
// (installer:% / PHASE / STATUS / COPY_TOTAL / COPY_FILE)
|
||||||
const installerArgs = ['/S', '/UPDATE', '/RESTARTFC', `/LOG=${logPath}`]
|
const installerArgs = ['/S', '/UPDATE', '/RESTARTFC', `/LOG=${logPath}`]
|
||||||
|
|
||||||
const installerProcess = spawn(resolvedPath, installerArgs, {
|
const installerProcess = spawn(resolvedPath, installerArgs, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user