424 lines
22 KiB
PowerShell
424 lines
22 KiB
PowerShell
# Installs the AI node (:8123) and the remote tunnel (:8384) of a Windows
|
|
# AIHub box as Windows services. Run once per box from an elevated PowerShell:
|
|
#
|
|
# powershell -NoProfile -ExecutionPolicy Bypass -File aihub-node-install-services.ps1
|
|
#
|
|
# The stage directory (default: this script's directory) holds
|
|
# makepad-service-host.exe, makepad-ai-hub.exe (the node, built per
|
|
# tools/aihub-farm.md) and makepad-remote.exe (the TLS tunnel server).
|
|
#
|
|
# Accounts and rights (least privilege):
|
|
# - MakepadAiNode: the service host runs as LocalSystem only so it can start
|
|
# the node in the console user's session as that user (local-use
|
|
# admission watches that session); with nobody logged on the node runs as
|
|
# LocalSystem in session 0.
|
|
# - MakepadTunnel: runs as the virtual account NT SERVICE\MakepadTunnel, not
|
|
# an administrator. It may modify the tunnel's working directory (the
|
|
# checkout), the cargo cache, C:\ai\services\node (node updates) and
|
|
# C:\ai\services\tunnel (its own binary, keys and identity); it may start,
|
|
# stop and query MakepadAiNode and no other service. Those are the only
|
|
# privileged actions reachable through the tunnel (see libs/remote/TUNNEL.md).
|
|
#
|
|
# The tunnel keeps its TLS identity and keys from phase 1 (the per-user
|
|
# server in %USERPROFILE%\.makepad\tunnel, or -SeedDir), so clients' pins and
|
|
# keys stay valid. The new service is probed with a real TLS handshake and
|
|
# key check before the old watchdog task is removed; if the probe fails the
|
|
# old task is restored and started again, so the box stays reachable.
|
|
#
|
|
# Layout (C:\ai\services): service host and configs, admin-only; node\ the
|
|
# node binary; tunnel\ the tunnel binary, server-keys, identity\, audit.log
|
|
# and tunnel.log, readable only by the tunnel account and administrators;
|
|
# logs\ the node log.
|
|
param(
|
|
[string]$StageDir = $PSScriptRoot,
|
|
[string]$InstallDir = 'C:\ai\services',
|
|
# Default: taken from the node now serving -Port (its --cache-dir and
|
|
# its .cmd launcher), else the usual C:\ai layout.
|
|
[string]$CacheDir,
|
|
[string]$Launcher,
|
|
[string]$Repo = (Join-Path $env:USERPROFILE 'makepad'),
|
|
# The tunnel's working directory; default: the one its current watchdog
|
|
# task starts it in, else -Repo.
|
|
[string]$TunnelWorkDir,
|
|
# Phase-1 tunnel keys and identity to carry over.
|
|
[string]$SeedDir = (Join-Path $env:USERPROFILE '.makepad\tunnel'),
|
|
[string]$Fleet = 'gen',
|
|
[string]$RemoteAddress = 'LocalSubnet',
|
|
[int]$Port = 8123,
|
|
[int]$TunnelPort = 8384,
|
|
[ValidateSet('Both','Node','Tunnel')][string]$Only = 'Both',
|
|
[switch]$Force
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
throw 'run this from an elevated (Administrator) PowerShell'
|
|
}
|
|
$doNode = $Only -ne 'Tunnel'
|
|
$doTunnel = $Only -ne 'Node'
|
|
$NodeService = 'MakepadAiNode'
|
|
$TunnelService = 'MakepadTunnel'
|
|
$TunnelAccount = "NT SERVICE\$TunnelService"
|
|
$hostExe = Join-Path $InstallDir 'makepad-service-host.exe'
|
|
$nodeDir = Join-Path $InstallDir 'node'
|
|
$nodeExe = Join-Path $nodeDir 'makepad-ai-hub.exe'
|
|
$tunnelDir = Join-Path $InstallDir 'tunnel'
|
|
$tunnelExe = Join-Path $tunnelDir 'makepad-remote.exe'
|
|
$tunnelKeys = Join-Path $tunnelDir 'server-keys'
|
|
$tunnelIdentity = Join-Path $tunnelDir 'identity'
|
|
$logDir = Join-Path $InstallDir 'logs'
|
|
|
|
function Write-Step([string]$text) { Write-Host "== $text" }
|
|
|
|
# The running node's cache and launcher, found the way aihub-node-update.ps1
|
|
# finds them, so one command line fits every box layout.
|
|
$running = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($running) {
|
|
$runningNode = Get-CimInstance Win32_Process -Filter "ProcessId=$($running.OwningProcess)"
|
|
if (-not $CacheDir -and $runningNode.CommandLine -match '--cache-dir\s+("([^"]+)"|(\S+))') { $CacheDir = if ($Matches[2]) { $Matches[2] } else { $Matches[3] } }
|
|
$runningParent = Get-CimInstance Win32_Process -Filter "ProcessId=$($runningNode.ParentProcessId)"
|
|
if (-not $Launcher -and $runningParent -and $runningParent.Name -eq 'cmd.exe' -and $runningParent.CommandLine -match '(?i)/c\s+"?([^"\r\n]+\.cmd)"?\s*$') { $Launcher = $Matches[1] }
|
|
}
|
|
if (-not $CacheDir) { $CacheDir = 'C:\ai\makepad-ai-content-cache' }
|
|
if (-not $Launcher) { $Launcher = 'C:\ai\makepad-ai-content\run-node.cmd' }
|
|
Write-Step "node cache $CacheDir, launcher settings from $Launcher"
|
|
|
|
# The old tunnel watchdog tasks, and the working directory they use.
|
|
$oldTunnelTasks = @()
|
|
if ($doTunnel) {
|
|
$oldTunnelTasks = @(Get-ScheduledTask | Where-Object {
|
|
((@($_.Actions.Execute) + @($_.Actions.Arguments)) -join ' ') -match '(?i)makepad-remote|cargo-makepad\S*\s.*tunnel'
|
|
})
|
|
if (-not $TunnelWorkDir) {
|
|
foreach ($task in $oldTunnelTasks) {
|
|
if ((($task.Actions.Arguments) -join ' ') -match "-WorkingDirectory '([^']+)'") { $TunnelWorkDir = $Matches[1]; break }
|
|
}
|
|
}
|
|
if (-not $TunnelWorkDir) { $TunnelWorkDir = $Repo }
|
|
Write-Step "tunnel working directory $TunnelWorkDir"
|
|
}
|
|
|
|
$needed = @('makepad-service-host.exe')
|
|
if ($doNode) { $needed += 'makepad-ai-hub.exe' }
|
|
if ($doTunnel) { $needed += 'makepad-remote.exe' }
|
|
foreach ($file in $needed) {
|
|
if (-not (Test-Path (Join-Path $StageDir $file))) { throw "missing $file in $StageDir" }
|
|
}
|
|
if ($doNode -and -not (Test-Path (Join-Path $CacheDir 'node-key'))) {
|
|
throw "no node-key in $CacheDir; pass the node's existing -CacheDir so it keeps its identity"
|
|
}
|
|
if ($doTunnel -and -not (Test-Path (Join-Path $SeedDir 'server-keys')) -and -not (Test-Path $tunnelKeys)) {
|
|
throw "no tunnel keys: neither $SeedDir\server-keys nor $tunnelKeys exists (makepad-remote keygen <host> on the client, see libs/remote/TUNNEL.md)"
|
|
}
|
|
|
|
function Get-Listener([int]$port) {
|
|
$listen = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($listen) { Get-CimInstance Win32_Process -Filter "ProcessId=$($listen.OwningProcess)" }
|
|
}
|
|
|
|
function Stop-OurService([string]$name) {
|
|
$service = Get-Service $name -ErrorAction SilentlyContinue
|
|
if ($service -and $service.Status -ne 'Stopped') {
|
|
Stop-Service $name -Force
|
|
$service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(30))
|
|
}
|
|
}
|
|
|
|
# Stops whatever still serves a port outside our services (the old
|
|
# scheduled-task launchers), with the cmd.exe that started it.
|
|
function Stop-OldListener([int]$port) {
|
|
$process = Get-Listener $port
|
|
if (-not $process) { return }
|
|
$parent = Get-CimInstance Win32_Process -Filter "ProcessId=$($process.ParentProcessId)"
|
|
Write-Step "stopping old $($process.Name) (pid $($process.ProcessId)) on :$port"
|
|
Stop-Process -Id $process.ProcessId -Force
|
|
if ($parent -and $parent.Name -eq 'cmd.exe') { Stop-Process -Id $parent.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(15)
|
|
while ((Get-Listener $port) -and [DateTime]::UtcNow -lt $deadline) { Start-Sleep -Milliseconds 300 }
|
|
if (Get-Listener $port) { throw "port $port is still in use" }
|
|
}
|
|
|
|
# A running executable cannot be overwritten but can be renamed, so a binary
|
|
# still in use is moved aside first.
|
|
function Install-Binary([string]$name, [string]$dir) {
|
|
$source = Join-Path $StageDir $name
|
|
$target = Join-Path $dir $name
|
|
if ((Test-Path $target) -and (Get-FileHash $target).Hash -eq (Get-FileHash $source).Hash) { return }
|
|
try { Copy-Item $source $target -Force }
|
|
catch {
|
|
$aside = "$target.old"
|
|
Remove-Item $aside -Force -ErrorAction SilentlyContinue
|
|
if (Test-Path $aside) { $aside = "$target.old-$([DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))" }
|
|
Rename-Item $target (Split-Path $aside -Leaf)
|
|
Copy-Item $source $target -Force
|
|
}
|
|
}
|
|
|
|
# KEY=VALUE pairs from the old launcher's `set "KEY=VALUE"` lines, with
|
|
# %VAR% references to earlier lines expanded. PATH is rebuilt by the host.
|
|
function Read-LauncherEnv([string]$path) {
|
|
$vars = [ordered]@{}
|
|
if (-not (Test-Path $path)) { return $vars }
|
|
foreach ($line in Get-Content $path) {
|
|
if ($line -notmatch '^\s*set\s+"([^=]+)=(.*)"\s*$') { continue }
|
|
$key = $Matches[1]; $value = $Matches[2]
|
|
if ($key -ieq 'PATH') { continue }
|
|
foreach ($known in @($vars.Keys)) { $value = $value -replace [regex]::Escape("%$known%"), $vars[$known] }
|
|
$vars[$key] = $value
|
|
}
|
|
$vars
|
|
}
|
|
|
|
function Write-Config([string]$name, [string[]]$lines) {
|
|
$path = Join-Path $InstallDir "$name.cfg"
|
|
[IO.File]::WriteAllLines($path, $lines, (New-Object Text.UTF8Encoding $false))
|
|
$path
|
|
}
|
|
|
|
function Install-Service([string]$name, [string]$display, [string]$description, [string]$config, [string]$account) {
|
|
$binPath = "`"$hostExe`" `"$config`""
|
|
if (-not (Get-Service $name -ErrorAction SilentlyContinue)) {
|
|
New-Service -Name $name -BinaryPathName $binPath -DisplayName $display -StartupType Automatic | Out-Null
|
|
}
|
|
& sc.exe config $name binPath= $binPath start= auto obj= $account DisplayName= $display | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "sc.exe could not configure $name" }
|
|
# A virtual account is identified by its service SID.
|
|
& sc.exe sidtype $name unrestricted | Out-Null
|
|
& sc.exe description $name $description | Out-Null
|
|
# Restart the host if it dies itself; it restarts its program on its own.
|
|
& sc.exe failure $name reset= 86400 actions= restart/5000/restart/10000/restart/30000 | Out-Null
|
|
& sc.exe failureflag $name 1 | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "sc.exe could not configure $name" }
|
|
}
|
|
|
|
function Set-LanRule([string]$display, [int]$port) {
|
|
Get-NetFirewallRule -DisplayName $display -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
New-NetFirewallRule -DisplayName $display -Direction Inbound -Protocol TCP -LocalPort $port `
|
|
-RemoteAddress $RemoteAddress -Action Allow -Profile Any | Out-Null
|
|
}
|
|
|
|
# Earlier allow rules scoped to the node or tunnel executables, open to any
|
|
# address. The port rules above replace them.
|
|
function Remove-ProgramRules([string[]]$programPatterns, [string[]]$names) {
|
|
$rules = @(Get-NetFirewallApplicationFilter | Where-Object {
|
|
$program = $_.Program; @($programPatterns | Where-Object { $program -like $_ }).Count -gt 0
|
|
} | Get-NetFirewallRule)
|
|
foreach ($name in $names) { $rules += @(Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue) }
|
|
foreach ($rule in $rules | Where-Object { $_.Direction -eq 'Inbound' }) {
|
|
Write-Step "removing firewall rule '$($rule.DisplayName)'"
|
|
$rule | Remove-NetFirewallRule -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
function Wait-Port([int]$port, [string]$exe) {
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(90)
|
|
while ([DateTime]::UtcNow -lt $deadline) {
|
|
$process = Get-Listener $port
|
|
if ($process -and $process.ExecutablePath -eq $exe) { return $process }
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
$null
|
|
}
|
|
|
|
function Grant([string]$path, [string]$rights) {
|
|
if (-not (Test-Path $path)) { return }
|
|
Write-Step "granting $TunnelAccount $rights on $path"
|
|
& icacls.exe $path /grant "$($TunnelAccount):$rights" /Q | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "icacls failed on $path" }
|
|
}
|
|
|
|
# --- preflight: never interrupt node work -----------------------------------
|
|
# A busy fleet node has short gaps between jobs; wait up to 15 minutes for one.
|
|
$before = $null
|
|
if ($doNode -and (Get-Listener $Port)) {
|
|
$deadline = [DateTime]::UtcNow.AddMinutes(15)
|
|
while ($true) {
|
|
try {
|
|
$before = Invoke-RestMethod "http://127.0.0.1:$Port/health" -TimeoutSec 5
|
|
$jobs = Invoke-RestMethod "http://127.0.0.1:$Port/jobs" -TimeoutSec 5
|
|
} catch {
|
|
Write-Step "node on :$Port does not answer; replacing it"
|
|
break
|
|
}
|
|
if ((@($jobs.jobs).Count -eq 0 -and $before.jobs_pending -eq 0) -or $Force) { break }
|
|
if ([DateTime]::UtcNow -gt $deadline) { throw 'the node stayed busy for 15 minutes; rerun later (or with -Force)' }
|
|
Write-Host -NoNewline '.'
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
}
|
|
|
|
# --- install directories -----------------------------------------------------
|
|
Write-Step "install directory $InstallDir"
|
|
New-Item -ItemType Directory -Force $InstallDir, $logDir, $nodeDir | Out-Null
|
|
& icacls.exe $InstallDir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)RX' /Q | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw 'icacls failed on the install directory' }
|
|
Install-Binary 'makepad-service-host.exe' $InstallDir
|
|
|
|
# --- node ----------------------------------------------------------------------
|
|
$cudaPath = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Machine')
|
|
if ($doNode) {
|
|
$oldNodeTasks = @(Get-ScheduledTask | Where-Object {
|
|
((@($_.Actions.Execute) + @($_.Actions.Arguments)) -join ' ') -match '(?i)run-node|makepad-ai-content|makepad-asset-ai|makepad-ai-hub'
|
|
})
|
|
foreach ($task in $oldNodeTasks) {
|
|
Write-Step "removing scheduled task $($task.TaskPath)$($task.TaskName)"
|
|
Unregister-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath -Confirm:$false
|
|
}
|
|
Stop-OurService $NodeService
|
|
Stop-OldListener $Port
|
|
Install-Binary 'makepad-ai-hub.exe' $nodeDir
|
|
$nodeEnv = Read-LauncherEnv $Launcher
|
|
if ($nodeEnv.Contains('CUDA_PATH')) { $cudaPath = $nodeEnv['CUDA_PATH'] }
|
|
if (-not $cudaPath) { throw 'CUDA_PATH is not set; the node needs the CUDA runtime' }
|
|
$nodeEnv['CUDA_PATH'] = $cudaPath
|
|
$nodeEnv['MAKEPAD_ASSET_AI_PORT'] = "$Port"
|
|
$nodeEnv['MAKEPAD_ASSET_AI_FLEET'] = $Fleet
|
|
$lines = @(
|
|
"name=$NodeService", 'session=user', "cwd=$CacheDir",
|
|
"log=$(Join-Path $logDir 'node.log')", "path=$cudaPath\bin"
|
|
)
|
|
foreach ($key in $nodeEnv.Keys) { $lines += "env=$key=$($nodeEnv[$key])" }
|
|
$lines += "exe=$nodeExe"
|
|
foreach ($arg in @('--port', "$Port", '--host', '0.0.0.0', '--fleet', $Fleet, '--cache-dir', $CacheDir)) { $lines += "arg=$arg" }
|
|
$config = Write-Config $NodeService $lines
|
|
Install-Service $NodeService 'Makepad AI node' "Makepad AIHub node on port $Port (cache $CacheDir)" $config 'LocalSystem'
|
|
Remove-ProgramRules @('*\makepad-ai-content.exe', '*\makepad-asset-ai.exe', '*\makepad-ai-hub.exe', '*\makepad-app-ai-hub.exe') @("Makepad AI Content $Port")
|
|
Set-LanRule "Makepad AI node $Port (LAN)" $Port
|
|
}
|
|
|
|
# --- tunnel --------------------------------------------------------------------
|
|
$tunnelOk = $false
|
|
if ($doTunnel) {
|
|
New-Item -ItemType Directory -Force $tunnelDir, $TunnelWorkDir | Out-Null
|
|
# Seed keys and identity (never overwrite a newer service copy with an
|
|
# older seed: the service may have rotated its keys since).
|
|
if (-not (Test-Path $tunnelKeys)) {
|
|
Copy-Item (Join-Path $SeedDir 'server-keys') $tunnelKeys
|
|
if (Test-Path (Join-Path $SeedDir 'identity')) {
|
|
New-Item -ItemType Directory -Force $tunnelIdentity | Out-Null
|
|
Copy-Item (Join-Path $SeedDir 'identity\*') $tunnelIdentity -Force
|
|
}
|
|
}
|
|
Install-Binary 'makepad-remote.exe' $tunnelDir
|
|
$lines = @(
|
|
"name=$TunnelService", 'session=system', "cwd=$TunnelWorkDir",
|
|
"log=$(Join-Path $tunnelDir 'tunnel.log')",
|
|
'env=MAKEPAD_REMOTE_SUPERVISED=1'
|
|
)
|
|
# The checkout owner's Rust toolchain, so builds through the tunnel use it.
|
|
$profileDir = Split-Path $Repo
|
|
if (Test-Path (Join-Path $profileDir '.cargo\bin')) {
|
|
$lines += "path=$(Join-Path $profileDir '.cargo\bin')"
|
|
$lines += "env=CARGO_HOME=$(Join-Path $profileDir '.cargo')"
|
|
$lines += "env=RUSTUP_HOME=$(Join-Path $profileDir '.rustup')"
|
|
}
|
|
if ($cudaPath) { $lines += "path=$cudaPath\bin"; $lines += "env=CUDA_PATH=$cudaPath" }
|
|
$lines += "exe=$tunnelExe"
|
|
foreach ($arg in @('--server', '--all', '--port', "$TunnelPort", '--bind', 'lan', '--keys', $tunnelKeys, '--identity', $tunnelIdentity, '--audit-log', (Join-Path $tunnelDir 'audit.log'))) { $lines += "arg=$arg" }
|
|
$config = Write-Config $TunnelService $lines
|
|
Install-Service $TunnelService 'Makepad remote tunnel' "Makepad TLS tunnel on port $TunnelPort in $TunnelWorkDir (LAN only, keyed)" $config $TunnelAccount
|
|
|
|
# Rights of the tunnel account (only now does its SID resolve).
|
|
$sid = (New-Object Security.Principal.NTAccount $TunnelAccount).Translate([Security.Principal.SecurityIdentifier]).Value
|
|
& icacls.exe $tunnelDir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' "*$($sid):(OI)(CI)M" /Q | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw 'icacls failed on the tunnel directory' }
|
|
# Children inherit the directory's ACL (an explicit /T grant with (OI)(CI) leaves files with an empty DACL).
|
|
& icacls.exe (Join-Path $tunnelDir '*') /reset /T /Q | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw 'icacls failed on the tunnel files' }
|
|
& icacls.exe $InstallDir /grant "*$($sid):(OI)(CI)RX" /Q | Out-Null
|
|
Grant $nodeDir '(OI)(CI)M'
|
|
Write-Step 'granting the tunnel its working directory and cargo cache (can take a few minutes on a big checkout)'
|
|
Grant $TunnelWorkDir '(OI)(CI)M'
|
|
if ($TunnelWorkDir -ne $Repo) { Grant $Repo '(OI)(CI)M' }
|
|
Grant (Join-Path $profileDir '.cargo') '(OI)(CI)M'
|
|
Grant (Join-Path $profileDir '.rustup') '(OI)(CI)RX'
|
|
# Start, stop and query MakepadAiNode: the node part of the tunnel's
|
|
# admin allowlist. No other service is granted.
|
|
if (Get-Service $NodeService -ErrorAction SilentlyContinue) {
|
|
$sddl = ((& sc.exe sdshow $NodeService) -join '').Trim()
|
|
$ace = "(A;;CCLCSWRPWPLORC;;;$sid)"
|
|
if ($sddl -notlike "*;;;$sid)*") {
|
|
$new = if ($sddl -match '^(D:.*?)(S:.*)$') { $Matches[1] + $ace + $Matches[2] } else { $sddl + $ace }
|
|
& sc.exe sdset $NodeService $new | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { throw "could not grant the tunnel control of $NodeService" }
|
|
}
|
|
}
|
|
# Git as the tunnel account in a checkout owned by the user.
|
|
$git = Get-Command git.exe -ErrorAction SilentlyContinue
|
|
if ($git) {
|
|
$safe = & git.exe config --system --get-all safe.directory
|
|
foreach ($dir in @($Repo, $TunnelWorkDir) | Select-Object -Unique) {
|
|
if (@($safe) -notcontains $dir.Replace('\', '/')) { & git.exe config --system --add safe.directory $dir.Replace('\', '/') }
|
|
}
|
|
}
|
|
|
|
# Switch over: the old watchdog is disabled (not yet removed), the old
|
|
# server stopped, the service started and probed end to end.
|
|
foreach ($task in $oldTunnelTasks) {
|
|
Write-Step "disabling scheduled task $($task.TaskPath)$($task.TaskName)"
|
|
Disable-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath | Out-Null
|
|
}
|
|
Stop-OurService $TunnelService
|
|
Stop-OldListener $TunnelPort
|
|
Set-LanRule "Makepad tunnel $TunnelPort (LAN)" $TunnelPort
|
|
Start-Service $TunnelService
|
|
$process = Wait-Port $TunnelPort $tunnelExe
|
|
if ($process) {
|
|
$ip = (Get-NetIPConfiguration | Where-Object { $_.IPv4DefaultGateway } | Select-Object -First 1).IPv4Address.IPAddress
|
|
& $tunnelExe --probe "$($ip):$TunnelPort" --keys $tunnelKeys --identity $tunnelIdentity
|
|
$tunnelOk = $LASTEXITCODE -eq 0
|
|
}
|
|
if ($tunnelOk) {
|
|
foreach ($task in $oldTunnelTasks) {
|
|
Write-Step "removing scheduled task $($task.TaskPath)$($task.TaskName)"
|
|
Unregister-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath -Confirm:$false
|
|
}
|
|
Remove-ProgramRules @('*\makepad-remote.exe') @()
|
|
# The per-user phase-1 copy of the key is no longer needed.
|
|
if (Test-Path (Join-Path $SeedDir 'server-keys')) { Remove-Item (Join-Path $SeedDir 'server-keys') -Force }
|
|
} else {
|
|
Write-Step "the tunnel service did not pass its probe; see $tunnelDir\tunnel.log. Restoring the old tunnel."
|
|
Stop-OurService $TunnelService
|
|
& sc.exe config $TunnelService start= disabled | Out-Null
|
|
foreach ($task in $oldTunnelTasks) {
|
|
Enable-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath | Out-Null
|
|
Start-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- start and verify the node -------------------------------------------------
|
|
$result = [ordered]@{ host = $env:COMPUTERNAME; install_dir = $InstallDir }
|
|
if ($doNode) {
|
|
Start-Service $NodeService
|
|
$process = Wait-Port $Port $nodeExe
|
|
if (-not $process) { throw "$nodeExe is not listening on :$Port after 90 s; see $logDir" }
|
|
$health = $null
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(60)
|
|
while (-not $health -and [DateTime]::UtcNow -lt $deadline) {
|
|
try { $health = Invoke-RestMethod "http://127.0.0.1:$Port/health" -TimeoutSec 3 } catch { Start-Sleep -Seconds 1 }
|
|
}
|
|
if (-not $health) { throw "node does not answer /health; see $logDir\node.log" }
|
|
if ($before -and $before.node_key -and $health.node_key -ne $before.node_key) {
|
|
throw "node identity changed ($($before.node_key) -> $($health.node_key))"
|
|
}
|
|
$result.node = [ordered]@{ pid = $process.ProcessId; version = $health.version; node_key = $health.node_key; admission_open = $health.activity.admission_open; sha256 = (Get-FileHash $nodeExe -Algorithm SHA256).Hash }
|
|
# The node updater, run by the tunnel's account, authenticates to the
|
|
# node's /health with the node's own credential: it may read exactly
|
|
# that and the certificate fingerprint (not fleet.key, not the TLS key).
|
|
$tunnelSid = $null
|
|
try { $tunnelSid = (New-Object Security.Principal.NTAccount $TunnelAccount).Translate([Security.Principal.SecurityIdentifier]).Value } catch {}
|
|
if ($tunnelSid) {
|
|
foreach ($file in @((Join-Path $CacheDir 'fleet\node.credential'), (Join-Path $CacheDir 'fleet\tls\tls-fingerprint.txt'))) {
|
|
if (Test-Path $file) { & icacls.exe $file /grant "*$($tunnelSid):R" /Q | Out-Null }
|
|
}
|
|
& icacls.exe (Join-Path $CacheDir 'fleet') /grant "*$($tunnelSid):(X)" /Q | Out-Null
|
|
& icacls.exe (Join-Path $CacheDir 'fleet\tls') /grant "*$($tunnelSid):(X)" /Q | Out-Null
|
|
}
|
|
}
|
|
if ($doTunnel) {
|
|
$fp = (Get-Content (Join-Path $tunnelIdentity 'tls-fingerprint.txt') -ErrorAction SilentlyContinue | Select-Object -First 1)
|
|
$result.tunnel = [ordered]@{ ok = $tunnelOk; account = $TunnelAccount; cwd = $TunnelWorkDir; certificate_sha256 = $fp }
|
|
}
|
|
$result | ConvertTo-Json -Depth 4
|
|
if ($doTunnel -and -not $tunnelOk) { exit 1 }
|