mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-15 00:35:47 -07:00
1794 lines
63 KiB
PowerShell
1794 lines
63 KiB
PowerShell
[CmdletBinding()]
|
|
param()
|
|
|
|
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
|
$script:BorealisTlsInitialized = $false
|
|
$script:BorealisTrustedThumbprints = @()
|
|
$script:BorealisCallbackApplied = $false
|
|
$script:AgentPythonHttpHelper = ''
|
|
$symbols = @{
|
|
Success = [char]0x2705
|
|
Running = [char]0x23F3
|
|
Fail = [char]0x274C
|
|
Info = [char]0x2139
|
|
}
|
|
|
|
function Write-UpdateLog {
|
|
param(
|
|
[string]$Message,
|
|
[string]$Level = 'INFO',
|
|
[string]$Color
|
|
)
|
|
|
|
if (-not $Message) { return }
|
|
|
|
$timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
|
|
$normalized = if ($Level) { $Level } else { 'INFO' }
|
|
$normalized = $normalized.ToUpperInvariant()
|
|
|
|
if (-not $Color) {
|
|
switch ($normalized) {
|
|
'WARN' { $Color = 'Yellow' }
|
|
'ERROR' { $Color = 'Red' }
|
|
'STEP' { $Color = 'Cyan' }
|
|
'SUCCESS' { $Color = 'Green' }
|
|
default { $Color = $null }
|
|
}
|
|
}
|
|
|
|
$line = "[{0}] [{1}] {2}" -f $timestamp, $normalized, $Message
|
|
if ($Color) {
|
|
Write-Host $line -ForegroundColor $Color
|
|
} else {
|
|
Write-Host $line
|
|
}
|
|
}
|
|
|
|
$repositoryUrl = 'https://github.com/bunny-lab-io/Borealis.git'
|
|
|
|
function Write-ProgressStep {
|
|
param (
|
|
[string]$Message,
|
|
[string]$Status = $symbols["Info"]
|
|
)
|
|
Write-Host "`r$Status $Message... " -NoNewline
|
|
}
|
|
|
|
function Run-Step {
|
|
param (
|
|
[string] $Message,
|
|
[scriptblock]$Script
|
|
)
|
|
Write-ProgressStep -Message $Message -Status "$($symbols.Running)"
|
|
try {
|
|
& $Script
|
|
if ($LASTEXITCODE -eq 0 -or $?) {
|
|
Write-Host "`r$($symbols.Success) $Message "
|
|
} else {
|
|
throw "Non-zero exit code"
|
|
}
|
|
} catch {
|
|
Write-Host "`r$($symbols.Fail) $Message - Failed: $_ " -ForegroundColor Red
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Get-GitExecutablePath {
|
|
param(
|
|
[string]$ProjectRoot
|
|
)
|
|
|
|
$candidates = @()
|
|
if ($ProjectRoot) {
|
|
$candidates += (Join-Path $ProjectRoot 'Dependencies\git\cmd\git.exe')
|
|
$candidates += (Join-Path $ProjectRoot 'Dependencies\git\bin\git.exe')
|
|
}
|
|
|
|
foreach ($candidate in ($candidates | Select-Object -Unique)) {
|
|
try {
|
|
if (Test-Path $candidate -PathType Leaf) { return $candidate }
|
|
} catch {}
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
function Invoke-GitCommand {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$GitExe,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WorkingDirectory,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$Arguments
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($GitExe) -or -not (Test-Path $GitExe -PathType Leaf)) {
|
|
throw "Git executable not found at '$GitExe'"
|
|
}
|
|
|
|
if (-not (Test-Path $WorkingDirectory -PathType Container)) {
|
|
throw "Working directory '$WorkingDirectory' does not exist."
|
|
}
|
|
|
|
$fullArgs = @('-C', $WorkingDirectory) + $Arguments
|
|
$output = & $GitExe @fullArgs 2>&1
|
|
$exitCode = $LASTEXITCODE
|
|
if ($exitCode -ne 0) {
|
|
$joined = ($Arguments -join ' ')
|
|
$message = "git $joined failed with exit code $exitCode."
|
|
if ($output) {
|
|
$message = "$message Output: $output"
|
|
}
|
|
throw $message
|
|
}
|
|
|
|
return $output
|
|
}
|
|
|
|
function Stop-AgentScheduledTasks {
|
|
param(
|
|
[string[]]$TaskNames
|
|
)
|
|
|
|
$stopped = @()
|
|
foreach ($name in $TaskNames) {
|
|
$taskExists = $false
|
|
try {
|
|
$null = Get-ScheduledTask -TaskName $name -ErrorAction Stop
|
|
$taskExists = $true
|
|
} catch {
|
|
try {
|
|
schtasks.exe /Query /TN "$name" 2>$null | Out-Null
|
|
if ($LASTEXITCODE -eq 0) { $taskExists = $true }
|
|
} catch {}
|
|
}
|
|
|
|
if (-not $taskExists) { continue }
|
|
|
|
Write-Host "Stopping scheduled task: $name" -ForegroundColor Yellow
|
|
$stopped += $name
|
|
try { Stop-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue } catch {}
|
|
try { schtasks.exe /End /TN "$name" /F 2>$null | Out-Null } catch {}
|
|
try {
|
|
for ($i = 0; $i -lt 20; $i++) {
|
|
$info = Get-ScheduledTaskInfo -TaskName $name -ErrorAction Stop
|
|
if ($info.State -ne 'Running' -and $info.State -ne 'Queued') { break }
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
return ,$stopped
|
|
}
|
|
|
|
function Start-AgentScheduledTasks {
|
|
param(
|
|
[string[]]$TaskNames
|
|
)
|
|
|
|
foreach ($name in $TaskNames) {
|
|
Write-Host "Restarting scheduled task: $name" -ForegroundColor Green
|
|
try {
|
|
Start-ScheduledTask -TaskName $name -ErrorAction Stop | Out-Null
|
|
continue
|
|
} catch {}
|
|
|
|
try { schtasks.exe /Run /TN "$name" 2>$null | Out-Null } catch {}
|
|
}
|
|
}
|
|
|
|
function Stop-AgentPythonProcesses {
|
|
param(
|
|
[string[]]$ProcessNames = @('python', 'pythonw')
|
|
)
|
|
|
|
foreach ($name in ($ProcessNames | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)) {
|
|
$name = $name.Trim()
|
|
if (-not $name) { continue }
|
|
|
|
$processes = @()
|
|
try {
|
|
$processes = Get-Process -Name $name -ErrorAction Stop
|
|
} catch {
|
|
$processes = @()
|
|
}
|
|
|
|
foreach ($proc in $processes) {
|
|
$procId = $null
|
|
$procName = $null
|
|
try {
|
|
$procId = $proc.Id
|
|
$procName = $proc.ProcessName
|
|
} catch {}
|
|
|
|
if ($procId -eq $null) { continue }
|
|
|
|
if (-not $procName) { $procName = $name }
|
|
|
|
$stopped = $false
|
|
Write-Host "Stopping process: $procName (PID $procId)" -ForegroundColor Yellow
|
|
|
|
try {
|
|
Stop-Process -Id $procId -Force -ErrorAction Stop
|
|
$stopped = $true
|
|
} catch {
|
|
Write-Host "Unable to stop process via Stop-Process: $procName (PID $procId). $_" -ForegroundColor DarkYellow
|
|
}
|
|
|
|
if (-not $stopped) {
|
|
try {
|
|
$taskkillOutput = taskkill.exe /PID $procId /F 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
$stopped = $true
|
|
} else {
|
|
if ($taskkillOutput) {
|
|
Write-Host "taskkill.exe returned exit code ${LASTEXITCODE} for PID ${procId}: $taskkillOutput" -ForegroundColor DarkYellow
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Host "Unable to stop process via taskkill.exe: $procName (PID $procId). $_" -ForegroundColor DarkYellow
|
|
}
|
|
}
|
|
|
|
if (-not $stopped) {
|
|
Write-Host "Process still running after termination attempts: $procName (PID $procId)" -ForegroundColor DarkYellow
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-BorealisServerUrl {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
$serverBaseUrl = $env:BOREALIS_SERVER_URL
|
|
if (-not $serverBaseUrl) {
|
|
try {
|
|
if (-not $AgentRoot) { $AgentRoot = $scriptDir }
|
|
$settingsDir = Join-Path $AgentRoot 'Settings'
|
|
$serverUrlFile = Join-Path $settingsDir 'server_url.txt'
|
|
if (Test-Path $serverUrlFile -PathType Leaf) {
|
|
$content = Get-Content -Path $serverUrlFile -Raw -ErrorAction Stop
|
|
if ($content) { $serverBaseUrl = $content.Trim() }
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if (-not $serverBaseUrl) { $serverBaseUrl = 'https://localhost:5000' }
|
|
|
|
$resolved = Resolve-BorealisServerUrl -Url $serverBaseUrl
|
|
if ([string]::IsNullOrWhiteSpace($resolved)) {
|
|
return 'https://localhost:5000'
|
|
}
|
|
|
|
Write-UpdateLog ("Resolved Borealis server URL: {0}" -f $resolved) 'INFO'
|
|
return $resolved
|
|
}
|
|
|
|
function Resolve-BorealisServerUrl {
|
|
param(
|
|
[string]$Url
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Url)) {
|
|
return ''
|
|
}
|
|
|
|
$candidate = $Url.Trim()
|
|
if ($candidate -notmatch '^[A-Za-z][A-Za-z0-9+.-]*://') {
|
|
$candidate = "https://$candidate"
|
|
}
|
|
|
|
try {
|
|
$builder = New-Object System.UriBuilder($candidate)
|
|
} catch {
|
|
return $candidate.TrimEnd('/')
|
|
}
|
|
|
|
$allowPlaintext = $false
|
|
try {
|
|
$allowValue = $env:BOREALIS_ALLOW_PLAINTEXT
|
|
if ($allowValue) {
|
|
$normalizedAllow = $allowValue.ToString().Trim().ToLowerInvariant()
|
|
if ($normalizedAllow -and @('1','true','yes','on') -contains $normalizedAllow) {
|
|
$allowPlaintext = $true
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
if ($builder.Scheme -eq 'http' -and -not $allowPlaintext) {
|
|
$hostName = $builder.Host.ToLowerInvariant()
|
|
if ($hostName -in @('localhost','127.0.0.1','::1')) {
|
|
$builder.Scheme = 'https'
|
|
if ($builder.Port -eq -1 -or $builder.Port -eq 80) {
|
|
$builder.Port = 5000
|
|
}
|
|
}
|
|
}
|
|
|
|
return $builder.Uri.AbsoluteUri.TrimEnd('/')
|
|
}
|
|
|
|
function Get-AgentCertificateCachePath {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
$settingsDir = Get-AgentSettingsDirectory -AgentRoot $AgentRoot
|
|
if (-not $settingsDir) { return '' }
|
|
return (Join-Path $settingsDir 'server_certificate.crt')
|
|
}
|
|
|
|
function Get-ExistingServerCertificatePath {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
$path = Get-AgentCertificateCachePath -AgentRoot $AgentRoot
|
|
if ($path -and (Test-Path $path -PathType Leaf)) {
|
|
return $path
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function Save-ServerCertificateCache {
|
|
param(
|
|
[string]$AgentRoot,
|
|
[string]$CertificatePem
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($CertificatePem)) {
|
|
return ''
|
|
}
|
|
|
|
$targetPath = Get-AgentCertificateCachePath -AgentRoot $AgentRoot
|
|
if (-not $targetPath) {
|
|
return ''
|
|
}
|
|
|
|
$targetDir = Split-Path -Path $targetPath -Parent
|
|
try {
|
|
if (-not (Test-Path $targetDir -PathType Container)) {
|
|
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
|
|
}
|
|
} catch {
|
|
Write-UpdateLog ("Failed to create certificate cache directory {0}: {1}" -f $targetDir, $_.Exception.Message) 'WARN'
|
|
return ''
|
|
}
|
|
|
|
try {
|
|
Set-Content -Path $targetPath -Value $CertificatePem -Encoding UTF8
|
|
Write-UpdateLog ("Cached server certificate to {0}" -f $targetPath) 'INFO'
|
|
return $targetPath
|
|
} catch {
|
|
Write-UpdateLog ("Failed to cache server certificate: {0}" -f $_.Exception.Message) 'WARN'
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function Get-CertificatesFromPem {
|
|
param(
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not $Path -or -not (Test-Path $Path -PathType Leaf)) {
|
|
return @()
|
|
}
|
|
|
|
$lines = @()
|
|
try {
|
|
$lines = Get-Content -Path $Path -ErrorAction Stop
|
|
} catch {
|
|
return @()
|
|
}
|
|
|
|
if (-not $lines -or $lines.Count -eq 0) {
|
|
Write-Verbose ("PEM content at {0} is empty." -f $Path)
|
|
return @()
|
|
}
|
|
|
|
$collecting = $false
|
|
$buffer = ''
|
|
$certificates = @()
|
|
|
|
foreach ($line in $lines) {
|
|
$lineValue = if ($null -ne $line) { $line } else { '' }
|
|
$trimmed = $lineValue.ToString().Trim()
|
|
if ($trimmed -eq '-----BEGIN CERTIFICATE-----') {
|
|
Write-Verbose ("Detected certificate begin marker in {0}." -f $Path)
|
|
$collecting = $true
|
|
$buffer = ''
|
|
continue
|
|
}
|
|
if ($trimmed -eq '-----END CERTIFICATE-----') {
|
|
Write-Verbose ("Detected certificate end marker in {0}; buffer length = {1} characters." -f $Path, $buffer.Length)
|
|
if ($collecting -and $buffer) {
|
|
try {
|
|
$raw = [Convert]::FromBase64String($buffer)
|
|
$cert = $null
|
|
try { $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($raw) } catch {}
|
|
if (-not $cert) {
|
|
try {
|
|
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
|
|
$raw,
|
|
'',
|
|
[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
|
|
)
|
|
} catch {}
|
|
}
|
|
if (-not $cert) {
|
|
throw "Unable to hydrate X509Certificate2 from PEM fragment."
|
|
}
|
|
$certificates += $cert
|
|
Write-Verbose ("Loaded certificate '{0}' from {1}" -f $cert.Subject, $Path)
|
|
} catch {
|
|
Write-Verbose ("Failed to decode certificate block from {0}: {1}" -f $Path, $_.Exception.Message)
|
|
}
|
|
}
|
|
$collecting = $false
|
|
$buffer = ''
|
|
continue
|
|
}
|
|
if ($collecting) {
|
|
$buffer += $trimmed
|
|
}
|
|
}
|
|
|
|
Write-Verbose ("Discovered {0} certificate(s) in {1}" -f $certificates.Count, $Path)
|
|
return $certificates
|
|
}
|
|
|
|
function Ensure-BorealisCertificateValidator {
|
|
if (-not ('Borealis.Update.CertificateValidator' -as [Type])) {
|
|
$typeDefinition = @"
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Net.Security;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
|
|
namespace Borealis.Update
|
|
{
|
|
public static class CertificateValidator
|
|
{
|
|
private static readonly HashSet<string> _trusted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
public static bool AllowLoopback { get; set; }
|
|
|
|
static CertificateValidator()
|
|
{
|
|
AllowLoopback = true;
|
|
}
|
|
|
|
public static void ReplaceTrustedThumbprints(string[] thumbprints)
|
|
{
|
|
_trusted.Clear();
|
|
if (thumbprints == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var thumb in thumbprints)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(thumb))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_trusted.Add(thumb);
|
|
}
|
|
}
|
|
|
|
public static bool Validate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
|
|
{
|
|
if (errors == SslPolicyErrors.None)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
X509Certificate2 cert2 = certificate as X509Certificate2;
|
|
if (cert2 == null && certificate != null)
|
|
{
|
|
cert2 = new X509Certificate2(certificate);
|
|
}
|
|
|
|
if (cert2 != null && _trusted.Contains(cert2.Thumbprint))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (chain != null)
|
|
{
|
|
foreach (var element in chain.ChainElements)
|
|
{
|
|
if (element.Certificate != null && _trusted.Contains(element.Certificate.Thumbprint))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (AllowLoopback)
|
|
{
|
|
var request = sender as HttpWebRequest;
|
|
if (request != null && request.RequestUri != null)
|
|
{
|
|
var host = request.RequestUri.DnsSafeHost;
|
|
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(host, "127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
|
|
string.Equals(host, "::1", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
"@
|
|
Add-Type -TypeDefinition $typeDefinition -Language CSharp -ErrorAction Stop
|
|
}
|
|
}
|
|
|
|
function Initialize-BorealisTlsContext {
|
|
param(
|
|
[string]$AgentRoot,
|
|
[string]$ServerBaseUrl
|
|
)
|
|
|
|
if ($script:BorealisTlsInitialized -and $script:BorealisTrustedThumbprints.Count -gt 0) {
|
|
return
|
|
}
|
|
|
|
$trusted = @()
|
|
$cachedCertPath = Get-ExistingServerCertificatePath -AgentRoot $AgentRoot
|
|
if ($cachedCertPath) {
|
|
Write-UpdateLog ("Using cached TLS certificate: {0}" -f $cachedCertPath) 'INFO'
|
|
try {
|
|
$trusted += Get-CertificatesFromPem -Path $cachedCertPath
|
|
} catch {
|
|
Write-UpdateLog ("Failed to load cached TLS certificate: {0}" -f $_.Exception.Message) 'WARN'
|
|
}
|
|
}
|
|
|
|
if ($trusted.Count -gt 0) {
|
|
$script:BorealisTrustedThumbprints = $trusted | ForEach-Object { $_.Thumbprint.ToUpperInvariant() } | Sort-Object -Unique
|
|
Write-Verbose ("Borealis TLS trust store loaded {0} certificate(s)." -f $script:BorealisTrustedThumbprints.Count)
|
|
Write-UpdateLog ("TLS trust store initialized with {0} certificate(s)." -f $script:BorealisTrustedThumbprints.Count) 'INFO'
|
|
} else {
|
|
$script:BorealisTrustedThumbprints = @()
|
|
Write-Verbose "No Borealis TLS certificates located; loopback hosts will be allowed without CA verification."
|
|
Write-UpdateLog "No TLS certificates found; falling back to loopback-only allowance." 'WARN'
|
|
}
|
|
|
|
Ensure-BorealisCertificateValidator
|
|
try {
|
|
[Borealis.Update.CertificateValidator]::ReplaceTrustedThumbprints($script:BorealisTrustedThumbprints)
|
|
} catch {}
|
|
|
|
try {
|
|
$protocolType = [System.Net.SecurityProtocolType]
|
|
$hasSystemDefault = [System.Enum]::IsDefined($protocolType, 'SystemDefault')
|
|
if ($hasSystemDefault) {
|
|
# Allow the OS to negotiate the strongest available protocol (TLS 1.3 on modern hosts).
|
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::SystemDefault
|
|
Write-UpdateLog "SecurityProtocol configured to SystemDefault (OS-negotiated)." 'DEBUG'
|
|
} else {
|
|
$protocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11
|
|
if ([System.Enum]::IsDefined($protocolType, 'Tls13')) {
|
|
$tls13 = [System.Enum]::Parse($protocolType, 'Tls13')
|
|
$protocol = $protocol -bor $tls13
|
|
}
|
|
[System.Net.ServicePointManager]::SecurityProtocol = $protocol
|
|
Write-UpdateLog ("SecurityProtocol configured to legacy mask: {0}" -f $protocol) 'DEBUG'
|
|
}
|
|
} catch {}
|
|
|
|
if (-not $script:BorealisCallbackApplied) {
|
|
try {
|
|
$callback = New-Object System.Net.Security.RemoteCertificateValidationCallback([Borealis.Update.CertificateValidator]::Validate)
|
|
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $callback
|
|
$script:BorealisCallbackApplied = $true
|
|
} catch {}
|
|
}
|
|
|
|
$script:BorealisTlsInitialized = $true
|
|
}
|
|
|
|
function Get-AgentPythonExecutable {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
$candidates = @()
|
|
if ($AgentRoot) {
|
|
try {
|
|
$agentParent = Split-Path $AgentRoot -Parent
|
|
if ($agentParent) {
|
|
$candidates += (Join-Path $agentParent 'Scripts\python.exe')
|
|
}
|
|
} catch {}
|
|
}
|
|
$candidates += (Join-Path $scriptDir 'Agent\Scripts\python.exe')
|
|
$candidates += (Join-Path $scriptDir 'Dependencies\Python\python.exe')
|
|
foreach ($candidate in ($candidates | Select-Object -Unique)) {
|
|
try {
|
|
if (Test-Path $candidate -PathType Leaf) {
|
|
Write-UpdateLog ("Using Python executable for HTTP helper: {0}" -f $candidate) 'DEBUG'
|
|
return $candidate
|
|
}
|
|
} catch {}
|
|
}
|
|
Write-UpdateLog "No Python executable found for HTTP helper fallback." 'WARN'
|
|
return ''
|
|
}
|
|
|
|
function Ensure-AgentPythonHttpHelper {
|
|
if ($script:AgentPythonHttpHelper -and (Test-Path $script:AgentPythonHttpHelper -PathType Leaf)) {
|
|
return $script:AgentPythonHttpHelper
|
|
}
|
|
|
|
$helperDir = Join-Path ([System.IO.Path]::GetTempPath()) 'BorealisUpdate'
|
|
try {
|
|
if (-not (Test-Path $helperDir -PathType Container)) {
|
|
New-Item -ItemType Directory -Force -Path $helperDir | Out-Null
|
|
}
|
|
} catch {}
|
|
|
|
$helperPath = Join-Path $helperDir 'agent_http_client.py'
|
|
$helperSource = @"
|
|
import json
|
|
import ssl
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
|
|
def _build_context(cafile):
|
|
if cafile:
|
|
ctx = ssl.create_default_context()
|
|
try:
|
|
ctx.load_verify_locations(cafile=cafile)
|
|
except Exception:
|
|
pass
|
|
minimum = getattr(ssl, "TLSVersion", None)
|
|
if minimum is not None:
|
|
try:
|
|
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
except Exception:
|
|
pass
|
|
return ctx
|
|
ctx = ssl._create_unverified_context()
|
|
try:
|
|
ctx.check_hostname = False
|
|
except Exception:
|
|
pass
|
|
try:
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
except Exception:
|
|
pass
|
|
return ctx
|
|
|
|
|
|
def _read_payload():
|
|
data = sys.stdin.buffer.read()
|
|
if not data:
|
|
return {}
|
|
try:
|
|
text = data.decode("utf-8-sig")
|
|
except Exception:
|
|
text = data.decode("utf-8", errors="ignore")
|
|
return json.loads(text)
|
|
|
|
|
|
def _peer_certificate(handle):
|
|
try:
|
|
raw = getattr(handle, "fp", None)
|
|
if raw is None:
|
|
raw = getattr(handle, "file", None)
|
|
if raw is None:
|
|
return None
|
|
raw = getattr(raw, "raw", raw)
|
|
sock = getattr(raw, "_sock", None)
|
|
if sock is None:
|
|
sock = getattr(raw, "socket", None)
|
|
if sock is None:
|
|
return None
|
|
der = sock.getpeercert(True)
|
|
if der:
|
|
return ssl.DER_cert_to_PEM_cert(der)
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
def main():
|
|
try:
|
|
payload = _read_payload()
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
json.dump({"error": "payload decode failed: %s" % (exc,)}, sys.stdout)
|
|
return 1
|
|
|
|
method = (payload.get("method") or "GET").upper()
|
|
url = payload.get("url")
|
|
headers = payload.get("headers") or {}
|
|
body = payload.get("body")
|
|
content_type = payload.get("content_type")
|
|
timeout = payload.get("timeout") or 30
|
|
cafile = payload.get("cafile")
|
|
|
|
if body is not None and not isinstance(body, (bytes, bytearray)):
|
|
body = str(body).encode("utf-8")
|
|
|
|
request = urllib.request.Request(url=url, data=body, method=method)
|
|
for key, value in headers.items():
|
|
if value is None:
|
|
continue
|
|
request.add_header(str(key), str(value))
|
|
|
|
if content_type and all(k.lower() != "content-type" for k in request.headers):
|
|
request.add_header("Content-Type", str(content_type))
|
|
|
|
ctx = _build_context(cafile if isinstance(cafile, str) else None)
|
|
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=float(timeout), context=ctx) as response:
|
|
text = response.read().decode("utf-8", errors="replace")
|
|
json.dump({"status": response.status, "body": text, "certificate": _peer_certificate(response)}, sys.stdout)
|
|
return 0
|
|
except urllib.error.HTTPError as exc:
|
|
text = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
|
|
json.dump({"status": exc.code, "body": text, "certificate": _peer_certificate(exc)}, sys.stdout)
|
|
return 0
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
json.dump({"error": str(exc)}, sys.stdout)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover - defensive
|
|
raise SystemExit(main())
|
|
"@
|
|
|
|
try {
|
|
Set-Content -Path $helperPath -Value $helperSource -Encoding UTF8 -Force
|
|
$script:AgentPythonHttpHelper = $helperPath
|
|
Write-UpdateLog ("Staged Python HTTP helper at {0}" -f $helperPath) 'DEBUG'
|
|
} catch {
|
|
$script:AgentPythonHttpHelper = ''
|
|
}
|
|
|
|
return $script:AgentPythonHttpHelper
|
|
}
|
|
|
|
function Invoke-AgentHttpRequest {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Method,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Uri,
|
|
|
|
[hashtable]$Headers,
|
|
[string]$Body,
|
|
[string]$ContentType,
|
|
[string]$AgentRoot,
|
|
[int]$TimeoutSeconds = 30
|
|
)
|
|
|
|
$headersToUse = @{}
|
|
if ($Headers) {
|
|
foreach ($key in $Headers.Keys) {
|
|
$value = $Headers[$key]
|
|
if ($null -ne $value -and $value.ToString()) {
|
|
$headersToUse[$key] = $value
|
|
}
|
|
}
|
|
}
|
|
|
|
$invokeParams = @{
|
|
Uri = $Uri
|
|
Method = $Method
|
|
Headers = $headersToUse
|
|
UseBasicParsing = $true
|
|
ErrorAction = 'Stop'
|
|
}
|
|
if ($Body) { $invokeParams['Body'] = $Body }
|
|
if ($ContentType) { $invokeParams['ContentType'] = $ContentType }
|
|
if ($TimeoutSeconds -gt 0) { $invokeParams['TimeoutSec'] = $TimeoutSeconds }
|
|
|
|
Write-UpdateLog ("HTTP {0} {1}" -f $Method.ToUpperInvariant(), $Uri) 'DEBUG'
|
|
try {
|
|
$response = Invoke-WebRequest @invokeParams
|
|
Write-UpdateLog ("Invoke-WebRequest succeeded (HTTP {0})." -f $response.StatusCode) 'DEBUG'
|
|
return [pscustomobject]@{
|
|
StatusCode = $response.StatusCode
|
|
Content = $response.Content
|
|
}
|
|
} catch {
|
|
Write-Verbose ("Invoke-WebRequest failed for {0}: {1}" -f $Uri, $_.Exception.Message)
|
|
Write-UpdateLog ("Invoke-WebRequest failed for {0}: {1}" -f $Uri, $_.Exception.Message) 'WARN'
|
|
}
|
|
|
|
$pythonExe = Get-AgentPythonExecutable -AgentRoot $AgentRoot
|
|
if (-not $pythonExe) {
|
|
Write-UpdateLog "Python executable for HTTP fallback not found." 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
$helperScript = Ensure-AgentPythonHttpHelper
|
|
if (-not $helperScript) {
|
|
Write-UpdateLog "Unable to stage Python HTTP helper script." 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
$cafile = Get-ExistingServerCertificatePath -AgentRoot $AgentRoot
|
|
if ($cafile) {
|
|
Write-UpdateLog ("Using cached TLS certificate for helper: {0}" -f $cafile) 'DEBUG'
|
|
} else {
|
|
Write-UpdateLog "No TLS bundle available; helper will skip certificate validation for this request." 'WARN'
|
|
}
|
|
$payload = @{
|
|
method = $Method
|
|
url = $Uri
|
|
headers = $headersToUse
|
|
body = $Body
|
|
content_type = $ContentType
|
|
timeout = $TimeoutSeconds
|
|
cafile = $cafile
|
|
} | ConvertTo-Json -Depth 6
|
|
|
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
$psi.FileName = $pythonExe
|
|
$psi.Arguments = ('"{0}"' -f $helperScript)
|
|
$psi.RedirectStandardInput = $true
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$psi.UseShellExecute = $false
|
|
$psi.CreateNoWindow = $true
|
|
|
|
try {
|
|
$process = [System.Diagnostics.Process]::Start($psi)
|
|
} catch {
|
|
Write-Verbose ("Failed to start Python helper: {0}" -f $_.Exception.Message)
|
|
Write-UpdateLog ("Failed to launch Python helper: {0}" -f $_.Exception.Message) 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
try {
|
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
$bytes = $utf8NoBom.GetBytes($payload)
|
|
$process.StandardInput.BaseStream.Write($bytes, 0, $bytes.Length)
|
|
$process.StandardInput.BaseStream.Flush()
|
|
$process.StandardInput.Close()
|
|
} catch {
|
|
Write-UpdateLog ("Failed to write payload to Python helper stdin: {0}" -f $_.Exception.Message) 'WARN'
|
|
try { $process.StandardInput.Close() } catch {}
|
|
}
|
|
|
|
$stdout = $process.StandardOutput.ReadToEnd()
|
|
$stderr = $process.StandardError.ReadToEnd()
|
|
$process.WaitForExit()
|
|
|
|
if ($stderr) {
|
|
Write-Verbose ("Python helper stderr: {0}" -f $stderr.Trim())
|
|
Write-UpdateLog ("Python helper stderr: {0}" -f $stderr.Trim()) 'DEBUG'
|
|
}
|
|
|
|
if (-not $stdout) {
|
|
Write-UpdateLog "Python helper returned empty response." 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
try {
|
|
$json = $stdout | ConvertFrom-Json -ErrorAction Stop
|
|
} catch {
|
|
Write-Verbose ("Unable to parse Python helper output: {0}" -f $_.Exception.Message)
|
|
return $null
|
|
}
|
|
|
|
if ($json.error) {
|
|
Write-Verbose ("Python helper reported error: {0}" -f $json.error)
|
|
Write-UpdateLog ("Python helper error: {0}" -f $json.error) 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
if ($json.certificate) {
|
|
$savedPath = Save-ServerCertificateCache -AgentRoot $AgentRoot -CertificatePem $json.certificate
|
|
if ($savedPath) {
|
|
$script:BorealisTlsInitialized = $false
|
|
Initialize-BorealisTlsContext -AgentRoot $AgentRoot -ServerBaseUrl $Uri
|
|
}
|
|
}
|
|
|
|
Write-UpdateLog ("Python helper completed HTTP call with status {0}." -f $json.status) 'DEBUG'
|
|
return [pscustomobject]@{
|
|
StatusCode = $json.status
|
|
Content = $json.body
|
|
Certificate = $json.certificate
|
|
}
|
|
}
|
|
|
|
function Get-AgentServiceId {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
if (-not $AgentRoot) { $AgentRoot = $scriptDir }
|
|
$settingsDir = Join-Path $AgentRoot 'Settings'
|
|
$candidates = @(
|
|
(Join-Path $settingsDir 'agent_settings_SYSTEM.json')
|
|
(Join-Path $settingsDir 'agent_settings_CURRENTUSER.json')
|
|
(Join-Path $settingsDir 'agent_settings_svc.json')
|
|
(Join-Path $settingsDir 'agent_settings_user.json')
|
|
(Join-Path $settingsDir 'agent_settings.json')
|
|
)
|
|
|
|
foreach ($path in $candidates) {
|
|
try {
|
|
if (Test-Path $path -PathType Leaf) {
|
|
$raw = Get-Content -Path $path -Raw -ErrorAction Stop
|
|
if (-not $raw) { continue }
|
|
$cfg = $raw | ConvertFrom-Json -ErrorAction Stop
|
|
$value = ($cfg.agent_id)
|
|
if ($value) { return ($value.ToString()).Trim() }
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
function Get-AgentGuid {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
if (-not $AgentRoot) { $AgentRoot = $scriptDir }
|
|
$candidates = @()
|
|
if ($AgentRoot) {
|
|
$settingsDir = Join-Path $AgentRoot 'Settings'
|
|
if ($settingsDir) {
|
|
$settingsGuid = Join-Path $settingsDir 'Agent_GUID.txt'
|
|
if ($candidates -notcontains $settingsGuid) { $candidates += $settingsGuid }
|
|
}
|
|
$legacyPath = Join-Path $AgentRoot 'agent_GUID'
|
|
if ($candidates -notcontains $legacyPath) { $candidates += $legacyPath }
|
|
}
|
|
|
|
$projectSettingsGuid = Join-Path $scriptDir 'Agent\Borealis\Settings\Agent_GUID.txt'
|
|
if ($candidates -notcontains $projectSettingsGuid) { $candidates += $projectSettingsGuid }
|
|
$projectLegacyGuid = Join-Path $scriptDir 'Agent\Borealis\agent_GUID'
|
|
if ($candidates -notcontains $projectLegacyGuid) { $candidates += $projectLegacyGuid }
|
|
|
|
foreach ($path in ($candidates | Select-Object -Unique)) {
|
|
try {
|
|
if (Test-Path $path -PathType Leaf) {
|
|
$value = (Get-Content -Path $path -Raw -ErrorAction Stop)
|
|
if ($value) { return $value.Trim() }
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
function Get-AgentSettingsDirectory {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
if (-not $AgentRoot) { $AgentRoot = $scriptDir }
|
|
$settingsDir = Join-Path $AgentRoot 'Settings'
|
|
if ($settingsDir -and (Test-Path $settingsDir -PathType Container)) {
|
|
return $settingsDir
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function Get-ProtectedTokenString {
|
|
param(
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not $Path -or -not (Test-Path $Path -PathType Leaf)) {
|
|
return ''
|
|
}
|
|
|
|
try {
|
|
$protected = [System.IO.File]::ReadAllBytes($Path)
|
|
if (-not $protected -or $protected.Length -eq 0) { return '' }
|
|
} catch {
|
|
return ''
|
|
}
|
|
|
|
$scopes = @(
|
|
[System.Security.Cryptography.DataProtectionScope]::CurrentUser,
|
|
[System.Security.Cryptography.DataProtectionScope]::LocalMachine
|
|
)
|
|
|
|
foreach ($scope in $scopes) {
|
|
try {
|
|
$unprotected = [System.Security.Cryptography.ProtectedData]::Unprotect($protected, $null, $scope)
|
|
if ($unprotected -and $unprotected.Length -gt 0) {
|
|
return [System.Text.Encoding]::UTF8.GetString($unprotected)
|
|
}
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
function Invoke-AgentTokenRefresh {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ServerBaseUrl,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AgentGuid,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RefreshToken,
|
|
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentGuid) -or [string]::IsNullOrWhiteSpace($RefreshToken)) {
|
|
Write-UpdateLog "Invoke-AgentTokenRefresh called with missing parameters." 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
Write-UpdateLog ("Requesting access token refresh for agent {0}" -f $AgentGuid) 'STEP'
|
|
$base = $ServerBaseUrl.TrimEnd('/')
|
|
$uri = "$base/api/agent/token/refresh"
|
|
$payload = @{
|
|
guid = $AgentGuid
|
|
refresh_token = $RefreshToken
|
|
} | ConvertTo-Json
|
|
$headers = @{
|
|
'User-Agent' = 'borealis-agent-updater'
|
|
'Content-Type' = 'application/json'
|
|
}
|
|
|
|
$response = Invoke-AgentHttpRequest -Method 'POST' -Uri $uri -Headers $headers -Body $payload -ContentType 'application/json' -AgentRoot $AgentRoot -TimeoutSeconds 60
|
|
if (-not $response) {
|
|
Write-UpdateLog "Token refresh request produced no response." 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
try {
|
|
$json = $response.Content | ConvertFrom-Json
|
|
} catch {
|
|
Write-UpdateLog ("Token refresh response decode failed: {0}" -f $_.Exception.Message) 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
if ($json -and $json.access_token) {
|
|
$expiresIn = 900
|
|
try {
|
|
if ($json.expires_in) {
|
|
$expiresIn = [int]$json.expires_in
|
|
}
|
|
} catch {}
|
|
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
$expiresAt = $now + [Math]::Max(0, $expiresIn - 5)
|
|
return [pscustomobject]@{
|
|
AccessToken = ($json.access_token).Trim()
|
|
ExpiresAt = $expiresAt
|
|
}
|
|
}
|
|
|
|
Write-UpdateLog "Token refresh response did not include access token." 'WARN'
|
|
return $null
|
|
}
|
|
|
|
function Get-AgentAccessTokenContext {
|
|
param(
|
|
[string]$AgentRoot,
|
|
[string]$ServerBaseUrl,
|
|
[string]$AgentGuid
|
|
)
|
|
|
|
$settingsDir = Get-AgentSettingsDirectory -AgentRoot $AgentRoot
|
|
if (-not $settingsDir) { return $null }
|
|
|
|
Write-UpdateLog ("Loading agent access tokens from {0}" -f $settingsDir) 'DEBUG'
|
|
$accessPath = Join-Path $settingsDir 'access.jwt'
|
|
$metaPath = Join-Path $settingsDir 'access.meta.json'
|
|
$refreshPath = Join-Path $settingsDir 'refresh.token'
|
|
|
|
$accessToken = ''
|
|
$expiresAt = 0
|
|
|
|
if (Test-Path $accessPath -PathType Leaf) {
|
|
try {
|
|
$accessToken = (Get-Content -Path $accessPath -Raw -ErrorAction Stop).Trim()
|
|
} catch {
|
|
$accessToken = ''
|
|
}
|
|
}
|
|
|
|
if (Test-Path $metaPath -PathType Leaf) {
|
|
try {
|
|
$metaRaw = Get-Content -Path $metaPath -Raw -ErrorAction Stop
|
|
if ($metaRaw) {
|
|
$metaJson = $metaRaw | ConvertFrom-Json -ErrorAction Stop
|
|
if ($metaJson -and $metaJson.access_expires_at) {
|
|
$expiresAt = [int]$metaJson.access_expires_at
|
|
}
|
|
}
|
|
} catch {
|
|
$expiresAt = 0
|
|
}
|
|
}
|
|
|
|
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
if ($accessToken -and $expiresAt -gt ($now + 30)) {
|
|
$secondsLeft = $expiresAt - $now
|
|
Write-UpdateLog ("Using cached access token (expires in {0} seconds)." -f $secondsLeft) 'INFO'
|
|
return [pscustomobject]@{
|
|
AccessToken = $accessToken
|
|
ExpiresAt = $expiresAt
|
|
}
|
|
}
|
|
|
|
$refreshToken = Get-ProtectedTokenString -Path $refreshPath
|
|
if (-not $refreshToken) {
|
|
Write-UpdateLog "Refresh token unavailable; cannot authenticate with server." 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
Write-UpdateLog "Cached token expired or missing; requesting refreshed access token." 'WARN'
|
|
$refreshResult = Invoke-AgentTokenRefresh -ServerBaseUrl $ServerBaseUrl -AgentGuid $AgentGuid -RefreshToken $refreshToken -AgentRoot $AgentRoot
|
|
if ($refreshResult -and $refreshResult.AccessToken) {
|
|
Write-UpdateLog "Access token successfully refreshed." 'SUCCESS'
|
|
return $refreshResult
|
|
}
|
|
|
|
Write-UpdateLog "Failed to refresh access token." 'ERROR'
|
|
return $null
|
|
}
|
|
function Get-RepositoryCommitHash {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ProjectRoot,
|
|
|
|
[string]$AgentRoot,
|
|
|
|
[string]$GitExe
|
|
)
|
|
|
|
$candidates = @()
|
|
if ($ProjectRoot -and ($candidates -notcontains $ProjectRoot)) { $candidates += $ProjectRoot }
|
|
if ($AgentRoot -and ($candidates -notcontains $AgentRoot)) { $candidates += $AgentRoot }
|
|
if ($ProjectRoot) {
|
|
$agentRootCandidate = Join-Path $ProjectRoot 'Agent\Borealis'
|
|
if ((Test-Path $agentRootCandidate -PathType Container) -and ($candidates -notcontains $agentRootCandidate)) {
|
|
$candidates += $agentRootCandidate
|
|
}
|
|
}
|
|
|
|
if ($candidates.Count -gt 0) {
|
|
Write-UpdateLog ("Evaluating repository hash from candidate roots: {0}" -f ([string]::Join(', ', $candidates))) 'DEBUG'
|
|
}
|
|
|
|
if ($GitExe -and (Test-Path $GitExe -PathType Leaf)) {
|
|
foreach ($root in $candidates) {
|
|
try {
|
|
if (-not (Test-Path (Join-Path $root '.git') -PathType Container)) { continue }
|
|
$revParse = Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $root -Arguments @('rev-parse','HEAD')
|
|
if ($revParse) {
|
|
$candidate = ($revParse | Select-Object -Last 1)
|
|
if ($candidate) {
|
|
$result = $candidate.Trim()
|
|
if ($result) {
|
|
Write-UpdateLog ("Repository hash determined via git: {0}" -f $result) 'INFO'
|
|
return $result
|
|
}
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
foreach ($root in $candidates) {
|
|
try {
|
|
$gitDir = Join-Path $root '.git'
|
|
$fetchHead = Join-Path $gitDir 'FETCH_HEAD'
|
|
if (-not (Test-Path $fetchHead -PathType Leaf)) { continue }
|
|
foreach ($line in Get-Content -Path $fetchHead -ErrorAction Stop) {
|
|
$trim = ($line).Trim()
|
|
if (-not $trim -or $trim.StartsWith('#')) { continue }
|
|
$split = $trim.Split(@("`t", ' '), [StringSplitOptions]::RemoveEmptyEntries)
|
|
if ($split.Count -gt 0) {
|
|
$candidate = $split[0].Trim()
|
|
if ($candidate) { return $candidate }
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
foreach ($root in $candidates) {
|
|
try {
|
|
$gitDir = Join-Path $root '.git'
|
|
$headPath = Join-Path $gitDir 'HEAD'
|
|
if (-not (Test-Path $headPath -PathType Leaf)) { continue }
|
|
$head = (Get-Content -Path $headPath -Raw -ErrorAction Stop).Trim()
|
|
if (-not $head) { continue }
|
|
|
|
if ($head -match '^ref:\s*(.+)$') {
|
|
$ref = $Matches[1].Trim()
|
|
if ($ref) {
|
|
$refPath = $gitDir
|
|
foreach ($part in ($ref -split '/')) {
|
|
if ($part) { $refPath = Join-Path $refPath $part }
|
|
}
|
|
if (Test-Path $refPath -PathType Leaf) {
|
|
$commit = (Get-Content -Path $refPath -Raw -ErrorAction Stop).Trim()
|
|
if ($commit) { return $commit }
|
|
}
|
|
$packedRefs = Join-Path $gitDir 'packed-refs'
|
|
if (Test-Path $packedRefs -PathType Leaf) {
|
|
foreach ($line in Get-Content -Path $packedRefs -ErrorAction Stop) {
|
|
$trim = ($line).Trim()
|
|
if (-not $trim -or $trim.StartsWith('#') -or $trim.StartsWith('^')) { continue }
|
|
$parts = $trim.Split(' ', 2)
|
|
if ($parts.Count -ge 2 -and $parts[1].Trim() -eq $ref) {
|
|
$candidate = $parts[0].Trim()
|
|
if ($candidate) { return $candidate }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$detached = $head.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries)
|
|
if ($detached.Length -gt 0) {
|
|
$candidate = $detached[0].Trim()
|
|
if ($candidate) { return $candidate }
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if ($AgentRoot) {
|
|
$stored = Get-StoredAgentHash -AgentRoot $AgentRoot
|
|
if ($stored) {
|
|
Write-UpdateLog ("Using stored agent hash fallback: {0}" -f $stored) 'WARN'
|
|
return $stored
|
|
}
|
|
}
|
|
|
|
Write-UpdateLog "Unable to determine repository hash from any source." 'WARN'
|
|
return ''
|
|
}
|
|
|
|
function Get-StoredAgentHash {
|
|
param(
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
if (-not $AgentRoot) { return '' }
|
|
|
|
try {
|
|
$settingsDir = Join-Path $AgentRoot 'Settings'
|
|
$hashFile = Join-Path $settingsDir 'agent_hash.txt'
|
|
if (Test-Path $hashFile -PathType Leaf) {
|
|
$value = (Get-Content -Path $hashFile -Raw -ErrorAction Stop).Trim()
|
|
return $value
|
|
}
|
|
} catch {}
|
|
|
|
return ''
|
|
}
|
|
|
|
function Set-StoredAgentHash {
|
|
param(
|
|
[string]$AgentRoot,
|
|
[string]$AgentHash
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($AgentRoot) -or [string]::IsNullOrWhiteSpace($AgentHash)) { return }
|
|
|
|
try {
|
|
$settingsDir = Join-Path $AgentRoot 'Settings'
|
|
if (-not (Test-Path $settingsDir -PathType Container)) {
|
|
New-Item -ItemType Directory -Force -Path $settingsDir | Out-Null
|
|
}
|
|
$hashFile = Join-Path $settingsDir 'agent_hash.txt'
|
|
Set-Content -Path $hashFile -Value $AgentHash.Trim() -Encoding UTF8
|
|
Write-UpdateLog ("Stored agent hash to {0}" -f $hashFile) 'DEBUG'
|
|
} catch {}
|
|
}
|
|
|
|
function Set-GitFetchHeadHash {
|
|
param(
|
|
[string]$ProjectRoot,
|
|
[string]$CommitHash,
|
|
[string]$BranchName = 'main'
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ProjectRoot) -or [string]::IsNullOrWhiteSpace($CommitHash)) { return }
|
|
|
|
try {
|
|
$gitDir = Join-Path $ProjectRoot '.git'
|
|
if (-not (Test-Path $gitDir -PathType Container)) {
|
|
New-Item -ItemType Directory -Force -Path $gitDir | Out-Null
|
|
}
|
|
$fetchHead = Join-Path $gitDir 'FETCH_HEAD'
|
|
$branchSegment = if ([string]::IsNullOrWhiteSpace($BranchName)) { '' } else { "`tbranch '$BranchName'" }
|
|
$content = "{0}{1}" -f ($CommitHash.Trim()), $branchSegment
|
|
Set-Content -Path $fetchHead -Value $content -Encoding UTF8
|
|
Write-UpdateLog ("Wrote FETCH_HEAD in {0} to {1}" -f $gitDir, $CommitHash) 'DEBUG'
|
|
} catch {}
|
|
}
|
|
|
|
function Get-ServerCurrentRepoHash {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ServerBaseUrl,
|
|
[string]$AuthToken,
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { return $null }
|
|
|
|
$base = $ServerBaseUrl.TrimEnd('/')
|
|
$uri = "$base/api/repo/current_hash"
|
|
$headers = @{ 'User-Agent' = 'borealis-agent-updater' }
|
|
if ($AuthToken -and $AuthToken.Trim()) {
|
|
$headers['Authorization'] = "Bearer $AuthToken"
|
|
}
|
|
|
|
$response = Invoke-AgentHttpRequest -Method 'GET' -Uri $uri -Headers $headers -AgentRoot $AgentRoot -TimeoutSeconds 40
|
|
if ($response -and $response.StatusCode -ge 200 -and $response.StatusCode -lt 300) {
|
|
try {
|
|
$json = $response.Content | ConvertFrom-Json
|
|
Write-UpdateLog ("Received repo hash payload from server (branch={0}, sha={1})." -f $json.branch, $json.sha) 'SUCCESS'
|
|
return $json
|
|
} catch {
|
|
Write-Verbose ("Unable to decode repo hash response: {0}" -f $_.Exception.Message)
|
|
Write-UpdateLog ("Failed to decode repo hash response JSON: {0}" -f $_.Exception.Message) 'ERROR'
|
|
return $null
|
|
}
|
|
}
|
|
|
|
if ($response) {
|
|
Write-Verbose ("Repo hash request returned HTTP {0}: {1}" -f $response.StatusCode, $response.Content)
|
|
Write-UpdateLog ("Repo hash request returned HTTP {0}: {1}" -f $response.StatusCode, $response.Content) 'WARN'
|
|
} else {
|
|
Write-Verbose ("Repo hash request to {0} returned no response." -f $uri)
|
|
Write-UpdateLog ("Repo hash request to {0} returned no response." -f $uri) 'ERROR'
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Submit-AgentHash {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ServerBaseUrl,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AgentId,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AgentHash,
|
|
|
|
[string]$AgentGuid,
|
|
|
|
[string]$AuthToken,
|
|
|
|
[string]$AgentRoot
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentHash)) {
|
|
return
|
|
}
|
|
|
|
$base = $ServerBaseUrl.TrimEnd('/')
|
|
$uri = "$base/api/agent/hash"
|
|
$payloadBody = @{ agent_hash = $AgentHash }
|
|
if (-not [string]::IsNullOrWhiteSpace($AgentId)) { $payloadBody.agent_id = $AgentId }
|
|
if (-not [string]::IsNullOrWhiteSpace($AgentGuid)) { $payloadBody.agent_guid = $AgentGuid }
|
|
$payload = $payloadBody | ConvertTo-Json -Depth 3
|
|
$headers = @{ 'User-Agent' = 'borealis-agent-updater' }
|
|
if ($AuthToken -and $AuthToken.Trim()) {
|
|
$headers['Authorization'] = "Bearer $AuthToken"
|
|
}
|
|
|
|
$response = Invoke-AgentHttpRequest -Method 'POST' -Uri $uri -Headers $headers -Body $payload -ContentType 'application/json' -AgentRoot $AgentRoot -TimeoutSeconds 60
|
|
if (-not $response) {
|
|
Write-Verbose "Submit-AgentHash request returned no response."
|
|
Write-UpdateLog "Agent hash submission produced no response from server." 'ERROR'
|
|
return $null
|
|
}
|
|
|
|
Write-UpdateLog ("Agent hash submission HTTP status: {0}" -f $response.StatusCode) 'DEBUG'
|
|
try {
|
|
$json = $response.Content | ConvertFrom-Json
|
|
return $json
|
|
} catch {
|
|
Write-Verbose ("Submit-AgentHash response decode failed: {0}" -f $_.Exception.Message)
|
|
Write-UpdateLog ("Failed to parse agent hash submission response: {0}" -f $_.Exception.Message) 'ERROR'
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Sync-AgentHashRecord {
|
|
param(
|
|
[string]$ProjectRoot,
|
|
[string]$AgentRoot,
|
|
[string]$AgentHash,
|
|
[string]$ServerBaseUrl,
|
|
[string]$AgentId,
|
|
[string]$AgentGuid,
|
|
[string]$AuthToken = '',
|
|
[string]$BranchName = 'main'
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($AgentHash)) { return }
|
|
|
|
Write-UpdateLog ("Sync-AgentHashRecord invoked with hash {0}" -f $AgentHash) 'STEP'
|
|
if ($ProjectRoot) {
|
|
Set-GitFetchHeadHash -ProjectRoot $ProjectRoot -CommitHash $AgentHash -BranchName $BranchName
|
|
}
|
|
if ($AgentRoot) {
|
|
Set-StoredAgentHash -AgentRoot $AgentRoot -AgentHash $AgentHash
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { return }
|
|
|
|
Write-Host ("Submitting agent hash to server: {0}" -f $AgentHash)
|
|
Write-UpdateLog ("Submitting agent hash to {0} (AgentId={1}, AgentGuid={2})" -f $ServerBaseUrl, $AgentId, $AgentGuid) 'STEP'
|
|
|
|
if ([string]::IsNullOrWhiteSpace($AgentId) -and [string]::IsNullOrWhiteSpace($AgentGuid)) {
|
|
Write-Host "Agent identifier unavailable; skipping agent hash submission." -ForegroundColor DarkYellow
|
|
Write-UpdateLog "Agent identifier unavailable; cannot submit hash to server." 'WARN'
|
|
return
|
|
}
|
|
|
|
try {
|
|
$submitResult = Submit-AgentHash -ServerBaseUrl $ServerBaseUrl -AgentId $AgentId -AgentHash $AgentHash -AgentGuid $AgentGuid -AuthToken $AuthToken -AgentRoot $AgentRoot
|
|
if ($submitResult -and ($submitResult.status -eq 'ok')) {
|
|
Write-Host "The server-side agent hash database record was updated successfully."
|
|
Write-UpdateLog "Server acknowledged agent hash update." 'SUCCESS'
|
|
} elseif ($submitResult -and ($submitResult.status -eq 'ignored')) {
|
|
Write-Host "Server ignored the agent hash update (the agent is not enrolled with the server)." -ForegroundColor DarkYellow
|
|
Write-UpdateLog "Server returned 'ignored' for agent hash submission." 'WARN'
|
|
} elseif ($submitResult) {
|
|
Write-Host "Server agent_hash update response unrecognized. We don't know what to do here. (Panic)" -ForegroundColor DarkYellow
|
|
Write-UpdateLog ("Unexpected server response for agent hash submission: {0}" -f ($submitResult | ConvertTo-Json -Depth 5)) 'WARN'
|
|
}
|
|
} catch {
|
|
Write-Verbose ("Failed to Submit Agent Hash: {0}" -f $_.Exception.Message)
|
|
Write-UpdateLog ("Agent hash submission failed: {0}" -f $_.Exception.Message) 'ERROR'
|
|
}
|
|
}
|
|
|
|
function Invoke-BorealisUpdate {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$GitExe,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RepositoryUrl,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TargetHash,
|
|
|
|
[string]$BranchName = 'main',
|
|
|
|
[switch]$Silent
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($TargetHash)) {
|
|
throw 'Target commit hash is required for Borealis update.'
|
|
}
|
|
|
|
$preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR"
|
|
$preserveBackupPath = Join-Path $scriptDir "Update_Staging\Tesseract-OCR"
|
|
$ansibleEePath = Join-Path $scriptDir "Agent\Ansible_EE"
|
|
$ansibleEeBackupPath = Join-Path $scriptDir "Update_Staging\Ansible_EE"
|
|
|
|
Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" {
|
|
if (Test-Path $preservePath) {
|
|
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
|
if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
|
|
Move-Item -Path $preservePath -Destination $preserveBackupPath -Force
|
|
}
|
|
}
|
|
|
|
Run-Step "Updating: Preserve Ansible Execution Environment" {
|
|
if (Test-Path $ansibleEePath) {
|
|
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
|
if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
|
|
if (Test-Path $ansibleEeBackupPath) {
|
|
Remove-Item -Path $ansibleEeBackupPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
Move-Item -Path $ansibleEePath -Destination $ansibleEeBackupPath -Force
|
|
}
|
|
}
|
|
|
|
Run-Step "Updating: Clean Up Folders to Prepare for Update" {
|
|
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue `
|
|
(Join-Path $scriptDir "Data"), `
|
|
(Join-Path $scriptDir "Server\web-interface\src"), `
|
|
(Join-Path $scriptDir "Server\web-interface\build"), `
|
|
(Join-Path $scriptDir "Server\web-interface\public"), `
|
|
(Join-Path $scriptDir "Server\Borealis"), `
|
|
(Join-Path $scriptDir '.git')
|
|
}
|
|
|
|
$stagingPath = Join-Path $scriptDir "Update_Staging"
|
|
$cloneDir = Join-Path $stagingPath 'repo'
|
|
|
|
Run-Step "Updating: Create Update Staging Folder" {
|
|
if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
|
|
if (Test-Path $cloneDir) {
|
|
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $cloneDir
|
|
}
|
|
}
|
|
|
|
Run-Step "Updating: Clone Repository Source" {
|
|
$cloneArgs = @('clone','--no-tags')
|
|
if (-not [string]::IsNullOrWhiteSpace($BranchName)) {
|
|
$cloneArgs += @('--branch', $BranchName)
|
|
}
|
|
$cloneArgs += @($RepositoryUrl, $cloneDir)
|
|
Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $stagingPath -Arguments $cloneArgs | Out-Null
|
|
}
|
|
|
|
Run-Step "Updating: Checkout Target Revision" {
|
|
$normalizedHash = $TargetHash.Trim()
|
|
$haveHash = $false
|
|
try {
|
|
Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('rev-parse', $normalizedHash) | Out-Null
|
|
$haveHash = $true
|
|
} catch {
|
|
$haveHash = $false
|
|
}
|
|
|
|
if (-not $haveHash) {
|
|
Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('fetch','origin',$normalizedHash) | Out-Null
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($BranchName)) {
|
|
Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('checkout', $normalizedHash) | Out-Null
|
|
} else {
|
|
Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('checkout','-B',$BranchName,$normalizedHash) | Out-Null
|
|
}
|
|
}
|
|
|
|
Run-Step "Updating: Copy Update Files into Production Borealis Root Folder" {
|
|
Get-ChildItem -Path $cloneDir -Force | ForEach-Object {
|
|
$destination = Join-Path $scriptDir $_.Name
|
|
if ($_.PSIsContainer) {
|
|
Copy-Item -Path $_.FullName -Destination $destination -Recurse -Force
|
|
} else {
|
|
Copy-Item -Path $_.FullName -Destination $scriptDir -Force
|
|
}
|
|
}
|
|
}
|
|
|
|
Run-Step "Updating: Restore Tesseract-OCR Folder" {
|
|
$restorePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints"
|
|
if (Test-Path $preserveBackupPath) {
|
|
if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null }
|
|
Move-Item -Path $preserveBackupPath -Destination $restorePath -Force
|
|
}
|
|
}
|
|
|
|
Run-Step "Updating: Restore Ansible Execution Environment" {
|
|
$restorePath = Join-Path $scriptDir "Agent"
|
|
if (Test-Path $ansibleEeBackupPath) {
|
|
if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null }
|
|
Move-Item -Path $ansibleEeBackupPath -Destination $restorePath -Force
|
|
}
|
|
}
|
|
|
|
Run-Step "Updating: Clean Up Update Staging Folder" {
|
|
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $stagingPath
|
|
}
|
|
|
|
if (-not $Silent) {
|
|
Write-Host "Unattended Borealis update completed." -ForegroundColor Green
|
|
}
|
|
}
|
|
|
|
function Invoke-BorealisAgentUpdate {
|
|
Write-Host "==============================================="
|
|
Write-Host " Borealis - Automation Platform Updater Script "
|
|
Write-Host "==============================================="
|
|
Write-UpdateLog "Starting Borealis updater execution." 'STEP'
|
|
|
|
$agentRootCandidate = Join-Path $scriptDir 'Agent\Borealis'
|
|
$agentRoot = $scriptDir
|
|
if (Test-Path $agentRootCandidate -PathType Container) {
|
|
try {
|
|
$agentRoot = (Resolve-Path -Path $agentRootCandidate -ErrorAction Stop).Path
|
|
} catch {
|
|
$agentRoot = $agentRootCandidate
|
|
}
|
|
}
|
|
Write-UpdateLog ("Agent root resolved to {0}" -f $agentRoot) 'INFO'
|
|
|
|
$agentGuid = Get-AgentGuid -AgentRoot $agentRoot
|
|
if ($agentGuid) {
|
|
Write-Host ("Agent GUID: {0}" -f $agentGuid)
|
|
Write-UpdateLog ("Operating on agent GUID {0}" -f $agentGuid) 'INFO'
|
|
} else {
|
|
Write-Host "Warning: No agent GUID detected - Please deploy the agent, associating it with a Borealis server then try running the updater script again." -ForegroundColor Yellow
|
|
Write-Host "⚠️ Borealis update aborted."
|
|
Write-UpdateLog "Agent GUID missing; aborting update." 'ERROR'
|
|
return
|
|
}
|
|
|
|
$gitExe = Get-GitExecutablePath -ProjectRoot $scriptDir
|
|
$currentHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot -GitExe $gitExe
|
|
$serverBaseUrl = Get-BorealisServerUrl -AgentRoot $agentRoot
|
|
Initialize-BorealisTlsContext -AgentRoot $agentRoot -ServerBaseUrl $serverBaseUrl
|
|
$agentId = Get-AgentServiceId -AgentRoot $agentRoot
|
|
Write-UpdateLog ("Agent service ID detected: {0}" -f $agentId) 'DEBUG'
|
|
|
|
$authContext = Get-AgentAccessTokenContext -AgentRoot $agentRoot -ServerBaseUrl $serverBaseUrl -AgentGuid $agentGuid
|
|
if (-not $authContext -or -not $authContext.AccessToken) {
|
|
Write-Host "Unable to obtain agent authentication token. Ensure the agent is running and enrolled, then rerun the updater." -ForegroundColor Yellow
|
|
Write-Host "⚠️ Borealis update aborted."
|
|
Write-UpdateLog "Authentication context unavailable; aborting update." 'ERROR'
|
|
return
|
|
}
|
|
$authToken = $authContext.AccessToken
|
|
|
|
Write-UpdateLog "Querying Borealis server for current repository hash." 'STEP'
|
|
$serverRepoInfo = Get-ServerCurrentRepoHash -ServerBaseUrl $serverBaseUrl -AuthToken $authToken -AgentRoot $agentRoot
|
|
$serverHash = ''
|
|
$serverBranch = 'main'
|
|
if ($serverRepoInfo) {
|
|
try { $serverHash = (($serverRepoInfo.sha) -as [string]).Trim() } catch { $serverHash = '' }
|
|
try {
|
|
$branchCandidate = (($serverRepoInfo.branch) -as [string]).Trim()
|
|
if ($branchCandidate) { $serverBranch = $branchCandidate }
|
|
} catch { $serverBranch = 'main' }
|
|
}
|
|
|
|
$updateMode = $env:update_mode
|
|
if ($updateMode) { $updateMode = $updateMode.ToLowerInvariant() } else { $updateMode = 'update' }
|
|
$forceUpdate = $updateMode -eq 'force_update'
|
|
Write-UpdateLog ("Updater mode: {0} (force={1})" -f $updateMode, $forceUpdate) 'INFO'
|
|
|
|
if ($currentHash) {
|
|
Write-Host ("Local Agent Hash: {0}" -f $currentHash)
|
|
} else {
|
|
Write-Host "Local Agent Hash: unavailable"
|
|
}
|
|
|
|
if ($serverHash) {
|
|
Write-Host ("Borealis Server Hash: {0}" -f $serverHash)
|
|
} else {
|
|
Write-Host "Borealis Server Hash: unavailable"
|
|
}
|
|
|
|
$normalizedLocalHash = if ($currentHash) { $currentHash.Trim().ToLowerInvariant() } else { '' }
|
|
$normalizedServerHash = if ($serverHash) { $serverHash.Trim().ToLowerInvariant() } else { '' }
|
|
$hashesMatch = ($normalizedLocalHash -and $normalizedServerHash -and ($normalizedLocalHash -eq $normalizedServerHash))
|
|
$needsUpdate = $forceUpdate -or (-not $hashesMatch)
|
|
|
|
if ($forceUpdate) {
|
|
Write-Host "Force update requested; skipping hash comparison." -ForegroundColor Yellow
|
|
Write-UpdateLog "Force update requested; bypassing hash comparison." 'WARN'
|
|
} elseif (-not $serverHash) {
|
|
Write-Host "Borealis server hash unavailable; cannot continue." -ForegroundColor Yellow
|
|
Write-Host "⚠️ Borealis update aborted."
|
|
Write-UpdateLog "Server hash unavailable; aborting." 'ERROR'
|
|
return
|
|
} elseif (-not $needsUpdate) {
|
|
Write-Host "Local agent files already match the server repository hash." -ForegroundColor Green
|
|
Write-UpdateLog "Local agent hash matches remote; ensuring server record is updated." 'SUCCESS'
|
|
Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $serverHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -AuthToken $authToken -BranchName $serverBranch
|
|
Write-Host "✅ Borealis - Automation Platform Already Up-to-Date"
|
|
return
|
|
} else {
|
|
Write-Host "Repository hash mismatch detected; update required."
|
|
Write-UpdateLog ("Repository hash mismatch detected (local={0}, remote={1})." -f $currentHash, $serverHash) 'WARN'
|
|
}
|
|
|
|
if (-not ($gitExe) -or -not (Test-Path $gitExe -PathType Leaf)) {
|
|
Write-Host "Bundled Git dependency not found. Run '.\Borealis.ps1 -Agent' to redeploy the agent dependencies and try again." -ForegroundColor Yellow
|
|
Write-Host "⚠️ Borealis update aborted."
|
|
Write-UpdateLog "Bundled Git dependency missing; aborting update." 'ERROR'
|
|
return
|
|
}
|
|
|
|
$mutex = $null
|
|
$gotMutex = $false
|
|
$managedTasks = @()
|
|
try {
|
|
$mutex = New-Object System.Threading.Mutex($false, 'Global\BorealisUpdate')
|
|
$gotMutex = $mutex.WaitOne(0)
|
|
if (-not $gotMutex) {
|
|
Write-Verbose 'Another update is already running (mutex held). Exiting quietly.'
|
|
Write-Host "⚠️ Borealis update already in progress on this device."
|
|
return
|
|
}
|
|
|
|
$staging = Join-Path $scriptDir 'Update_Staging'
|
|
|
|
$managedTasks = Stop-AgentScheduledTasks -TaskNames @('Borealis Agent','Borealis Agent (UserHelper)')
|
|
if ($managedTasks.Count -gt 0) {
|
|
Write-UpdateLog ("Managed tasks stopped: {0}" -f ($managedTasks -join ', ')) 'INFO'
|
|
} else {
|
|
Write-UpdateLog "No managed tasks were running when update started." 'DEBUG'
|
|
}
|
|
Run-Step "Updating: Terminate Running Python Processes" { Stop-AgentPythonProcesses }
|
|
|
|
$updateSucceeded = $false
|
|
try {
|
|
Write-UpdateLog ("Starting repository sync to commit {0} (branch={1})." -f $serverHash, $serverBranch) 'STEP'
|
|
Invoke-BorealisUpdate -GitExe $gitExe -RepositoryUrl $repositoryUrl -TargetHash $serverHash -BranchName $serverBranch -Silent
|
|
$updateSucceeded = $true
|
|
Write-UpdateLog "Repository sync completed successfully." 'SUCCESS'
|
|
} finally {
|
|
if ($managedTasks.Count -gt 0) {
|
|
Start-AgentScheduledTasks -TaskNames $managedTasks
|
|
Write-UpdateLog "Agent scheduled tasks restarted." 'INFO'
|
|
}
|
|
}
|
|
|
|
if (-not $updateSucceeded) {
|
|
Write-UpdateLog "Repository sync reported failure." 'ERROR'
|
|
throw 'Borealis update failed.'
|
|
}
|
|
|
|
$refreshedContext = Get-AgentAccessTokenContext -AgentRoot $agentRoot -ServerBaseUrl $serverBaseUrl -AgentGuid $agentGuid
|
|
if ($refreshedContext -and $refreshedContext.AccessToken) {
|
|
$authToken = $refreshedContext.AccessToken
|
|
}
|
|
$postUpdateInfo = Get-ServerCurrentRepoHash -ServerBaseUrl $serverBaseUrl -AuthToken $authToken -AgentRoot $agentRoot
|
|
if ($postUpdateInfo) {
|
|
try {
|
|
$refreshedSha = (($postUpdateInfo.sha) -as [string]).Trim()
|
|
if ($refreshedSha) { $serverHash = $refreshedSha }
|
|
} catch {}
|
|
try {
|
|
$branchCandidate = (($postUpdateInfo.branch) -as [string]).Trim()
|
|
if ($branchCandidate) { $serverBranch = $branchCandidate }
|
|
} catch {}
|
|
}
|
|
|
|
$newHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot -GitExe $gitExe
|
|
|
|
$normalizedNewHash = if ($newHash) { $newHash.Trim().ToLowerInvariant() } else { '' }
|
|
$normalizedServerHash = if ($serverHash) { $serverHash.Trim().ToLowerInvariant() } else { '' }
|
|
|
|
if ($normalizedServerHash -and (-not $normalizedNewHash -or $normalizedNewHash -ne $normalizedServerHash)) {
|
|
$newHash = $serverHash
|
|
$normalizedNewHash = $normalizedServerHash
|
|
} elseif (-not $newHash -and $serverHash) {
|
|
$newHash = $serverHash
|
|
}
|
|
|
|
if ($newHash) {
|
|
Write-UpdateLog ("Final agent hash determined: {0}" -f $newHash) 'INFO'
|
|
Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $newHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -AuthToken $authToken -BranchName $serverBranch
|
|
} else {
|
|
Write-Host "Unable to determine repository hash for submission; server hash not updated." -ForegroundColor DarkYellow
|
|
Write-UpdateLog "Unable to determine final agent hash; skipping submission." 'WARN'
|
|
}
|
|
|
|
Write-Host "✅ Borealis - Automation Platform Successfully Updated"
|
|
Write-UpdateLog "Update workflow completed successfully." 'SUCCESS'
|
|
} finally {
|
|
if ($mutex -and $gotMutex) { $mutex.ReleaseMutex() | Out-Null }
|
|
if ($mutex) { $mutex.Dispose() }
|
|
Write-UpdateLog "Released update mutex and cleaned up resources." 'DEBUG'
|
|
}
|
|
}
|
|
|
|
Invoke-BorealisAgentUpdate
|