Odin/scripts/restart-vili.ps1
Meta Vens 515a864ef0 Rebase Idunn actuator scripts off the projects root
The previous commit fixed the target catalog and the operator alarm, but the
actuators the catalog invokes carried the same E: paths. Idunn would have
resolved every target and then failed on the first restart or deploy it ran,
for exactly the reason the alarm failed.

Rebased 41 references across 27 scripts:

- .cmd bodies invoke their sibling .ps1 through %~dp0 instead of an absolute
  drive path, and restart-weksa.cmd reaches its sibling repo the same way.
- .ps1 parameter defaults and assignments derive from $PSScriptRoot: one level
  up for paths inside Odin, two for sibling repos under the projects root.

Left deliberately alone: $RemoteViliRoot and $StateRoot in restart-vili.ps1.
Both are consumed inside the EncodedCommand block that restart-vili ssh's to
raven, so they describe raven's filesystem, not starfire's. Whether raven also
moved off E: is an operator question, not something to infer from here.

Verified: all 46 scripts under scripts/ parse clean, and the derivation
resolves to F:\Projects\Odin and F:\Projects on this host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 01:05:40 +02:00

287 lines
12 KiB
PowerShell

param(
[string] $RavenHost = "raven",
[string] $LocalViliRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Vili"),
[string] $LocalCultLibRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "CultLib"),
[string] $RemoteViliRoot = "E:\Projects\Vili",
[int] $Port = 8824,
[string] $HostName = "0.0.0.0",
[string] $StateRoot = "E:\Projects\Vili\.vili",
[string] $IdunnRudpHealth = $(if ($env:VILI_IDUNN_RUDP_HEALTH) { $env:VILI_IDUNN_RUDP_HEALTH } else { $env:IDUNN_RUDP_HEALTH }),
[string] $IdunnDaemon = "vili",
[string] $IdunnHealthContract = "vili.cultnet-rudp-animation-health",
[int] $IdunnHealthIntervalSeconds = 15
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "restart-vili.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by VILI_IDUNN_RUDP_HEALTH, IDUNN_RUDP_HEALTH, or -IdunnRudpHealth; no WireGuard endpoint default is allowed."
}
$localFiles = @(
(Join-Path $LocalViliRoot "package.json"),
(Join-Path $LocalViliRoot "package-lock.json"),
(Join-Path $LocalViliRoot "scripts\idunn-rudp.cjs"),
(Join-Path $LocalViliRoot "scripts\install-vili-task.ps1"),
(Join-Path $LocalViliRoot "scripts\restart-vili-daemon.cmd"),
(Join-Path $LocalViliRoot "scripts\restart-vili-daemon.ps1"),
(Join-Path $LocalViliRoot "scripts\run-hidden-powershell.vbs"),
(Join-Path $LocalViliRoot "scripts\run-vili-daemon.ps1"),
(Join-Path $LocalViliRoot "scripts\start-vili-daemon.ps1"),
(Join-Path $LocalViliRoot "scripts\vili-daemon.mjs"),
(Join-Path $LocalCultLibRoot "packages\cultnet-ts"),
(Join-Path $LocalCultLibRoot "packages\cultcache-ts"),
(Join-Path $LocalCultLibRoot "node_modules\ajv"),
(Join-Path $LocalCultLibRoot "node_modules\fast-deep-equal"),
(Join-Path $LocalCultLibRoot "node_modules\fast-uri"),
(Join-Path $LocalCultLibRoot "node_modules\json-schema-traverse"),
(Join-Path $LocalCultLibRoot "node_modules\require-from-string"),
(Join-Path $LocalViliRoot "node_modules\@msgpack")
)
foreach ($path in $localFiles) {
if (-not (Test-Path -LiteralPath $path)) {
throw "Required Vili runtime path not found: $path"
}
}
$syncId = [guid]::NewGuid().ToString("N")
$remoteSyncRootPs = "$RemoteViliRoot\.odin-vili-sync-$syncId"
$remoteSyncRootSftp = ($remoteSyncRootPs -replace "\\", "/")
$remoteTempPs = "C:\Windows\Temp\odin-raven-vili-restart-$syncId.ps1"
$remoteTempSftp = ($remoteTempPs -replace "\\", "/")
$remoteBundlePs = "C:\Windows\Temp\odin-raven-vili-bundle-$syncId.zip"
$remoteBundleSftp = ($remoteBundlePs -replace "\\", "/")
$localRemoteScript = Join-Path $env:TEMP "odin-raven-vili-restart-$syncId.ps1"
$localSftpBatch = Join-Path $env:TEMP "odin-raven-vili-restart-$syncId.sftp"
$localBundleRoot = Join-Path $env:TEMP "odin-raven-vili-bundle-$syncId"
$localBundleScripts = Join-Path $localBundleRoot "scripts"
$localBundleNodeModules = Join-Path $localBundleRoot "node_modules"
$localBundleZip = Join-Path $env:TEMP "odin-raven-vili-bundle-$syncId.zip"
$remoteScript = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
`$remoteRoot = "$RemoteViliRoot"
`$syncRoot = "$remoteSyncRootPs"
`$scriptsRoot = Join-Path `$remoteRoot "scripts"
`$stateRoot = "$StateRoot"
`$nodeModulesRoot = Join-Path `$remoteRoot "node_modules"
`$pidPath = Join-Path `$stateRoot "vili-daemon.pid"
try {
Expand-Archive -LiteralPath "$remoteBundlePs" -DestinationPath `$syncRoot -Force
if (-not (Test-Path -LiteralPath `$syncRoot)) {
throw "Uploaded sync root not found at `$syncRoot"
}
if (Test-Path -LiteralPath `$pidPath) {
`$existingPid = (Get-Content -LiteralPath `$pidPath -Raw -ErrorAction SilentlyContinue).Trim()
if (`$existingPid -match '^\d+$') {
`$process = Get-Process -Id ([int] `$existingPid) -ErrorAction SilentlyContinue
if (`$null -ne `$process) {
Stop-Process -Id `$process.Id -Force
Start-Sleep -Seconds 1
}
}
Remove-Item -LiteralPath `$pidPath -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path `$scriptsRoot | Out-Null
New-Item -ItemType Directory -Force -Path `$stateRoot | Out-Null
Copy-Item -LiteralPath (Join-Path `$syncRoot "package.json") -Destination (Join-Path `$remoteRoot "package.json") -Force
Copy-Item -LiteralPath (Join-Path `$syncRoot "package-lock.json") -Destination (Join-Path `$remoteRoot "package-lock.json") -Force
`$scriptFiles = @(
"idunn-rudp.cjs",
"install-vili-task.ps1",
"restart-vili-daemon.cmd",
"restart-vili-daemon.ps1",
"run-hidden-powershell.vbs",
"run-vili-daemon.ps1",
"start-vili-daemon.ps1",
"vili-daemon.mjs"
)
foreach (`$name in `$scriptFiles) {
Copy-Item -LiteralPath (Join-Path `$syncRoot "scripts\`$name") -Destination (Join-Path `$scriptsRoot `$name) -Force
}
if (Test-Path -LiteralPath `$nodeModulesRoot) {
Remove-Item -LiteralPath `$nodeModulesRoot -Recurse -Force
}
Move-Item -LiteralPath (Join-Path `$syncRoot "node_modules") -Destination `$nodeModulesRoot -Force
& powershell.exe @(
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", (Join-Path `$scriptsRoot "install-vili-task.ps1"),
"-TaskName", "GameCult\Vili",
"-Port", "$Port",
"-HostName", "$HostName",
"-StateRoot", "$StateRoot",
"-IdunnRudpHealth", "$IdunnRudpHealth",
"-IdunnDaemon", "$IdunnDaemon",
"-IdunnHealthContract", "$IdunnHealthContract",
"-IdunnHealthIntervalSeconds", "$IdunnHealthIntervalSeconds"
)
& powershell.exe @(
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-File", (Join-Path `$scriptsRoot "restart-vili-daemon.ps1"),
"-Port", "$Port",
"-HostName", "$HostName",
"-StateRoot", "$StateRoot",
"-IdunnRudpHealth", "$IdunnRudpHealth",
"-IdunnDaemon", "$IdunnDaemon",
"-IdunnHealthContract", "$IdunnHealthContract",
"-IdunnHealthIntervalSeconds", "$IdunnHealthIntervalSeconds"
)
`$task = Get-ScheduledTask -TaskName "Vili" -TaskPath "\GameCult\" -ErrorAction Stop
`$action = @(`$task.Actions)[0]
if (`$action.Execute -notmatch '(^|\\)wscript\.exe$') {
throw "GameCult\Vili action executes `$(`$action.Execute), expected wscript.exe"
}
if (`$action.Arguments -notlike "*run-hidden-powershell.vbs*") {
throw "GameCult\Vili action arguments do not reference run-hidden-powershell.vbs"
}
`$taskHealthChecks = @(
(`$action.Arguments -like "*-IdunnRudpHealth*"),
(`$action.Arguments -like "*$IdunnRudpHealth*"),
(`$action.Arguments -like "*-IdunnDaemon*"),
(`$action.Arguments -like "*$IdunnDaemon*"),
(`$action.Arguments -like "*-IdunnHealthContract*"),
(`$action.Arguments -like "*$IdunnHealthContract*")
)
if (`$taskHealthChecks -contains `$false) {
throw "GameCult\Vili action arguments do not carry the Idunn RUDP health contract."
}
`$process = Get-CimInstance Win32_Process |
Where-Object { `$_.CommandLine -like '*vili-daemon.mjs*' } |
Select-Object -First 1
if (`$null -eq `$process) {
throw "Vili process is not running after restart."
}
`$processHealthChecks = @(
(`$process.CommandLine -like "*--idunn-rudp-health*"),
(`$process.CommandLine -like "*$IdunnRudpHealth*"),
(`$process.CommandLine -like "*--idunn-daemon*"),
(`$process.CommandLine -like "*$IdunnDaemon*"),
(`$process.CommandLine -like "*--idunn-health-contract*"),
(`$process.CommandLine -like "*$IdunnHealthContract*")
)
if (`$processHealthChecks -contains `$false) {
throw "Vili process command line does not include the Idunn RUDP health arguments."
}
`$operatorStatePath = Join-Path `$stateRoot "operator-state.json"
`$deadline = (Get-Date).AddSeconds(30)
`$state = `$null
while ((Get-Date) -lt `$deadline) {
if (Test-Path -LiteralPath `$operatorStatePath) {
try {
`$state = Get-Content -LiteralPath `$operatorStatePath -Raw | ConvertFrom-Json
if (`$null -ne `$state.idunnRudpHealth -and `$state.idunnRudpHealth.status -eq "published") {
break
}
} catch {
}
}
Start-Sleep -Seconds 1
}
if (`$null -eq `$state -or `$null -eq `$state.idunnRudpHealth) {
throw "Vili daemon state file did not report idunnRudpHealth after restart."
}
if (`$state.idunnRudpHealth.status -ne "published") {
throw "Vili idunnRudpHealth status is `$(`$state.idunnRudpHealth.status): `$(`$state.idunnRudpHealth.error)"
}
Write-Host "Vili Raven runtime repaired and restarted with Idunn RUDP health."
Write-Host "ProcessId=`$(`$process.ProcessId)"
Write-Host "TaskArguments=`$(`$action.Arguments)"
Write-Host "HealthStatus=`$(`$state.idunnRudpHealth.status)"
Write-Host "HealthContract=`$(`$state.idunnRudpHealth.contract)"
} finally {
Remove-Item -LiteralPath `$syncRoot -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath "$remoteBundlePs" -Force -ErrorAction SilentlyContinue
}
"@
$batchLines = @(
"put ""$localBundleZip"" ""$remoteBundleSftp""",
"put ""$localRemoteScript"" ""$remoteTempSftp"""
)
try {
$moduleCopies = @(
@{ Source = (Join-Path $LocalCultLibRoot "packages\cultnet-ts"); Destination = (Join-Path $localBundleNodeModules "cultnet-ts") },
@{ Source = (Join-Path $LocalCultLibRoot "packages\cultcache-ts"); Destination = (Join-Path $localBundleNodeModules "cultcache-ts") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\ajv"); Destination = (Join-Path $localBundleNodeModules "ajv") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\fast-deep-equal"); Destination = (Join-Path $localBundleNodeModules "fast-deep-equal") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\fast-uri"); Destination = (Join-Path $localBundleNodeModules "fast-uri") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\json-schema-traverse"); Destination = (Join-Path $localBundleNodeModules "json-schema-traverse") },
@{ Source = (Join-Path $LocalCultLibRoot "node_modules\require-from-string"); Destination = (Join-Path $localBundleNodeModules "require-from-string") },
@{ Source = (Join-Path $LocalViliRoot "node_modules\@msgpack"); Destination = (Join-Path $localBundleNodeModules "@msgpack") }
)
New-Item -ItemType Directory -Force -Path $localBundleScripts, $localBundleNodeModules | Out-Null
Copy-Item -LiteralPath (Join-Path $LocalViliRoot "package.json") -Destination (Join-Path $localBundleRoot "package.json") -Force
Copy-Item -LiteralPath (Join-Path $LocalViliRoot "package-lock.json") -Destination (Join-Path $localBundleRoot "package-lock.json") -Force
$scriptCopies = @(
"idunn-rudp.cjs",
"install-vili-task.ps1",
"restart-vili-daemon.cmd",
"restart-vili-daemon.ps1",
"run-hidden-powershell.vbs",
"run-vili-daemon.ps1",
"start-vili-daemon.ps1",
"vili-daemon.mjs"
)
foreach ($scriptName in $scriptCopies) {
Copy-Item -LiteralPath (Join-Path $LocalViliRoot "scripts\$scriptName") -Destination (Join-Path $localBundleScripts $scriptName) -Force
}
foreach ($copy in $moduleCopies) {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $copy.Destination) | Out-Null
Copy-Item -LiteralPath $copy.Source -Destination $copy.Destination -Recurse -Force
}
if (Test-Path -LiteralPath $localBundleZip) {
Remove-Item -LiteralPath $localBundleZip -Force
}
Compress-Archive -Path (Join-Path $localBundleRoot "*") -DestinationPath $localBundleZip -CompressionLevel Optimal
Set-Content -LiteralPath $localRemoteScript -Encoding ASCII -Value $remoteScript
Set-Content -LiteralPath $localSftpBatch -Encoding ASCII -Value ($batchLines -join "`r`n")
& sftp.exe -b $localSftpBatch $RavenHost
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$remoteRunner = @"
`$ErrorActionPreference = "Stop"
try {
& "$remoteTempPs"
`$code = `$LASTEXITCODE
} finally {
Remove-Item -LiteralPath "$remoteTempPs" -Force -ErrorAction SilentlyContinue
}
exit `$code
"@
$encodedRunner = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteRunner))
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $RavenHost "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedRunner"
} finally {
Remove-Item -LiteralPath $localRemoteScript, $localSftpBatch -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $localBundleZip -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $localBundleRoot -Recurse -Force -ErrorAction SilentlyContinue
}
$restartExit = $LASTEXITCODE
exit $restartExit