146 lines
6.6 KiB
PowerShell
146 lines
6.6 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)][string]$BinaryPath,
|
|
[Parameter(Mandatory = $true)][string]$ExpectedSha256,
|
|
[Parameter(Mandatory = $true)][string]$ExpectedVersion,
|
|
[Parameter(Mandatory = $true)][string]$CurrentExecutable,
|
|
[Parameter(Mandatory = $true)][string]$Portable,
|
|
[Parameter(Mandatory = $true)][int]$ParentPid,
|
|
[Parameter(Mandatory = $true)][string]$LogPath,
|
|
[Parameter(Mandatory = $true)][string]$StatusPath,
|
|
[Parameter(Mandatory = $true)][string]$UpdateId,
|
|
[switch]$HandshakeOnly,
|
|
[switch]$VerifyOnly
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$isPortable = $Portable -eq "True"
|
|
|
|
function Write-UpdateState {
|
|
param([string]$State, [string]$Message = "", [bool]$RestartLaunched = $false)
|
|
$payload = [ordered]@{
|
|
schemaVersion = 1
|
|
updateId = $UpdateId
|
|
state = $State
|
|
expectedVersion = $ExpectedVersion
|
|
installedVersion = if ($State -eq "success") { $ExpectedVersion } else { $null }
|
|
message = $Message
|
|
restartLaunched = $RestartLaunched
|
|
logPath = $LogPath
|
|
updatedAt = [DateTime]::UtcNow.ToString("o")
|
|
}
|
|
if ($State -in @("success", "failed", "rolled-back")) { $payload.completedAt = [DateTime]::UtcNow.ToString("o") }
|
|
$directory = Split-Path -Parent $StatusPath
|
|
if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null }
|
|
$temporary = "$StatusPath.$PID.tmp"
|
|
$backup = "$StatusPath.$PID.bak"
|
|
$json = $payload | ConvertTo-Json -Depth 4
|
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
[IO.File]::WriteAllText($temporary, $json, $utf8NoBom)
|
|
try {
|
|
if ([IO.File]::Exists($StatusPath)) {
|
|
[IO.File]::Replace($temporary, $StatusPath, $backup)
|
|
[IO.File]::Delete($backup)
|
|
} else {
|
|
[IO.File]::Move($temporary, $StatusPath)
|
|
}
|
|
} catch {
|
|
[IO.File]::Copy($temporary, $StatusPath, $true)
|
|
[IO.File]::Delete($temporary)
|
|
if ([IO.File]::Exists($backup)) { [IO.File]::Delete($backup) }
|
|
}
|
|
}
|
|
function Write-Log([string]$Message) {
|
|
"{0} {1}" -f [DateTime]::UtcNow.ToString("o"), $Message | Add-Content -LiteralPath $LogPath -Encoding UTF8
|
|
}
|
|
function Get-Sha256([string]$Path) {
|
|
$stream = [IO.File]::OpenRead($Path)
|
|
$algorithm = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
return ([BitConverter]::ToString($algorithm.ComputeHash($stream))).Replace("-", "").ToLowerInvariant()
|
|
} finally {
|
|
$algorithm.Dispose()
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
function Start-ForgeFlowAndVerify([string]$Executable) {
|
|
$process = Start-Process -FilePath $Executable -WorkingDirectory (Split-Path -Parent $Executable) -PassThru
|
|
Start-Sleep -Milliseconds 1500
|
|
if (-not $process -or $process.HasExited) { throw "ForgeFlow restart process exited before the application could stay running." }
|
|
return $process
|
|
}
|
|
|
|
try {
|
|
Write-Log "Validating ForgeFlow $ExpectedVersion binary update."
|
|
if ($HandshakeOnly) {
|
|
Write-UpdateState -State "started" -Message "Binary updater owns the update request."
|
|
Write-Log "Handshake-only verification completed successfully."
|
|
exit 0
|
|
}
|
|
$actualSha256 = Get-Sha256 -Path $BinaryPath
|
|
if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) { throw "Binary update SHA-256 verification failed." }
|
|
if (-not (Test-Path -LiteralPath $CurrentExecutable -PathType Leaf)) { throw "Current ForgeFlow executable was not found." }
|
|
Write-UpdateState -State "started" -Message "Binary preflight passed; updater owns the update request."
|
|
if ($VerifyOnly) {
|
|
Write-Log "Verification-only SHA-256 check completed successfully."
|
|
exit 0
|
|
}
|
|
|
|
Write-UpdateState -State "waiting-for-exit" -Message "Waiting for ForgeFlow to close."
|
|
try { Wait-Process -Id $ParentPid -Timeout 60 -ErrorAction Stop } catch {
|
|
if (Get-Process -Id $ParentPid -ErrorAction SilentlyContinue) { throw "ForgeFlow did not close within 60 seconds." }
|
|
}
|
|
|
|
if ($isPortable) {
|
|
Write-UpdateState -State "applying" -Message "Replacing the portable executable."
|
|
$backupPath = "$CurrentExecutable.previous"
|
|
Copy-Item -LiteralPath $CurrentExecutable -Destination $backupPath -Force
|
|
try {
|
|
Copy-Item -LiteralPath $BinaryPath -Destination $CurrentExecutable -Force
|
|
} catch {
|
|
$copyFailure = $_.Exception.Message
|
|
try {
|
|
Copy-Item -LiteralPath $backupPath -Destination $CurrentExecutable -Force
|
|
$rollbackRestart = Start-ForgeFlowAndVerify -Executable $CurrentExecutable
|
|
Write-Log "Portable replacement failed; previous ForgeFlow restored and restarted as PID $($rollbackRestart.Id)."
|
|
Write-UpdateState -State "rolled-back" -Message $copyFailure -RestartLaunched $true
|
|
} catch {
|
|
Write-UpdateState -State "failed" -Message "$copyFailure Rollback also failed: $($_.Exception.Message)" -RestartLaunched $false
|
|
}
|
|
throw $copyFailure
|
|
}
|
|
} else {
|
|
Write-UpdateState -State "applying" -Message "Running the verified ForgeFlow installer."
|
|
$installer = Start-Process -FilePath $BinaryPath -ArgumentList "/S" -PassThru -Wait -WindowStyle Hidden
|
|
if ($installer.ExitCode -ne 0) { throw "ForgeFlow installer exited with code $($installer.ExitCode)." }
|
|
}
|
|
|
|
try {
|
|
$restart = Start-ForgeFlowAndVerify -Executable $CurrentExecutable
|
|
Write-Log "ForgeFlow $ExpectedVersion installed; verified restart PID $($restart.Id)."
|
|
Write-UpdateState -State "success" -Message "ForgeFlow $ExpectedVersion installed successfully." -RestartLaunched $true
|
|
} catch {
|
|
$restartFailure = $_.Exception.Message
|
|
if ($isPortable -and $backupPath -and (Test-Path -LiteralPath $backupPath -PathType Leaf)) {
|
|
Write-Log "Updated portable executable failed its restart probe; restoring the previous executable."
|
|
try {
|
|
Copy-Item -LiteralPath $backupPath -Destination $CurrentExecutable -Force
|
|
$rollbackRestart = Start-ForgeFlowAndVerify -Executable $CurrentExecutable
|
|
Write-Log "Previous ForgeFlow restored and restarted as PID $($rollbackRestart.Id)."
|
|
Write-UpdateState -State "rolled-back" -Message $restartFailure -RestartLaunched $true
|
|
} catch {
|
|
Write-UpdateState -State "failed" -Message "$restartFailure Rollback also failed: $($_.Exception.Message)" -RestartLaunched $false
|
|
}
|
|
exit 1
|
|
}
|
|
Write-Log "ForgeFlow $ExpectedVersion installed, but automatic restart failed: $restartFailure"
|
|
Write-UpdateState -State "success" -Message "ForgeFlow $ExpectedVersion installed successfully, but must be started manually." -RestartLaunched $false
|
|
}
|
|
} catch {
|
|
Write-Log $_.Exception.Message
|
|
$current = $null
|
|
try { $current = Get-Content -LiteralPath $StatusPath -Raw | ConvertFrom-Json } catch {}
|
|
if ($current.state -notin @("rolled-back", "failed")) { Write-UpdateState -State "failed" -Message $_.Exception.Message }
|
|
exit 1
|
|
}
|