Files
MyGoNavi/internal/app/windows_msi_update.ps1
Syngnat 6e2e3afc85 feat(update): 支持 Windows MSI 与 Portable 分流更新
- 根据当前安装标记精确选择 MSI 或 Portable 更新包
- MSI 更新下载到用户缓存并通过 msiexec 保留原目录安装重启
- 下载与安装前校验包类型并隔离不同安装方式的缓存
- 补充安装模式、脚本执行和更新清理测试
2026-07-13 09:24:13 +08:00

152 lines
5.4 KiB
PowerShell

$ErrorActionPreference = 'Stop'
$Source = $env:GONAVI_UPDATE_SOURCE
$Target = $env:GONAVI_UPDATE_TARGET
$StagedDir = $env:GONAVI_UPDATE_STAGED_DIR
$LogPath = $env:GONAVI_UPDATE_LOG_PATH
$MSILogPath = $env:GONAVI_UPDATE_MSI_LOG_PATH
$MSIExecPath = $env:GONAVI_UPDATE_MSIEXEC_PATH
$HostProcessId = 0
$HostExited = $false
$InstallSucceeded = $false
$LaunchSucceeded = $false
function Write-UpdateLog {
param([string]$Message)
if ([string]::IsNullOrWhiteSpace($LogPath)) {
return
}
try {
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
Add-Content -LiteralPath $LogPath -Value "[$timestamp] $Message" -Encoding UTF8
} catch {
# Logging must never hide the original updater error.
}
}
function Quote-NativeArgument {
param([string]$Value)
return '"' + $Value.Replace('"', '\"') + '"'
}
function Remove-UpdateArtifact {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) {
return
}
try {
if (Test-Path -LiteralPath $Path) {
Remove-Item -LiteralPath $Path -Force -Recurse
}
} catch {
Write-UpdateLog ("cleanup failed for " + $Path + ": " + $_.Exception.Message)
}
}
try {
foreach ($requiredPath in @($Source, $Target, $StagedDir, $LogPath, $MSILogPath, $MSIExecPath)) {
if ([string]::IsNullOrWhiteSpace($requiredPath)) {
throw 'missing required MSI updater path'
}
}
if (-not [int]::TryParse($env:GONAVI_UPDATE_PID, [ref]$HostProcessId) -or $HostProcessId -le 0) {
throw 'invalid host process id'
}
if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) {
throw 'MSI package not found'
}
if (-not [string]::Equals([IO.Path]::GetExtension($Source), '.msi', [StringComparison]::OrdinalIgnoreCase)) {
throw 'update package is not an MSI file'
}
if (-not (Test-Path -LiteralPath $MSIExecPath -PathType Leaf)) {
throw 'msiexec.exe not found'
}
$TargetDir = [IO.Path]::GetDirectoryName($Target)
if ([string]::IsNullOrWhiteSpace($TargetDir) -or -not (Test-Path -LiteralPath $TargetDir -PathType Container)) {
throw 'target directory does not exist'
}
Write-UpdateLog 'MSI updater started'
$waitedSeconds = 0
while (Get-Process -Id $HostProcessId -ErrorAction SilentlyContinue) {
if ($waitedSeconds -ge 90) {
throw 'host process still running after 90 seconds'
}
Start-Sleep -Seconds 1
$waitedSeconds++
}
$HostExited = $true
Write-UpdateLog 'host process exited'
Start-Sleep -Seconds 2
$MsiArguments = @(
'/i',
(Quote-NativeArgument $Source),
('INSTALLFOLDER=' + (Quote-NativeArgument $TargetDir)),
'/passive',
'/norestart',
'/L*v',
(Quote-NativeArgument $MSILogPath)
)
Write-UpdateLog ("launching msiexec; verbose log: " + $MSILogPath)
$InstallerProcess = Start-Process -FilePath $MSIExecPath -Verb RunAs -ArgumentList $MsiArguments -Wait -PassThru -ErrorAction Stop
$InstallerExitCode = $InstallerProcess.ExitCode
Write-UpdateLog ("msiexec exit code: " + $InstallerExitCode)
if ($InstallerExitCode -notin @(0, 1641, 3010)) {
throw ("msiexec failed with exit code " + $InstallerExitCode)
}
$InstallSucceeded = $true
if (-not (Test-Path -LiteralPath $Target -PathType Leaf)) {
throw 'installed application executable not found'
}
Write-UpdateLog ("launching installed application: " + $Target)
$NewProcess = Start-Process -FilePath $Target -WorkingDirectory $TargetDir -PassThru -ErrorAction Stop
Start-Sleep -Milliseconds 1500
$NewProcess.Refresh()
if ($NewProcess.HasExited) {
throw 'updated application exited immediately after launch'
}
$LaunchSucceeded = $true
Remove-UpdateArtifact $Source
Write-UpdateLog 'MSI update finished'
$CleanupCommand = 'Start-Sleep -Seconds 2; Remove-Item -LiteralPath $env:GONAVI_UPDATE_STAGED_DIR -Recurse -Force -ErrorAction SilentlyContinue'
$EncodedCleanupCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($CleanupCommand))
$CleanupWorkingDirectory = [IO.Path]::GetTempPath()
try {
Start-Process -FilePath 'powershell.exe' -WorkingDirectory $CleanupWorkingDirectory -WindowStyle Hidden -ArgumentList @(
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Bypass',
'-EncodedCommand',
$EncodedCleanupCommand
) -ErrorAction Stop | Out-Null
} catch {
Write-UpdateLog ("cleanup scheduler failed: " + $_.Exception.Message)
}
exit 0
} catch {
Write-UpdateLog ("MSI updater failed: " + $_.Exception.Message)
Write-UpdateLog 'MSI package retained for manual install'
if ($HostExited -and -not $LaunchSucceeded -and (Test-Path -LiteralPath $Target -PathType Leaf)) {
try {
$TargetDir = [IO.Path]::GetDirectoryName($Target)
Start-Process -FilePath $Target -WorkingDirectory $TargetDir -ErrorAction Stop | Out-Null
if ($InstallSucceeded) {
Write-UpdateLog 'installed application relaunched after updater failure'
} else {
Write-UpdateLog 'previous application relaunched after MSI failure'
}
} catch {
Write-UpdateLog ("application relaunch failed: " + $_.Exception.Message)
}
}
exit 1
}