Odin/scripts/restart-vili.ps1

287 lines
12 KiB
PowerShell

param(
[string] $RavenHost = "raven",
[string] $LocalViliRoot = "E:\Projects\Vili",
[string] $LocalCultLibRoot = "E:\Projects\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