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 ) $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 } try { Write-UpdateState -State "started" -Message "Binary updater owns the update request." Write-Log "Validating ForgeFlow $ExpectedVersion binary update." if ($HandshakeOnly) { Write-Log "Handshake-only verification completed successfully." exit 0 } $actualSha256 = (Get-FileHash -LiteralPath $BinaryPath -Algorithm SHA256).Hash.ToLowerInvariant() 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 "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 { Copy-Item -LiteralPath $backupPath -Destination $CurrentExecutable -Force Write-UpdateState -State "rolled-back" -Message $_.Exception.Message throw } } 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)." } } $restart = Start-Process -FilePath $CurrentExecutable -WorkingDirectory (Split-Path -Parent $CurrentExecutable) -PassThru Write-Log "ForgeFlow $ExpectedVersion installed; restart PID $($restart.Id)." Write-UpdateState -State "success" -Message "ForgeFlow $ExpectedVersion installed successfully." -RestartLaunched $true } catch { Write-Log $_.Exception.Message $current = $null try { $current = Get-Content -LiteralPath $StatusPath -Raw | ConvertFrom-Json } catch {} if ($current.state -ne "rolled-back") { Write-UpdateState -State "failed" -Message $_.Exception.Message } exit 1 }