初始化系统融合软件代码仓库
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$CoordinatorUrl = 'http://127.0.0.1:18100',
|
||||
[string]$CredentialFile = (Join-Path (Split-Path -Parent $PSScriptRoot) 'runtime/cluster.local.properties'),
|
||||
[ValidatePattern('^[0-9a-fA-F-]{36}$')][string]$SourceEndpointId,
|
||||
[ValidateRange(1,3600)][int]$DurationSeconds = 30,
|
||||
[ValidateRange(100,5000)][int]$PollMilliseconds = 500,
|
||||
[string]$OutputDirectory = (Join-Path (Split-Path -Parent $PSScriptRoot) 'runtime/image-observation'),
|
||||
[switch]$InventoryOnly,
|
||||
[switch]$AllowPending,
|
||||
[switch]$QuietFrames,
|
||||
[switch]$PrepareMigration,
|
||||
[switch]$Migrate,
|
||||
[string]$FromNodeId,
|
||||
[string]$ToNodeId
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'Image-Verification.Common.ps1')
|
||||
|
||||
function Get-ImageTask {
|
||||
param($Snapshot)
|
||||
$matches = @($Snapshot.tasks | Where-Object { $_.mediaKind -eq 'ORIGINAL_IMAGE' -and $_.sourceEndpointId -eq $SourceEndpointId })
|
||||
if ($matches.Count -gt 1) { throw '来源出现多个原图任务,已停止联调。' }
|
||||
if ($matches.Count -eq 0) { return $null }
|
||||
return $matches[0]
|
||||
}
|
||||
|
||||
function Write-ImageRecord {
|
||||
param($Record)
|
||||
# 白名单记录不含 playUrl/mediaKey、私有设备密钥、集群凭证或 PXHJ 会话。
|
||||
[IO.File]::AppendAllText($script:journalFile, ($Record | ConvertTo-Json -Depth 5 -Compress) + [Environment]::NewLine, [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
function Write-ImageTaskState {
|
||||
param($Snapshot)
|
||||
$task = Get-ImageTask -Snapshot $Snapshot
|
||||
$status = if ($null -eq $task) { 'NOT_REGISTERED' } else { [string]$task.status }
|
||||
$nodeId = if ($null -ne $task -and $task.PSObject.Properties['nodeId']) { [string]$task.nodeId } else { '' }
|
||||
$version = if ($null -ne $task -and $task.PSObject.Properties['version']) { [long]$task.version } else { 0L }
|
||||
$ready = $null -ne $task -and $task.PSObject.Properties['imageReady'] -and [bool]$task.imageReady
|
||||
$stateKey = $status + '/' + $nodeId + '/' + $version + '/' + $ready
|
||||
if ($stateKey -eq $script:lastTaskState) { return }
|
||||
$record = [ordered]@{ event = 'task-state'; observedTime = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
||||
endpointId = $SourceEndpointId; status = $status; nodeId = $nodeId; version = $version; imageReady = $ready }
|
||||
[IO.File]::AppendAllText((Join-Path $script:observationRoot 'task-states.jsonl'),
|
||||
($record | ConvertTo-Json -Compress) + [Environment]::NewLine, [Text.UTF8Encoding]::new($false))
|
||||
Write-Information ('原图任务状态:' + $status + ',node=' + $nodeId + ',version=' + $version + ',imageReady=' + $ready) -InformationAction Continue
|
||||
$script:lastTaskState = $stateKey
|
||||
}
|
||||
|
||||
function Read-CurrentOriginalFrame {
|
||||
param($Snapshot)
|
||||
$task = Get-ImageTask -Snapshot $Snapshot
|
||||
if ($null -eq $task -or -not $task.PSObject.Properties['nodeId'] -or -not $task.PSObject.Properties['version']) { return $null }
|
||||
$node = @($Snapshot.nodes | Where-Object { $_.nodeId -eq $task.nodeId })
|
||||
if ($node.Count -ne 1 -or -not $node[0].online) { return $null }
|
||||
$path = '/node/images/' + [Uri]::EscapeDataString([string]$task.taskId) + '/' + [long]$task.version
|
||||
$metadataResponse = Invoke-XtrhImageSigned -BaseUrl $node[0].baseUrl -Path ($path + '/latest')
|
||||
if ($metadataResponse.Status -eq 404) { return $null }
|
||||
$metadata = ConvertFrom-XtrhImageJson -Response $metadataResponse
|
||||
if ($metadata.endpointId -ne $SourceEndpointId -or [int]$metadata.bytes -lt 4 -or [int]$metadata.bytes -gt 2097152 -or
|
||||
[int]$metadata.width -lt 1 -or [int]$metadata.width -gt 4096 -or [int]$metadata.height -lt 1 -or [int]$metadata.height -gt 4096) {
|
||||
throw '原图元数据与请求来源或大小限制不一致。'
|
||||
}
|
||||
$frameResponse = Invoke-XtrhImageSigned -BaseUrl $node[0].baseUrl -Path ($path + '/frames/' + [Uri]::EscapeDataString([string]$metadata.frameId))
|
||||
# 迁移或近期帧淘汰可使取图返回 404,重新查询即可,不能借用其他版本的图片。
|
||||
if ($frameResponse.Status -eq 404) { return $null }
|
||||
if ($frameResponse.Status -ne 200 -or $frameResponse.ContentType -notmatch '^image/jpeg(?:;|$)' -or
|
||||
$frameResponse.Bytes.Length -ne [int]$metadata.bytes -or $frameResponse.Bytes[0] -ne 255 -or $frameResponse.Bytes[1] -ne 216 -or
|
||||
$frameResponse.Bytes[-2] -ne 255 -or $frameResponse.Bytes[-1] -ne 217) { throw '节点未返回一致的签名 JPEG。' }
|
||||
$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
||||
$record = [ordered]@{
|
||||
observedTime = $now; endpointId = $SourceEndpointId; taskId = $task.taskId; version = [long]$task.version
|
||||
nodeId = $task.nodeId; frameId = $metadata.frameId; sourceTime = [long]$metadata.sourceTime
|
||||
receivedTime = [long]$metadata.receivedTime; ageMs = $now - [long]$metadata.sourceTime
|
||||
width = [int]$metadata.width; height = [int]$metadata.height; bytes = $frameResponse.Bytes.Length
|
||||
sha256 = Get-XtrhImageDigest -Bytes $frameResponse.Bytes; responseAuthenticated = $true
|
||||
}
|
||||
# 图像按帧号缓存,只落业务图片与白名单元数据,绝不保存原始总控完整快照。
|
||||
if ($record.frameId -ne $script:lastFrameId -or $record.nodeId -ne $script:lastNodeId -or $record.version -ne $script:lastVersion) {
|
||||
[IO.File]::WriteAllBytes((Join-Path $script:observationRoot ('frame-' + [Guid]::Parse($record.frameId).ToString() + '.jpg')), $frameResponse.Bytes)
|
||||
Write-ImageRecord -Record $record
|
||||
if (-not $QuietFrames) {
|
||||
Write-Information ('原图已验证:node=' + $record.nodeId + ',version=' + $record.version + ',sourceTime=' + $record.sourceTime +
|
||||
',receivedTime=' + $record.receivedTime + ',ageMs=' + $record.ageMs + ',bytes=' + $record.bytes) -InformationAction Continue
|
||||
}
|
||||
$script:lastFrameId = $record.frameId; $script:lastNodeId = $record.nodeId; $script:lastVersion = $record.version
|
||||
$script:verifiedFrames++
|
||||
if ($record.ageMs -ge -5000 -and $record.ageMs -le 5000) { $script:freshFrames++ }
|
||||
}
|
||||
return [pscustomobject]$record
|
||||
}
|
||||
|
||||
function Get-MigrationPlan {
|
||||
param($Snapshot)
|
||||
if (-not $SourceEndpointId -or -not $FromNodeId -or -not $ToNodeId -or $FromNodeId -eq $ToNodeId) {
|
||||
throw '迁移准备需提供来源 endpoint、不同的起点和终点 nodeId。'
|
||||
}
|
||||
$source = @($Snapshot.nodes | Where-Object { $_.nodeId -eq $FromNodeId })
|
||||
$target = @($Snapshot.nodes | Where-Object { $_.nodeId -eq $ToNodeId })
|
||||
if ($source.Count -ne 1 -or $target.Count -ne 1 -or -not $source[0].online -or -not $target[0].online -or
|
||||
-not $target[0].enabled -or [int]$target[0].imageInputPort -lt 1) { throw '迁移节点未在线或目标原图接入未就绪。' }
|
||||
# 强制指定目标需要临时限制其他节点,因此只允许当前来源这一项活任务存在。
|
||||
# 用户正在运行的其他试验或其他原图通道存在时拒绝操作,避免改变无关调度。
|
||||
$foreign = @($Snapshot.tasks | Where-Object { $_.status -ne 'ENDED' -and
|
||||
(-not $_.PSObject.Properties['sourceEndpointId'] -or $_.sourceEndpointId -ne $SourceEndpointId) })
|
||||
if ($foreign.Count -gt 0) { throw '存在其他活任务,不能通过全节点容量执行定向迁移。' }
|
||||
$changes = @($Snapshot.nodes | Where-Object online | ForEach-Object {
|
||||
[pscustomobject]@{ nodeId = $_.nodeId; savedCapacity = [int]$_.capacity
|
||||
temporaryCapacity = $(if ($_.nodeId -eq $ToNodeId) { [Math]::Max(1, [int]$_.capacity) } else { 0 }) }
|
||||
})
|
||||
return [pscustomobject]@{ coordinatorId = $Snapshot.serviceId; endpointId = $SourceEndpointId
|
||||
fromNodeId = $FromNodeId; toNodeId = $ToNodeId; capacities = $changes }
|
||||
}
|
||||
|
||||
function Set-TemporaryCapacity {
|
||||
param([string]$NodeId, [int]$Capacity)
|
||||
$response = Invoke-XtrhImageSigned -BaseUrl $CoordinatorUrl -Path ('/control/nodes/' + [Uri]::EscapeDataString($NodeId) + '/capacity') `
|
||||
-Method PATCH -Payload @{ capacity = $Capacity }
|
||||
if ($response.Status -ne 200) { throw ('节点容量变更失败,HTTP ' + $response.Status) }
|
||||
}
|
||||
|
||||
$script:observationRoot = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
[void][IO.Directory]::CreateDirectory($script:observationRoot)
|
||||
$script:journalFile = Join-Path $script:observationRoot 'frames.jsonl'
|
||||
$script:lastFrameId = ''; $script:lastNodeId = ''; $script:lastVersion = 0L; $script:verifiedFrames = 0; $script:freshFrames = 0
|
||||
$script:lastTaskState = ''
|
||||
$changed = [Collections.Generic.List[object]]::new()
|
||||
$plan = $null
|
||||
Initialize-XtrhImageVerification -CredentialFile $CredentialFile
|
||||
try {
|
||||
$initial = Get-XtrhImageSnapshot -CoordinatorUrl $CoordinatorUrl
|
||||
$inventory = [ordered]@{ observedTime = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(); epoch = $initial.epoch
|
||||
nodes = @($initial.nodes | Select-Object nodeId,name,nodeRole,online,baseUrl,imageHost,imageInputPort,capacity,running,reserved)
|
||||
routes = @($initial.routes | Select-Object endpointId,sourceHost,sourcePort,relayPort,targetHost,targetPort)
|
||||
tasks = @($initial.tasks | Select-Object taskId,mediaKind,sourceEndpointId,collectorId,desired,status,nodeId,version,imageReady,lastFrameTime) }
|
||||
[IO.File]::WriteAllText((Join-Path $script:observationRoot 'inventory.json'), ($inventory | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false))
|
||||
if ($InventoryOnly) {
|
||||
Write-Output ('只读库存已保存:在线节点 ' + @($initial.nodes | Where-Object online).Count + ',任务 ' + @($initial.tasks).Count + ',UDP 路由 ' + @($initial.routes).Count)
|
||||
return
|
||||
}
|
||||
if (-not $SourceEndpointId) { throw '持续观测需提供正式采集连接的 source endpointId。' }
|
||||
if ($PrepareMigration -or $Migrate) {
|
||||
$plan = Get-MigrationPlan -Snapshot $initial
|
||||
[IO.File]::WriteAllText((Join-Path $script:observationRoot 'migration-plan.json'), ($plan | ConvertTo-Json -Depth 6), [Text.UTF8Encoding]::new($false))
|
||||
if ($PrepareMigration -and -not $Migrate) { Write-Output '迁移计划与原容量已保存,未执行任何变更。'; return }
|
||||
}
|
||||
if ($Migrate) {
|
||||
$active = Get-ImageTask -Snapshot $initial
|
||||
if ($null -eq $active -or -not $active.PSObject.Properties['nodeId'] -or $active.nodeId -ne $FromNodeId) {
|
||||
throw '来源任务当前不在指定起点,拒绝执行迁移。'
|
||||
}
|
||||
# 先给目标留足接管空间,再限制其他节点;旧接收端等目标收到新鲜图后才撤销。
|
||||
foreach ($change in @($plan.capacities | Sort-Object { if ($_.nodeId -eq $ToNodeId) { 0 } else { 1 } })) {
|
||||
if ($change.savedCapacity -eq $change.temporaryCapacity) { continue }
|
||||
$changed.Add($change)
|
||||
Set-TemporaryCapacity -NodeId $change.nodeId -Capacity $change.temporaryCapacity
|
||||
}
|
||||
}
|
||||
$deadline = [DateTimeOffset]::UtcNow.AddSeconds($DurationSeconds)
|
||||
$migrationObserved = $false
|
||||
$oldVersionRevoked = $false
|
||||
while ([DateTimeOffset]::UtcNow -lt $deadline) {
|
||||
$snapshot = Get-XtrhImageSnapshot -CoordinatorUrl $CoordinatorUrl
|
||||
Write-ImageTaskState -Snapshot $snapshot
|
||||
$frame = Read-CurrentOriginalFrame -Snapshot $snapshot
|
||||
if ($Migrate -and $null -ne $frame -and $frame.nodeId -eq $ToNodeId -and $frame.ageMs -ge -5000 -and $frame.ageMs -le 5000) {
|
||||
$migrationObserved = $true
|
||||
}
|
||||
if ($Migrate -and $migrationObserved -and -not $oldVersionRevoked) {
|
||||
# 新节点接管后旧节点需及时释放许可;首个心跳之前可能仍可读,窗口内持续检查。
|
||||
$oldNode = @($initial.nodes | Where-Object { $_.nodeId -eq $FromNodeId })[0]
|
||||
$oldPath = '/node/images/' + [Uri]::EscapeDataString([string]$active.taskId) + '/' + [long]$active.version + '/latest'
|
||||
$oldResponse = Invoke-XtrhImageSigned -BaseUrl $oldNode.baseUrl -Path $oldPath
|
||||
if ($oldResponse.Status -eq 404) {
|
||||
$oldVersionRevoked = $true
|
||||
$revokedRecord = [ordered]@{ event = 'old-version-revoked'; observedTime = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
||||
endpointId = $SourceEndpointId; nodeId = $FromNodeId; version = [long]$active.version; signedHttpStatus = 404 }
|
||||
[IO.File]::AppendAllText((Join-Path $script:observationRoot 'migration-events.jsonl'),
|
||||
($revokedRecord | ConvertTo-Json -Compress) + [Environment]::NewLine, [Text.UTF8Encoding]::new($false))
|
||||
Write-Information '旧节点分配已撤销:私有原图查询返回签名 404。' -InformationAction Continue
|
||||
} elseif ($oldResponse.Status -ne 200) { throw '旧节点未明确返回有效图片或已撤销状态。' }
|
||||
}
|
||||
Start-Sleep -Milliseconds $PollMilliseconds
|
||||
}
|
||||
if ($script:verifiedFrames -eq 0) {
|
||||
# 部署前允许记录尚未登记,不将空窗口宣称为取图验收通过;迁移必须取得新图。
|
||||
if ($AllowPending -and -not $Migrate -and $script:lastTaskState.StartsWith('NOT_REGISTERED/')) {
|
||||
Write-Output '观测窗口结束:来源尚未登记,未得到原图,原图接入尚未验证。'
|
||||
return
|
||||
}
|
||||
throw '观测期间没有取得经过签名验证的原图。'
|
||||
}
|
||||
if ($script:freshFrames -eq 0) { throw '只读到了旧缓存,观测期间没有收到五秒内的新鲜原图。' }
|
||||
if ($Migrate -and -not $migrationObserved) { throw '观测期间未证明目标节点已接管新鲜原图。' }
|
||||
if ($Migrate -and -not $oldVersionRevoked) { throw '目标已接管,但未证明旧节点版本已撤销。' }
|
||||
Write-Output ('原图观测完成:验证 ' + $script:verifiedFrames + ' 个新帧,记录已保存。')
|
||||
} finally {
|
||||
# 仅 -Migrate 会进入恢复分支;默认观测与 -PrepareMigration 都没有容量写操作。
|
||||
# 目标/原节点先恢复,再恢复其余节点,使用保存的正式原值,不推断历史默认容量。
|
||||
$restoreFailed = $false
|
||||
foreach ($change in @($changed | Sort-Object { if ($_.nodeId -eq $FromNodeId) { 0 } else { 1 } })) {
|
||||
try { Set-TemporaryCapacity -NodeId $change.nodeId -Capacity $change.savedCapacity }
|
||||
catch { $restoreFailed = $true; Write-Warning ('容量恢复失败:node=' + $change.nodeId + ',原值=' + $change.savedCapacity + ';请使用 migration-plan.json 恢复。') }
|
||||
}
|
||||
Close-XtrhImageVerification
|
||||
if ($restoreFailed) { throw '部分原容量未恢复,请按保存的迁移计划处理。' }
|
||||
}
|
||||
Reference in New Issue
Block a user