995 lines
39 KiB
PowerShell
995 lines
39 KiB
PowerShell
$ErrorActionPreference = "Stop"
|
|
|
|
$taskName = "SDR Rover RX Agent"
|
|
$sourceRoot = [IO.Path]::GetFullPath((Split-Path -Parent $PSScriptRoot))
|
|
$installRoot = Join-Path $env:LOCALAPPDATA "SDR_Rover_RX_Agent"
|
|
$versionsRoot = Join-Path $installRoot "versions"
|
|
$launcherRoot = Join-Path $installRoot "launcher"
|
|
$sharedRoot = Join-Path $installRoot "shared"
|
|
$logsRoot = Join-Path $installRoot "logs"
|
|
$repository = Join-Path $env:USERPROFILE "Documents\Codex\SDR_Rover_Video"
|
|
$python = Join-Path $repository ".venv\Scripts\pythonw.exe"
|
|
$hackrfRoot = Join-Path $env:USERPROFILE "Documents\Codex\sdr-tools\envs\sdr\Library\bin"
|
|
$workRoot = Join-Path $env:USERPROFILE "Documents\SDR_Rover_Runtime"
|
|
$manifestPath = Join-Path $sourceRoot "BOOTSTRAP_MANIFEST.json"
|
|
$signaturePath = Join-Path $sourceRoot "BOOTSTRAP_MANIFEST.hmac"
|
|
$legacyToken = Join-Path $installRoot "app\.local\rf001_http_token.txt"
|
|
$sharedToken = Join-Path $sharedRoot "rf001_http_token.txt"
|
|
$expectedReferenceHash = "fa260719659b2984fe29588238415622cd75c6daefec6cc9130ba1bf851bd3f9"
|
|
|
|
function Get-LowerSha256([string]$Path) {
|
|
$stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
return ([BitConverter]::ToString($sha256.ComputeHash($stream))).Replace("-", "").ToLowerInvariant()
|
|
}
|
|
finally {
|
|
$sha256.Dispose()
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
function Test-FixedTimeHex([string]$Left, [string]$Right) {
|
|
if ($Left.Length -ne $Right.Length) {
|
|
return $false
|
|
}
|
|
$difference = 0
|
|
for ($index = 0; $index -lt $Left.Length; $index++) {
|
|
$difference = $difference -bor ([int][char]$Left[$index] -bxor [int][char]$Right[$index])
|
|
}
|
|
return $difference -eq 0
|
|
}
|
|
|
|
function Assert-ExactReleasePath(
|
|
[string]$InstallRoot,
|
|
[string]$VersionsRoot,
|
|
[string]$ReleaseId,
|
|
[string]$TargetRelease
|
|
) {
|
|
$expectedVersions = [IO.Path]::GetFullPath((Join-Path $InstallRoot "versions"))
|
|
$actualVersions = [IO.Path]::GetFullPath($VersionsRoot)
|
|
if (-not $expectedVersions.Equals($actualVersions, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Release versions root is outside the fixed install root"
|
|
}
|
|
$expectedTarget = [IO.Path]::GetFullPath((Join-Path $actualVersions $ReleaseId))
|
|
$actualTarget = [IO.Path]::GetFullPath($TargetRelease)
|
|
if (-not $expectedTarget.Equals($actualTarget, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Release target path is not the installer-defined canonical path"
|
|
}
|
|
}
|
|
|
|
function Get-ReleaseDirectoryState(
|
|
[string]$TargetRelease,
|
|
[object[]]$ReleaseRows,
|
|
[string]$ReleasePrefix
|
|
) {
|
|
if (-not (Test-Path -LiteralPath $TargetRelease)) {
|
|
return [pscustomobject]@{
|
|
state = "TARGET_ABSENT"
|
|
mismatches = @()
|
|
conflicting_files = @()
|
|
}
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $TargetRelease -PathType Container)) {
|
|
$item = Get-Item -LiteralPath $TargetRelease -Force
|
|
$digest = $null
|
|
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0) {
|
|
try {
|
|
$digest = Get-LowerSha256 $item.FullName
|
|
}
|
|
catch {
|
|
$digest = $null
|
|
}
|
|
}
|
|
return [pscustomobject]@{
|
|
state = "TARGET_CONFLICT"
|
|
mismatches = @([ordered]@{
|
|
kind = "TARGET_NOT_DIRECTORY"
|
|
path = "."
|
|
expected_bytes = $null
|
|
actual_bytes = [int64]$item.Length
|
|
expected_sha256 = $null
|
|
actual_sha256 = $digest
|
|
})
|
|
conflicting_files = @([ordered]@{
|
|
path = "."
|
|
bytes = [int64]$item.Length
|
|
sha256 = $digest
|
|
reparse_point = (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)
|
|
})
|
|
}
|
|
}
|
|
|
|
$expected = @{}
|
|
$normalizedRows = @($ReleaseRows | ForEach-Object { $_ })
|
|
foreach ($row in $normalizedRows) {
|
|
$pathValues = @($row.path)
|
|
$byteValues = @($row.bytes)
|
|
$hashValues = @($row.sha256)
|
|
if ($pathValues.Count -ne 1 -or $byteValues.Count -ne 1 -or $hashValues.Count -ne 1) {
|
|
throw "Release manifest row fields are absent or ambiguous"
|
|
}
|
|
$manifestPath = [string]$pathValues[0]
|
|
if (-not $manifestPath.StartsWith($ReleasePrefix, [StringComparison]::Ordinal)) {
|
|
throw "Release manifest row is outside the expected prefix: $manifestPath"
|
|
}
|
|
$relative = $manifestPath.Substring($ReleasePrefix.Length)
|
|
if (
|
|
[string]::IsNullOrWhiteSpace($relative) -or
|
|
$relative -match '(^/|\\|(^|/)\.\.(/|$)|:)'
|
|
) {
|
|
throw "Unsafe path inside release manifest: $relative"
|
|
}
|
|
$key = $relative.ToLowerInvariant()
|
|
if ($expected.ContainsKey($key)) {
|
|
throw "Duplicate path inside release manifest: $relative"
|
|
}
|
|
$expected[$key] = [pscustomobject]@{
|
|
path = $relative
|
|
bytes = [int64]($byteValues[0])
|
|
sha256 = ([string]$hashValues[0]).ToLowerInvariant()
|
|
}
|
|
}
|
|
if ($expected.Count -eq 0) {
|
|
throw "Release manifest does not contain any files"
|
|
}
|
|
|
|
$targetPrefix = [IO.Path]::GetFullPath($TargetRelease).TrimEnd('\') + '\'
|
|
$actual = @{}
|
|
$mismatches = @()
|
|
$conflictingFiles = @()
|
|
$entries = @(Get-ChildItem -LiteralPath $TargetRelease -Recurse -Force)
|
|
foreach ($entry in $entries) {
|
|
$relative = $entry.FullName.Substring($targetPrefix.Length).Replace('\', '/')
|
|
$isReparsePoint = ($entry.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0
|
|
if ($entry.PSIsContainer) {
|
|
if ($isReparsePoint) {
|
|
$mismatches += [ordered]@{
|
|
kind = "REPARSE_POINT"
|
|
path = $relative
|
|
expected_bytes = $null
|
|
actual_bytes = $null
|
|
expected_sha256 = $null
|
|
actual_sha256 = $null
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
$digest = $null
|
|
$hashError = $null
|
|
if (-not $isReparsePoint) {
|
|
try {
|
|
$digest = Get-LowerSha256 $entry.FullName
|
|
}
|
|
catch {
|
|
$hashError = $_.Exception.Message
|
|
}
|
|
}
|
|
$metadata = [pscustomobject]@{
|
|
path = $relative
|
|
bytes = [int64]$entry.Length
|
|
sha256 = $digest
|
|
reparse_point = $isReparsePoint
|
|
hash_error = $hashError
|
|
}
|
|
$conflictingFiles += $metadata
|
|
$key = $relative.ToLowerInvariant()
|
|
if ($actual.ContainsKey($key)) {
|
|
$mismatches += [ordered]@{
|
|
kind = "DUPLICATE_PATH"
|
|
path = $relative
|
|
expected_bytes = $null
|
|
actual_bytes = [int64]$entry.Length
|
|
expected_sha256 = $null
|
|
actual_sha256 = $digest
|
|
}
|
|
continue
|
|
}
|
|
$actual[$key] = $metadata
|
|
if ($isReparsePoint) {
|
|
$mismatches += [ordered]@{
|
|
kind = "REPARSE_POINT"
|
|
path = $relative
|
|
expected_bytes = $null
|
|
actual_bytes = [int64]$entry.Length
|
|
expected_sha256 = $null
|
|
actual_sha256 = $null
|
|
}
|
|
}
|
|
elseif ($null -ne $hashError) {
|
|
$mismatches += [ordered]@{
|
|
kind = "HASH_UNAVAILABLE"
|
|
path = $relative
|
|
expected_bytes = $null
|
|
actual_bytes = [int64]$entry.Length
|
|
expected_sha256 = $null
|
|
actual_sha256 = $null
|
|
error = $hashError
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($key in $expected.Keys) {
|
|
$wanted = $expected[$key]
|
|
if (-not $actual.ContainsKey($key)) {
|
|
$mismatches += [ordered]@{
|
|
kind = "MISSING"
|
|
path = $wanted.path
|
|
expected_bytes = $wanted.bytes
|
|
actual_bytes = $null
|
|
expected_sha256 = $wanted.sha256
|
|
actual_sha256 = $null
|
|
}
|
|
continue
|
|
}
|
|
$found = $actual[$key]
|
|
if ($found.reparse_point -or $null -ne $found.hash_error) {
|
|
continue
|
|
}
|
|
if ($found.bytes -ne $wanted.bytes) {
|
|
$mismatches += [ordered]@{
|
|
kind = "SIZE_MISMATCH"
|
|
path = $wanted.path
|
|
expected_bytes = $wanted.bytes
|
|
actual_bytes = $found.bytes
|
|
expected_sha256 = $wanted.sha256
|
|
actual_sha256 = $found.sha256
|
|
}
|
|
}
|
|
if ($found.sha256 -ne $wanted.sha256) {
|
|
$mismatches += [ordered]@{
|
|
kind = "SHA256_MISMATCH"
|
|
path = $wanted.path
|
|
expected_bytes = $wanted.bytes
|
|
actual_bytes = $found.bytes
|
|
expected_sha256 = $wanted.sha256
|
|
actual_sha256 = $found.sha256
|
|
}
|
|
}
|
|
}
|
|
foreach ($key in $actual.Keys) {
|
|
if (-not $expected.ContainsKey($key)) {
|
|
$found = $actual[$key]
|
|
$mismatches += [ordered]@{
|
|
kind = "UNEXPECTED"
|
|
path = $found.path
|
|
expected_bytes = $null
|
|
actual_bytes = $found.bytes
|
|
expected_sha256 = $null
|
|
actual_sha256 = $found.sha256
|
|
}
|
|
}
|
|
}
|
|
|
|
$state = if ($mismatches.Count -eq 0) { "TARGET_VERIFIED" } else { "TARGET_CONFLICT" }
|
|
return [pscustomobject]@{
|
|
state = $state
|
|
mismatches = @($mismatches)
|
|
conflicting_files = @($conflictingFiles)
|
|
}
|
|
}
|
|
|
|
function Assert-ReleaseMetadata(
|
|
[string]$ReleaseRoot,
|
|
[string]$ExpectedBootstrapId,
|
|
[string]$ExpectedReleaseId,
|
|
[string]$ExpectedAgentVersion,
|
|
[int]$ExpectedProtocolVersion
|
|
) {
|
|
$readyPath = Join-Path $ReleaseRoot "READY.json"
|
|
if (-not (Test-Path -LiteralPath $readyPath -PathType Leaf)) {
|
|
throw "Release READY metadata is absent: $readyPath"
|
|
}
|
|
$ready = Get-Content -LiteralPath $readyPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
if (
|
|
$ready.schema_version -ne 1 -or
|
|
$ready.status -ne "READY" -or
|
|
$ready.bootstrap_id -ne $ExpectedBootstrapId -or
|
|
$ready.release_id -ne $ExpectedReleaseId -or
|
|
$ready.agent_version -ne $ExpectedAgentVersion -or
|
|
$ready.protocol_version -ne $ExpectedProtocolVersion -or
|
|
$ready.hardware_accessed -ne $false -or
|
|
$ready.rf_tx -ne 0
|
|
) {
|
|
throw "Release READY metadata does not match the signed bootstrap manifest"
|
|
}
|
|
}
|
|
|
|
function New-UniqueQuarantineDestination(
|
|
[string]$InstallRoot,
|
|
[string]$ReleaseId,
|
|
[string]$Category
|
|
) {
|
|
if ($Category -notin @("conflict", "failed_staging", "failed_release")) {
|
|
throw "Unsupported quarantine category"
|
|
}
|
|
$quarantineRoot = Join-Path $InstallRoot "quarantine"
|
|
New-Item -ItemType Directory -Force -Path $quarantineRoot | Out-Null
|
|
for ($attempt = 0; $attempt -lt 16; $attempt++) {
|
|
$stamp = [DateTime]::UtcNow.ToString("yyyyMMddTHHmmssfff'Z'")
|
|
$unique = [Guid]::NewGuid().ToString("N")
|
|
$name = "{0}_{1}_{2}_{3}" -f $ReleaseId, $Category, $stamp, $unique
|
|
$candidate = Join-Path $quarantineRoot $name
|
|
if (
|
|
-not (Test-Path -LiteralPath $candidate) -and
|
|
-not (Test-Path -LiteralPath ($candidate + ".diagnostic.json"))
|
|
) {
|
|
return $candidate
|
|
}
|
|
}
|
|
throw "Could not allocate a unique quarantine path"
|
|
}
|
|
|
|
function Write-CreateNewUtf8Json([string]$Path, [object]$Value) {
|
|
$encoding = New-Object Text.UTF8Encoding($false)
|
|
$bytes = $encoding.GetBytes((($Value | ConvertTo-Json -Depth 10) + "`n"))
|
|
$stream = [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
|
try {
|
|
$stream.Write($bytes, 0, $bytes.Length)
|
|
$stream.Flush($true)
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
function Move-ConflictingTargetToQuarantine(
|
|
[string]$InstallRoot,
|
|
[string]$VersionsRoot,
|
|
[string]$TargetRelease,
|
|
[string]$ReleaseId,
|
|
[string]$BootstrapId,
|
|
[object]$State
|
|
) {
|
|
Assert-ExactReleasePath $InstallRoot $VersionsRoot $ReleaseId $TargetRelease
|
|
if ($State.state -ne "TARGET_CONFLICT") {
|
|
throw "Only TARGET_CONFLICT may be moved by the conflict quarantine operation"
|
|
}
|
|
$destination = New-UniqueQuarantineDestination $InstallRoot $ReleaseId "conflict"
|
|
$diagnosticPath = $destination + ".diagnostic.json"
|
|
$diagnostic = [ordered]@{
|
|
schema_version = 1
|
|
timestamp_utc = [DateTime]::UtcNow.ToString("o")
|
|
source_path = [IO.Path]::GetFullPath($TargetRelease)
|
|
quarantine_path = [IO.Path]::GetFullPath($destination)
|
|
bootstrap_id = $BootstrapId
|
|
expected_release_id = $ReleaseId
|
|
mismatches = @($State.mismatches)
|
|
conflicting_files = @($State.conflicting_files)
|
|
}
|
|
Write-CreateNewUtf8Json $diagnosticPath $diagnostic
|
|
Move-Item -LiteralPath $TargetRelease -Destination $destination
|
|
return [pscustomobject]@{
|
|
path = $destination
|
|
diagnostic_path = $diagnosticPath
|
|
}
|
|
}
|
|
|
|
function Move-FailedReleasePathToQuarantine(
|
|
[string]$InstallRoot,
|
|
[string]$Path,
|
|
[string]$ReleaseId,
|
|
[string]$BootstrapId,
|
|
[string]$Category,
|
|
[string]$Cause
|
|
) {
|
|
$versionsRoot = [IO.Path]::GetFullPath((Join-Path $InstallRoot "versions")).TrimEnd('\') + '\'
|
|
$fullPath = [IO.Path]::GetFullPath($Path)
|
|
if (-not $fullPath.StartsWith($versionsRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Failed release path is outside the fixed versions root"
|
|
}
|
|
$leaf = Split-Path -Leaf $fullPath
|
|
$validCanonical = $leaf.Equals($ReleaseId, [StringComparison]::OrdinalIgnoreCase)
|
|
$validStaging = $leaf.StartsWith(".$ReleaseId.staging_", [StringComparison]::OrdinalIgnoreCase)
|
|
if (
|
|
($Category -eq "failed_release" -and -not $validCanonical) -or
|
|
($Category -eq "failed_staging" -and -not $validStaging)
|
|
) {
|
|
throw "Failed release path is not an installer-defined release path"
|
|
}
|
|
$destination = New-UniqueQuarantineDestination $InstallRoot $ReleaseId $Category
|
|
$diagnosticPath = $destination + ".diagnostic.json"
|
|
$diagnostic = [ordered]@{
|
|
schema_version = 1
|
|
timestamp_utc = [DateTime]::UtcNow.ToString("o")
|
|
source_path = $fullPath
|
|
quarantine_path = [IO.Path]::GetFullPath($destination)
|
|
bootstrap_id = $BootstrapId
|
|
expected_release_id = $ReleaseId
|
|
category = $Category
|
|
cause = $Cause
|
|
}
|
|
Write-CreateNewUtf8Json $diagnosticPath $diagnostic
|
|
Move-Item -LiteralPath $fullPath -Destination $destination
|
|
return $destination
|
|
}
|
|
|
|
function Install-CleanRelease(
|
|
[string]$InstallRoot,
|
|
[string]$VersionsRoot,
|
|
[string]$SourceRelease,
|
|
[string]$TargetRelease,
|
|
[string]$ReleaseId,
|
|
[string]$BootstrapId,
|
|
[string]$ReleaseBootstrapId,
|
|
[string]$AgentVersion,
|
|
[int]$ProtocolVersion,
|
|
[object[]]$ReleaseRows,
|
|
[string]$ReleasePrefix
|
|
) {
|
|
if ([string]::IsNullOrWhiteSpace($ReleaseBootstrapId)) {
|
|
$ReleaseBootstrapId = $BootstrapId
|
|
}
|
|
Assert-ExactReleasePath $InstallRoot $VersionsRoot $ReleaseId $TargetRelease
|
|
if (Test-Path -LiteralPath $TargetRelease) {
|
|
throw "Canonical release path is not absent before clean deployment"
|
|
}
|
|
$staging = Join-Path $VersionsRoot (".{0}.staging_{1}" -f $ReleaseId, [Guid]::NewGuid().ToString("N"))
|
|
$canonicalCreated = $false
|
|
try {
|
|
New-Item -ItemType Directory -Path $staging | Out-Null
|
|
Copy-Item -LiteralPath (Join-Path $SourceRelease "app") -Destination $staging -Recurse
|
|
Copy-Item -LiteralPath (Join-Path $SourceRelease "READY.json") -Destination $staging
|
|
$stagingState = Get-ReleaseDirectoryState `
|
|
-TargetRelease $staging `
|
|
-ReleaseRows $ReleaseRows `
|
|
-ReleasePrefix $ReleasePrefix
|
|
if ($stagingState.state -ne "TARGET_VERIFIED") {
|
|
throw "Staged release failed exact file verification: $($stagingState.mismatches | ConvertTo-Json -Depth 6 -Compress)"
|
|
}
|
|
Assert-ReleaseMetadata `
|
|
-ReleaseRoot $staging `
|
|
-ExpectedBootstrapId $ReleaseBootstrapId `
|
|
-ExpectedReleaseId $ReleaseId `
|
|
-ExpectedAgentVersion $AgentVersion `
|
|
-ExpectedProtocolVersion $ProtocolVersion
|
|
Move-Item -LiteralPath $staging -Destination $TargetRelease
|
|
$canonicalCreated = $true
|
|
$targetState = Get-ReleaseDirectoryState `
|
|
-TargetRelease $TargetRelease `
|
|
-ReleaseRows $ReleaseRows `
|
|
-ReleasePrefix $ReleasePrefix
|
|
if ($targetState.state -ne "TARGET_VERIFIED") {
|
|
throw "Canonical release failed exact file verification: $($targetState.mismatches | ConvertTo-Json -Depth 6 -Compress)"
|
|
}
|
|
Assert-ReleaseMetadata `
|
|
-ReleaseRoot $TargetRelease `
|
|
-ExpectedBootstrapId $ReleaseBootstrapId `
|
|
-ExpectedReleaseId $ReleaseId `
|
|
-ExpectedAgentVersion $AgentVersion `
|
|
-ExpectedProtocolVersion $ProtocolVersion
|
|
return $targetState
|
|
}
|
|
catch {
|
|
$failure = $_.Exception.Message
|
|
$quarantineErrors = @()
|
|
if ($canonicalCreated -and (Test-Path -LiteralPath $TargetRelease)) {
|
|
try {
|
|
Move-FailedReleasePathToQuarantine $InstallRoot $TargetRelease $ReleaseId $BootstrapId "failed_release" $failure | Out-Null
|
|
}
|
|
catch {
|
|
$quarantineErrors += $_.Exception.Message
|
|
}
|
|
}
|
|
if (Test-Path -LiteralPath $staging) {
|
|
try {
|
|
Move-FailedReleasePathToQuarantine $InstallRoot $staging $ReleaseId $BootstrapId "failed_staging" $failure | Out-Null
|
|
}
|
|
catch {
|
|
$quarantineErrors += $_.Exception.Message
|
|
}
|
|
}
|
|
if ($quarantineErrors.Count -ne 0) {
|
|
throw "Clean release deployment failed: $failure; failed-path quarantine errors: $($quarantineErrors -join '; ')"
|
|
}
|
|
throw "Clean release deployment failed: $failure"
|
|
}
|
|
}
|
|
|
|
function Prepare-TargetRelease(
|
|
[string]$InstallRoot,
|
|
[string]$VersionsRoot,
|
|
[string]$SourceRelease,
|
|
[string]$TargetRelease,
|
|
[string]$ReleaseId,
|
|
[string]$BootstrapId,
|
|
[string]$ReleaseBootstrapId,
|
|
[string]$AgentVersion,
|
|
[int]$ProtocolVersion,
|
|
[object[]]$ReleaseRows,
|
|
[string]$ReleasePrefix
|
|
) {
|
|
if ([string]::IsNullOrWhiteSpace($ReleaseBootstrapId)) {
|
|
$ReleaseBootstrapId = $BootstrapId
|
|
}
|
|
Assert-ExactReleasePath $InstallRoot $VersionsRoot $ReleaseId $TargetRelease
|
|
$sourceState = Get-ReleaseDirectoryState `
|
|
-TargetRelease $SourceRelease `
|
|
-ReleaseRows $ReleaseRows `
|
|
-ReleasePrefix $ReleasePrefix
|
|
if ($sourceState.state -ne "TARGET_VERIFIED") {
|
|
throw "Signed bootstrap release content is not exact: $($sourceState.mismatches | ConvertTo-Json -Depth 6 -Compress)"
|
|
}
|
|
Assert-ReleaseMetadata `
|
|
-ReleaseRoot $SourceRelease `
|
|
-ExpectedBootstrapId $ReleaseBootstrapId `
|
|
-ExpectedReleaseId $ReleaseId `
|
|
-ExpectedAgentVersion $AgentVersion `
|
|
-ExpectedProtocolVersion $ProtocolVersion
|
|
|
|
$initial = Get-ReleaseDirectoryState `
|
|
-TargetRelease $TargetRelease `
|
|
-ReleaseRows $ReleaseRows `
|
|
-ReleasePrefix $ReleasePrefix
|
|
$quarantine = $null
|
|
if ($initial.state -eq "TARGET_CONFLICT") {
|
|
$quarantine = Move-ConflictingTargetToQuarantine `
|
|
-InstallRoot $InstallRoot `
|
|
-VersionsRoot $VersionsRoot `
|
|
-TargetRelease $TargetRelease `
|
|
-ReleaseId $ReleaseId `
|
|
-BootstrapId $BootstrapId `
|
|
-State $initial
|
|
}
|
|
if ($initial.state -ne "TARGET_VERIFIED") {
|
|
$verified = Install-CleanRelease `
|
|
-InstallRoot $InstallRoot `
|
|
-VersionsRoot $VersionsRoot `
|
|
-SourceRelease $SourceRelease `
|
|
-TargetRelease $TargetRelease `
|
|
-ReleaseId $ReleaseId `
|
|
-BootstrapId $BootstrapId `
|
|
-ReleaseBootstrapId $ReleaseBootstrapId `
|
|
-AgentVersion $AgentVersion `
|
|
-ProtocolVersion $ProtocolVersion `
|
|
-ReleaseRows $ReleaseRows `
|
|
-ReleasePrefix $ReleasePrefix
|
|
}
|
|
else {
|
|
Assert-ReleaseMetadata `
|
|
-ReleaseRoot $TargetRelease `
|
|
-ExpectedBootstrapId $ReleaseBootstrapId `
|
|
-ExpectedReleaseId $ReleaseId `
|
|
-ExpectedAgentVersion $AgentVersion `
|
|
-ExpectedProtocolVersion $ProtocolVersion
|
|
$verified = $initial
|
|
}
|
|
return [pscustomobject]@{
|
|
initial_state = $initial.state
|
|
final_state = $verified.state
|
|
reused = ($initial.state -eq "TARGET_VERIFIED")
|
|
quarantine_path = if ($null -eq $quarantine) { $null } else { $quarantine.path }
|
|
quarantine_diagnostic = if ($null -eq $quarantine) { $null } else { $quarantine.diagnostic_path }
|
|
}
|
|
}
|
|
|
|
function Invoke-SignedAgentSelfTest(
|
|
[string]$Python,
|
|
[string]$AgentScript,
|
|
[string]$ExpectedAgentVersion,
|
|
[string]$StandardOutputPath,
|
|
[string]$StandardErrorPath
|
|
) {
|
|
$process = Start-Process `
|
|
-FilePath $Python `
|
|
-ArgumentList @("-B", "-X", "utf8", $AgentScript, "--self-test") `
|
|
-RedirectStandardOutput $StandardOutputPath `
|
|
-RedirectStandardError $StandardErrorPath `
|
|
-Wait -PassThru -WindowStyle Hidden
|
|
if ($process.ExitCode -ne 0) {
|
|
throw "Agent self-test exited with code $($process.ExitCode)"
|
|
}
|
|
$output = if (Test-Path -LiteralPath $StandardOutputPath) {
|
|
Get-Content -LiteralPath $StandardOutputPath -Raw -Encoding UTF8
|
|
}
|
|
else {
|
|
""
|
|
}
|
|
if ($output -notmatch ("(?m)^AGENT VERSION = {0}\s*$" -f [regex]::Escape($ExpectedAgentVersion))) {
|
|
throw "Agent self-test did not confirm signed AGENT_VERSION"
|
|
}
|
|
if ($output -notmatch '(?m)^SELF TEST = PASS\s*$') {
|
|
throw "Agent self-test did not report PASS"
|
|
}
|
|
return $output
|
|
}
|
|
|
|
foreach ($required in @(
|
|
$manifestPath,
|
|
$signaturePath,
|
|
$python,
|
|
(Join-Path $repository ".git"),
|
|
(Join-Path $hackrfRoot "hackrf_info.exe"),
|
|
(Join-Path $hackrfRoot "hackrf_clock.exe"),
|
|
(Join-Path $hackrfRoot "hackrf_transfer.exe")
|
|
)) {
|
|
if (-not (Test-Path -LiteralPath $required)) {
|
|
throw "Required path is absent: $required"
|
|
}
|
|
}
|
|
|
|
if ((Test-Path -LiteralPath $sharedToken) -and (Test-Path -LiteralPath $legacyToken)) {
|
|
if ((Get-LowerSha256 $sharedToken) -ne (Get-LowerSha256 $legacyToken)) {
|
|
throw "Installed signing-token copies differ; refusing to choose one"
|
|
}
|
|
}
|
|
if (Test-Path -LiteralPath $sharedToken) {
|
|
$tokenSource = $sharedToken
|
|
}
|
|
elseif (Test-Path -LiteralPath $legacyToken) {
|
|
$tokenSource = $legacyToken
|
|
}
|
|
else {
|
|
throw "The existing HTTP signing token is absent; it is intentionally not in this package"
|
|
}
|
|
|
|
$manifestBytes = [IO.File]::ReadAllBytes($manifestPath)
|
|
$tokenText = [IO.File]::ReadAllText($tokenSource, [Text.Encoding]::UTF8).Trim()
|
|
$tokenBytes = [Text.Encoding]::UTF8.GetBytes($tokenText)
|
|
if ($tokenBytes.Length -lt 32) {
|
|
throw "The existing HTTP signing token is invalid"
|
|
}
|
|
$domainBytes = [Text.Encoding]::UTF8.GetBytes("RF001-UPDATE-MANIFEST-V1`n")
|
|
$signedBytes = New-Object byte[] ($domainBytes.Length + $manifestBytes.Length)
|
|
[Buffer]::BlockCopy($domainBytes, 0, $signedBytes, 0, $domainBytes.Length)
|
|
[Buffer]::BlockCopy($manifestBytes, 0, $signedBytes, $domainBytes.Length, $manifestBytes.Length)
|
|
$hmac = New-Object Security.Cryptography.HMACSHA256 -ArgumentList (, $tokenBytes)
|
|
try {
|
|
$expectedSignature = ([BitConverter]::ToString($hmac.ComputeHash($signedBytes))).Replace("-", "").ToLowerInvariant()
|
|
}
|
|
finally {
|
|
$hmac.Dispose()
|
|
}
|
|
$suppliedSignature = [IO.File]::ReadAllText($signaturePath, [Text.Encoding]::ASCII).Trim().ToLowerInvariant()
|
|
if (-not (Test-FixedTimeHex $expectedSignature $suppliedSignature)) {
|
|
throw "Bootstrap manifest signature mismatch"
|
|
}
|
|
|
|
$manifest = [Text.Encoding]::UTF8.GetString($manifestBytes) | ConvertFrom-Json
|
|
if ($manifest.schema_version -ne 1 -or $manifest.package_kind -ne "BOOTSTRAP") {
|
|
throw "Unsupported bootstrap manifest"
|
|
}
|
|
if ($manifest.protocol_version -ne 1) {
|
|
throw "Unsupported agent protocol version"
|
|
}
|
|
if ($manifest.main_server -ne "http://192.168.255.185:8765") {
|
|
throw "Bootstrap MAIN address differs from the approved address"
|
|
}
|
|
if ($manifest.allowed_rx_ipv4 -ne "192.168.255.56") {
|
|
throw "Bootstrap RX address differs from the approved address"
|
|
}
|
|
if ($manifest.reference_sha256 -ne $expectedReferenceHash) {
|
|
throw "Bootstrap reference identity is invalid"
|
|
}
|
|
$releaseId = [string]$manifest.agent_release_id
|
|
$agentVersion = [string]$manifest.agent_version
|
|
$bootstrapId = [string]$manifest.bootstrap_id
|
|
$releaseBootstrapId = [string]$manifest.agent_release_bootstrap_id
|
|
if ([string]::IsNullOrWhiteSpace($releaseBootstrapId)) {
|
|
$releaseBootstrapId = $bootstrapId
|
|
}
|
|
if ($bootstrapId -notmatch '^rf001-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$') {
|
|
throw "Bootstrap ID is invalid"
|
|
}
|
|
if ($releaseBootstrapId -notmatch '^rf001-bootstrap-[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$') {
|
|
throw "Agent release bootstrap ID is invalid"
|
|
}
|
|
if ($releaseId -notmatch '^rf001-agent-[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$') {
|
|
throw "Bootstrap release_id is invalid"
|
|
}
|
|
if ($agentVersion -notmatch '^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$') {
|
|
throw "Bootstrap agent version is invalid"
|
|
}
|
|
if ($releaseId -ne "rf001-agent-$agentVersion") {
|
|
throw "Bootstrap release_id does not match agent_version"
|
|
}
|
|
|
|
$releasePrefix = "versions/$releaseId/"
|
|
$expectedPaths = @(
|
|
"INSTALL_RX_AGENT.cmd",
|
|
"README_RX_AGENT.txt",
|
|
"UNINSTALL_RX_AGENT.cmd",
|
|
"launcher/rf001_agent_launcher.py",
|
|
"reference/lab065_reference_staircase.cs8",
|
|
"tools/install_rf001_rx_autostart.ps1",
|
|
"tools/uninstall_rf001_rx_autostart.ps1",
|
|
"${releasePrefix}READY.json",
|
|
"${releasePrefix}app/tools/hackrf_process_check.py",
|
|
"${releasePrefix}app/tools/rf001_agent_update.py",
|
|
"${releasePrefix}app/tools/rf001_capture_runner.py",
|
|
"${releasePrefix}app/tools/rf001_http_control.py",
|
|
"${releasePrefix}app/tools/rf001_offline_decode.py",
|
|
"${releasePrefix}app/tools/rf001_rx_capture.py",
|
|
"${releasePrefix}app/tools/rf001_rx_http_agent.py",
|
|
"${releasePrefix}app/tools/rf001_staircase_job.py",
|
|
"${releasePrefix}app/tools/rf001_staircase_search.py"
|
|
)
|
|
$expectedSet = @{}
|
|
foreach ($path in $expectedPaths) {
|
|
$expectedSet[$path.ToLowerInvariant()] = $true
|
|
}
|
|
$manifestSet = @{}
|
|
$sourcePrefix = $sourceRoot.TrimEnd('\') + '\'
|
|
foreach ($row in @($manifest.files)) {
|
|
$relative = [string]$row.path
|
|
if ($relative -match '(^/|\\|(^|/)\.\.(/|$)|:)' -or [string]::IsNullOrWhiteSpace($relative)) {
|
|
throw "Unsafe bootstrap path: $relative"
|
|
}
|
|
$key = $relative.ToLowerInvariant()
|
|
if ($manifestSet.ContainsKey($key)) {
|
|
throw "Duplicate bootstrap path: $relative"
|
|
}
|
|
if (-not $expectedSet.ContainsKey($key)) {
|
|
throw "Bootstrap file is outside the fixed allowlist: $relative"
|
|
}
|
|
$manifestSet[$key] = $true
|
|
$source = [IO.Path]::GetFullPath((Join-Path $sourceRoot ($relative.Replace('/', '\'))))
|
|
if (-not $source.StartsWith($sourcePrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Bootstrap path escaped source root: $relative"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
|
|
throw "Bootstrap file is absent: $relative"
|
|
}
|
|
$item = Get-Item -LiteralPath $source
|
|
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw "Bootstrap reparse points are prohibited: $relative"
|
|
}
|
|
if ($item.Length -ne [int64]$row.bytes) {
|
|
throw "Bootstrap file size mismatch: $relative"
|
|
}
|
|
if ((Get-LowerSha256 $source) -ne ([string]$row.sha256).ToLowerInvariant()) {
|
|
throw "Bootstrap file SHA256 mismatch: $relative"
|
|
}
|
|
}
|
|
if ($manifestSet.Count -ne $expectedSet.Count) {
|
|
throw "Bootstrap manifest does not contain the complete fixed allowlist"
|
|
}
|
|
$actualFiles = Get-ChildItem -LiteralPath $sourceRoot -Recurse -File | ForEach-Object {
|
|
$_.FullName.Substring($sourcePrefix.Length).Replace('\', '/')
|
|
}
|
|
$allowedPackageFiles = @{}
|
|
foreach ($path in $expectedPaths + @("BOOTSTRAP_MANIFEST.json", "BOOTSTRAP_MANIFEST.hmac")) {
|
|
$allowedPackageFiles[$path.ToLowerInvariant()] = $true
|
|
}
|
|
foreach ($relative in $actualFiles) {
|
|
if (-not $allowedPackageFiles.ContainsKey($relative.ToLowerInvariant())) {
|
|
throw "Unexpected file in the bootstrap package: $relative"
|
|
}
|
|
}
|
|
if ($actualFiles.Count -ne $allowedPackageFiles.Count) {
|
|
throw "Bootstrap package file set is incomplete or ambiguous"
|
|
}
|
|
|
|
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
if ($null -eq $existingTask) {
|
|
throw "The existing scheduled task '$taskName' is absent"
|
|
}
|
|
$wasRunning = $existingTask.State -eq "Running"
|
|
$backupRoot = Join-Path $installRoot (
|
|
"backups\bootstrap_{0}_{1}" -f
|
|
[DateTime]::UtcNow.ToString("yyyyMMddTHHmmssfff'Z'"),
|
|
[Guid]::NewGuid().ToString("N").Substring(0, 12)
|
|
)
|
|
New-Item -ItemType Directory -Path $backupRoot | Out-Null
|
|
$taskXmlPath = Join-Path $backupRoot "scheduled_task.xml"
|
|
[IO.File]::WriteAllText($taskXmlPath, (Export-ScheduledTask -TaskName $taskName), [Text.Encoding]::Unicode)
|
|
$backupInventory = @()
|
|
foreach ($name in @("app", "launcher", "shared", "agent_config.json", "active_release.json", "launcher_status.json", "agent_health.json")) {
|
|
$current = Join-Path $installRoot $name
|
|
$existed = Test-Path -LiteralPath $current
|
|
$backupInventory += [ordered]@{
|
|
name = $name
|
|
existed = $existed
|
|
container = ($existed -and (Test-Path -LiteralPath $current -PathType Container))
|
|
}
|
|
if ($existed) {
|
|
Copy-Item -LiteralPath $current -Destination (Join-Path $backupRoot $name) -Recurse -Force
|
|
}
|
|
}
|
|
Write-CreateNewUtf8Json (Join-Path $backupRoot "install_state_inventory.json") $backupInventory
|
|
|
|
New-Item -ItemType Directory -Force -Path $versionsRoot, $launcherRoot, $sharedRoot, $logsRoot | Out-Null
|
|
if (-not (Test-Path -LiteralPath $sharedToken)) {
|
|
Copy-Item -LiteralPath $tokenSource -Destination $sharedToken
|
|
}
|
|
if ((Get-LowerSha256 $sharedToken) -ne (Get-LowerSha256 $tokenSource)) {
|
|
throw "The preserved signing token differs after copying"
|
|
}
|
|
|
|
$sourceRelease = Join-Path $sourceRoot ($releasePrefix.Replace('/', '\').TrimEnd('\'))
|
|
$targetRelease = Join-Path $versionsRoot $releaseId
|
|
$sourceAgentScript = Join-Path $sourceRelease "app\tools\rf001_rx_http_agent.py"
|
|
Invoke-SignedAgentSelfTest `
|
|
-Python $python `
|
|
-AgentScript $sourceAgentScript `
|
|
-ExpectedAgentVersion $agentVersion `
|
|
-StandardOutputPath (Join-Path $backupRoot "source_agent_self_test.stdout.txt") `
|
|
-StandardErrorPath (Join-Path $backupRoot "source_agent_self_test.stderr.txt") | Out-Null
|
|
$releaseRows = @($manifest.files | Where-Object { ([string]$_.path).StartsWith($releasePrefix) })
|
|
$targetPreparation = Prepare-TargetRelease `
|
|
-InstallRoot $installRoot `
|
|
-VersionsRoot $versionsRoot `
|
|
-SourceRelease $sourceRelease `
|
|
-TargetRelease $targetRelease `
|
|
-ReleaseId $releaseId `
|
|
-BootstrapId $bootstrapId `
|
|
-ReleaseBootstrapId $releaseBootstrapId `
|
|
-AgentVersion $agentVersion `
|
|
-ProtocolVersion ([int]$manifest.protocol_version) `
|
|
-ReleaseRows $releaseRows `
|
|
-ReleasePrefix $releasePrefix
|
|
|
|
Copy-Item -LiteralPath (Join-Path $sourceRoot "launcher\rf001_agent_launcher.py") -Destination $launcherRoot -Force
|
|
Copy-Item -LiteralPath (Join-Path $sourceRoot "tools\uninstall_rf001_rx_autostart.ps1") -Destination $installRoot -Force
|
|
Copy-Item -LiteralPath (Join-Path $sourceRoot "UNINSTALL_RX_AGENT.cmd") -Destination $installRoot -Force
|
|
$referenceRoot = Join-Path $workRoot "references"
|
|
$referenceDestination = Join-Path $referenceRoot "lab065_reference_staircase.cs8"
|
|
New-Item -ItemType Directory -Force -Path $referenceRoot | Out-Null
|
|
if (Test-Path -LiteralPath $referenceDestination) {
|
|
if ((Get-LowerSha256 $referenceDestination) -ne $expectedReferenceHash) {
|
|
throw "The existing staircase reference differs; refusing to overwrite it"
|
|
}
|
|
}
|
|
else {
|
|
Copy-Item -LiteralPath (Join-Path $sourceRoot "reference\lab065_reference_staircase.cs8") -Destination $referenceDestination
|
|
}
|
|
if ((Get-LowerSha256 $referenceDestination) -ne $expectedReferenceHash) {
|
|
throw "Installed staircase reference SHA256 is invalid"
|
|
}
|
|
|
|
$configPath = Join-Path $installRoot "agent_config.json"
|
|
$activeReleasePath = Join-Path $installRoot "active_release.json"
|
|
$launcherStatusPath = Join-Path $installRoot "launcher_status.json"
|
|
$launcherRequestPath = Join-Path $installRoot "launcher_request.json"
|
|
$healthPath = Join-Path $installRoot "agent_health.json"
|
|
$agentLogPath = Join-Path $logsRoot "agent.log"
|
|
$launcherLogPath = Join-Path $logsRoot "launcher.log"
|
|
$agentArguments = @(
|
|
"--server", "http://192.168.255.185:8765",
|
|
"--token-file", $sharedToken,
|
|
"--agent-id", "user-pc",
|
|
"--repository", $repository,
|
|
"--hackrf-info", (Join-Path $hackrfRoot "hackrf_info.exe"),
|
|
"--hackrf-clock", (Join-Path $hackrfRoot "hackrf_clock.exe"),
|
|
"--hackrf-transfer", (Join-Path $hackrfRoot "hackrf_transfer.exe"),
|
|
"--work-root", $workRoot,
|
|
"--poll-interval", "5",
|
|
"--quiet-idle"
|
|
)
|
|
$config = [ordered]@{
|
|
schema_version = 1
|
|
install_root = $installRoot
|
|
releases_root = $versionsRoot
|
|
active_release_file = $activeReleasePath
|
|
launcher_status = $launcherStatusPath
|
|
launcher_request = $launcherRequestPath
|
|
health_file = $healthPath
|
|
agent_log = $agentLogPath
|
|
startup_timeout_seconds = 30
|
|
restart_delay_seconds = 2
|
|
agent_arguments = $agentArguments
|
|
hardware_accessed = $false
|
|
rf_tx = 0
|
|
}
|
|
[IO.File]::WriteAllText($configPath, (($config | ConvertTo-Json -Depth 5 -Compress) + "`n"), [Text.Encoding]::UTF8)
|
|
$activeRelease = [ordered]@{
|
|
schema_version = 1
|
|
release_id = $releaseId
|
|
updated_at = [DateTime]::UtcNow.ToString("o")
|
|
}
|
|
[IO.File]::WriteAllText($activeReleasePath, (($activeRelease | ConvertTo-Json -Compress) + "`n"), [Text.Encoding]::UTF8)
|
|
|
|
$agentScript = Join-Path $targetRelease "app\tools\rf001_rx_http_agent.py"
|
|
$selfTestOutput = Join-Path $backupRoot "new_agent_self_test.stdout.txt"
|
|
$selfTestError = Join-Path $backupRoot "new_agent_self_test.stderr.txt"
|
|
Invoke-SignedAgentSelfTest `
|
|
-Python $python `
|
|
-AgentScript $agentScript `
|
|
-ExpectedAgentVersion $agentVersion `
|
|
-StandardOutputPath $selfTestOutput `
|
|
-StandardErrorPath $selfTestError | Out-Null
|
|
|
|
$launcherScript = Join-Path $launcherRoot "rf001_agent_launcher.py"
|
|
$launcherArguments = @(
|
|
"-u",
|
|
"-B",
|
|
"-X", "utf8",
|
|
('"{0}"' -f $launcherScript),
|
|
"--config", ('"{0}"' -f $configPath),
|
|
"--log-file", ('"{0}"' -f $launcherLogPath)
|
|
) -join " "
|
|
$action = New-ScheduledTaskAction -Execute $python -Argument $launcherArguments -WorkingDirectory $launcherRoot
|
|
$healthBefore = if (Test-Path -LiteralPath $healthPath) { (Get-Item -LiteralPath $healthPath).LastWriteTimeUtc } else { [DateTime]::MinValue }
|
|
$installStartedAt = [DateTime]::UtcNow
|
|
|
|
try {
|
|
Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
Set-ScheduledTask -TaskName $taskName -Action $action | Out-Null
|
|
Start-ScheduledTask -TaskName $taskName
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(35)
|
|
$confirmed = $false
|
|
do {
|
|
Start-Sleep -Milliseconds 500
|
|
$currentTask = Get-ScheduledTask -TaskName $taskName
|
|
if ((Test-Path -LiteralPath $healthPath) -and (Test-Path -LiteralPath $launcherStatusPath)) {
|
|
$healthItem = Get-Item -LiteralPath $healthPath
|
|
$health = Get-Content -LiteralPath $healthPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
$launcherStatus = Get-Content -LiteralPath $launcherStatusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
if (
|
|
$currentTask.State -eq "Running" -and
|
|
$healthItem.LastWriteTimeUtc -gt $healthBefore -and
|
|
$healthItem.LastWriteTimeUtc -ge $installStartedAt.AddSeconds(-2) -and
|
|
$health.state -eq "RUNNING" -and
|
|
$health.agent_version -eq $agentVersion -and
|
|
$health.release_id -eq $releaseId -and
|
|
$health.rf_tx -eq 0 -and
|
|
$launcherStatus.state -eq "RUNNING" -and
|
|
$launcherStatus.active_release_id -eq $releaseId -and
|
|
$launcherStatus.active_agent_version -eq $agentVersion -and
|
|
$launcherStatus.rf_tx -eq 0
|
|
) {
|
|
$confirmed = $true
|
|
}
|
|
}
|
|
} while (-not $confirmed -and [DateTime]::UtcNow -lt $deadline)
|
|
if (-not $confirmed) {
|
|
$currentInfo = Get-ScheduledTaskInfo -TaskName $taskName
|
|
throw "Versioned agent did not report healthy startup; LastTaskResult=$($currentInfo.LastTaskResult)"
|
|
}
|
|
}
|
|
catch {
|
|
$failure = $_.Exception.Message
|
|
Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
$xml = [IO.File]::ReadAllText($taskXmlPath, [Text.Encoding]::Unicode)
|
|
Register-ScheduledTask -TaskName $taskName -Xml $xml -Force | Out-Null
|
|
foreach ($name in @("launcher", "agent_config.json", "active_release.json", "launcher_status.json", "agent_health.json")) {
|
|
$inventoryRow = @($backupInventory | Where-Object { $_.name -eq $name })
|
|
if ($inventoryRow.Count -ne 1) {
|
|
throw "Bootstrap failed and rollback inventory is invalid. Original cause: $failure"
|
|
}
|
|
$current = Join-Path $installRoot $name
|
|
$saved = Join-Path $backupRoot $name
|
|
if ($inventoryRow[0].existed) {
|
|
if ($inventoryRow[0].container) {
|
|
if (-not (Test-Path -LiteralPath $current -PathType Container)) {
|
|
New-Item -ItemType Directory -Path $current -Force | Out-Null
|
|
}
|
|
Copy-Item -Path (Join-Path $saved '*') -Destination $current -Recurse -Force
|
|
}
|
|
else {
|
|
Copy-Item -LiteralPath $saved -Destination $current -Force
|
|
}
|
|
}
|
|
}
|
|
if ($wasRunning) {
|
|
Start-ScheduledTask -TaskName $taskName
|
|
}
|
|
throw "Bootstrap failed; previous scheduled task restored. Cause: $failure"
|
|
}
|
|
|
|
$taskAfter = Get-ScheduledTask -TaskName $taskName
|
|
$taskInfoAfter = Get-ScheduledTaskInfo -TaskName $taskName
|
|
"BOOTSTRAP_INSTALL=COMPLETE"
|
|
"INSTALL_ROOT=$installRoot"
|
|
"BACKUP_ROOT=$backupRoot"
|
|
"AUTOSTART_TASK=$taskName"
|
|
"AUTOSTART_ACCOUNT=$($taskAfter.Principal.UserId)"
|
|
"AUTOSTART_RUN_LEVEL=$($taskAfter.Principal.RunLevel)"
|
|
"TASK_STATE=$($taskAfter.State)"
|
|
"LAST_TASK_RESULT=$($taskInfoAfter.LastTaskResult)"
|
|
"BOOTSTRAP_ID=$bootstrapId"
|
|
"AGENT_RELEASE_ID=$releaseId"
|
|
"AGENT_VERSION=$agentVersion"
|
|
"TARGET_INITIAL_STATE=$($targetPreparation.initial_state)"
|
|
"TARGET_FINAL_STATE=$($targetPreparation.final_state)"
|
|
"TARGET_REUSED=$($targetPreparation.reused)"
|
|
"CONFLICT_QUARANTINE_PATH=$($targetPreparation.quarantine_path)"
|
|
"CONFLICT_DIAGNOSTIC_PATH=$($targetPreparation.quarantine_diagnostic)"
|
|
"CLEAN_RELEASE_HASH_VERIFICATION=PASS"
|
|
"REFERENCE_SHA256=$expectedReferenceHash"
|
|
"MANIFEST_AUTHENTICATION=HMAC-SHA256"
|
|
"ARBITRARY_SHELL=DISABLED"
|
|
"HARDWARE_ACCESSED=false"
|
|
"RF_TX=0"
|