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 }