$ErrorActionPreference = 'Stop' $project = Split-Path -Parent $PSScriptRoot Start-Transcript -Path (Join-Path $project 'runtime/full-readiness-transfer.log') -Force | Out-Null $binary = Join-Path $project 'build/linux-arm64/tera-clone-demo-api-full-readiness' $webBinary = Join-Path $project 'build/linux-arm64/tera-clone-demo-web-full-readiness' $timeBinary = Join-Path $project 'build/linux-arm64/tera-timed-full-readiness' $alarmBinary = Join-Path $project 'build/linux-arm64/tera-clone-demo-alarmd-full-readiness' $timeHash = (Get-FileHash -LiteralPath $timeBinary -Algorithm SHA256).Hash.ToLowerInvariant() $alarmHash = (Get-FileHash -LiteralPath $alarmBinary -Algorithm SHA256).Hash.ToLowerInvariant() $webHash = (Get-FileHash -LiteralPath $webBinary -Algorithm SHA256).Hash.ToLowerInvariant() $hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() $script = @' #!/bin/sh set -eu stage=/tmp/tera-full-readiness-20260909 mkdir -p "$stage" wget -q -T 20 -O "$stage/api" http://192.168.0.100:9001/api wget -q -T 20 -O "$stage/web" http://192.168.0.100:9001/web wget -q -T 20 -O "$stage/timed" http://192.168.0.100:9001/timed wget -q -T 20 -O "$stage/alarmd" http://192.168.0.100:9001/alarmd echo '__TIMEHASH__ '"$stage/timed" | sha256sum -c - echo '__ALARMHASH__ '"$stage/alarmd" | sha256sum -c - echo '__HASH__ '"$stage/api" | sha256sum -c - echo '__WEBHASH__ '"$stage/web" | sha256sum -c - backup=/userdata/tera-backups/full-readiness-$(date +%Y%m%d-%H%M%S) mkdir -p "$backup" cp -p /usr/bin/tera-clone-demo-api "$backup/api" cp -p /usr/bin/tera-clone-demo-web "$backup/web" cp -p /usr/bin/tera-timed "$backup/timed" cp -p /usr/bin/tera-clone-demo-alarmd "$backup/alarmd" echo "BACKUP=$backup" cp "$stage/api" /usr/bin/tera-clone-demo-api.next cp "$stage/web" /usr/bin/tera-clone-demo-web.next chmod 0755 /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-web.next cp "$stage/timed" /usr/bin/tera-timed.next cp "$stage/alarmd" /usr/bin/tera-clone-demo-alarmd.next chmod 0755 /usr/bin/tera-timed.next /usr/bin/tera-clone-demo-alarmd.next rollback() { trap - EXIT /etc/init.d/S61tera-clone-demo-web stop || true /etc/init.d/S51tera-clone-demo-api stop || true /etc/init.d/S48tera-clone-demo-alarmd stop || true /etc/init.d/S45tera-timed stop || true sleep 2 cp "$backup/api" /usr/bin/tera-clone-demo-api.restore cp "$backup/web" /usr/bin/tera-clone-demo-web.restore chmod 0755 /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-web.restore mv /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-api mv /usr/bin/tera-clone-demo-web.restore /usr/bin/tera-clone-demo-web cp "$backup/timed" /usr/bin/tera-timed.restore cp "$backup/alarmd" /usr/bin/tera-clone-demo-alarmd.restore chmod 0755 /usr/bin/tera-timed.restore /usr/bin/tera-clone-demo-alarmd.restore mv /usr/bin/tera-timed.restore /usr/bin/tera-timed mv /usr/bin/tera-clone-demo-alarmd.restore /usr/bin/tera-clone-demo-alarmd /etc/init.d/S45tera-timed start /etc/init.d/S48tera-clone-demo-alarmd start /etc/init.d/S51tera-clone-demo-api start /etc/init.d/S61tera-clone-demo-web start echo FULL_READINESS_ROLLED_BACK } trap rollback EXIT /etc/init.d/S61tera-clone-demo-web stop /etc/init.d/S51tera-clone-demo-api stop /etc/init.d/S48tera-clone-demo-alarmd stop /etc/init.d/S45tera-timed stop sleep 2 mv /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-api mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web mv /usr/bin/tera-timed.next /usr/bin/tera-timed mv /usr/bin/tera-clone-demo-alarmd.next /usr/bin/tera-clone-demo-alarmd /etc/init.d/S45tera-timed start /etc/init.d/S48tera-clone-demo-alarmd start ( set -a . /etc/tera/tera-clone-demo-api.env set +a start-stop-daemon -S -q -b -m -p /run/tera-clone-demo-api.pid -c tera:tera -x /bin/sh -- -c 'exec /usr/bin/tera-clone-demo-api > /run/tera-clone-demo-api/full-readiness-startup.log 2>&1' ) /etc/init.d/S61tera-clone-demo-web start ready=0 for n in $(seq 1 120); do if wget -q -T 2 -O "$stage/status" http://127.0.0.1:8083/login/backend-status; then if grep -q '"ready":true' "$stage/status"; then ready=1; break; fi if grep -q '"state":"failed"' "$stage/status"; then cat "$stage/status"; grep -E "startup|reconciliation" /run/tera-clone-demo-api/full-readiness-startup.log; exit 1; fi fi sleep 1 done cat "$stage/status" if [ "$ready" != 1 ]; then grep -E "startup|reconciliation|observation" /run/tera-clone-demo-api/full-readiness-startup.log || true exit 1 fi wget -q -T 3 -O "$stage/login.js" http://127.0.0.1:8083/static/js/login.js grep -q '/login/bootstrap-time' "$stage/login.js" sha256sum /usr/bin/tera-clone-demo-api /usr/bin/tera-clone-demo-web /usr/bin/tera-timed /usr/bin/tera-clone-demo-alarmd sync trap - EXIT echo FULL_READINESS_DEPLOY_OK '@ $script = $script.Replace('__HASH__', $hash).Replace('__WEBHASH__', $webHash).Replace('__TIMEHASH__', $timeHash).Replace('__ALARMHASH__', $alarmHash).Replace("`r`n", "`n") + "`n" $rule = 'TERA-full-readiness-' + [guid]::NewGuid().ToString('N') $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add('http://192.168.0.100:9001/') $serial = New-Object System.IO.Ports.SerialPort 'COM5',115200,'None',8,'One' $serial.Encoding = [Text.Encoding]::UTF8 try { New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null $listener.Start() $serial.Open() $serial.WriteLine('wget -q -O /tmp/deploy-full-readiness.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-full-readiness.sh') $deadline = [DateTime]::UtcNow.AddSeconds(360) $output = '' $pending = $listener.GetContextAsync() while ([DateTime]::UtcNow -lt $deadline) { if ($pending.IsCompleted) { $context = $pending.GetAwaiter().GetResult() if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { $context.Response.StatusCode = 403 $context.Response.Close() } else { $bytes = $null switch ($context.Request.Url.AbsolutePath) { '/deploy.sh' { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } '/web' { $bytes = [IO.File]::ReadAllBytes($webBinary) } '/api' { $bytes = [IO.File]::ReadAllBytes($binary) } '/timed' { $bytes = [IO.File]::ReadAllBytes($timeBinary) } '/alarmd' { $bytes = [IO.File]::ReadAllBytes($alarmBinary) } default { $context.Response.StatusCode = 404 } } if ($null -ne $bytes) { $context.Response.ContentLength64 = $bytes.Length $context.Response.OutputStream.Write($bytes, 0, $bytes.Length) } $context.Response.Close() } $pending = $listener.GetContextAsync() } $chunk = $serial.ReadExisting() if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } if ($output.Contains('FULL_READINESS_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } if ($output.Contains('FULL_READINESS_DEPLOY_OK')) { break } Start-Sleep -Milliseconds 100 } if (-not $output.Contains('FULL_READINESS_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } if (-not $output.Contains($hash) -or -not $output.Contains($webHash) -or -not $output.Contains($timeHash) -or -not $output.Contains($alarmHash)) { throw 'Deployed hash missing from console output.' } Write-Output "VERIFIED deployed SHA256=$hash and API login-policy endpoint" } finally { if ($serial.IsOpen) { $serial.Close() } $serial.Dispose() $listener.Close() Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue Stop-Transcript | Out-Null }