diff options
Diffstat (limited to 'packaging/win')
| -rw-r--r-- | packaging/win/build-node-runtime.ps1 | 7 | ||||
| -rw-r--r-- | packaging/win/service-mode.ps1 | 37 | ||||
| -rw-r--r-- | packaging/win/service.ps1 | 27 | ||||
| -rw-r--r-- | packaging/win/smoke-node-runtime.ps1 | 107 |
4 files changed, 167 insertions, 11 deletions
diff --git a/packaging/win/build-node-runtime.ps1 b/packaging/win/build-node-runtime.ps1 index 28c4061..371311b 100644 --- a/packaging/win/build-node-runtime.ps1 +++ b/packaging/win/build-node-runtime.ps1 @@ -198,6 +198,13 @@ if ($LASTEXITCODE -ne 0 -or $help -notmatch "meshbay-node") { if ($LASTEXITCODE -ne 0) { throw "frozen meshbay-node --help exited $LASTEXITCODE`n$help" } if ($help -notmatch "meshbay-node") { throw "frozen --help output looks wrong:`n$help" } +# --help imports the parser and nothing else; start the daemon itself. +Step "smoke test: the frozen daemon starts and answers" +$initPy = Join-Path $Repo "packages\meshbay-node\src\meshbay_node\__init__.py" +$expect = [regex]::Match((Get-Content -LiteralPath $initPy -Raw), '__version__\s*=\s*"([^"]+)"').Groups[1].Value +if (-not $expect) { throw "cannot read __version__ from $initPy" } +& (Join-Path $WinDir "smoke-node-runtime.ps1") -Exe $exe -ExpectVersion $expect + $mb = (Get-ChildItem $OutDir -Recurse | Measure-Object Length -Sum).Sum / 1MB Write-Host "" Write-Host ("OK node-runtime ready at {0} ({1:N0} MB)" -f $OutDir, $mb) -ForegroundColor Green diff --git a/packaging/win/service-mode.ps1 b/packaging/win/service-mode.ps1 index 2cb61b4..21794da 100644 --- a/packaging/win/service-mode.ps1 +++ b/packaging/win/service-mode.ps1 @@ -24,31 +24,48 @@ #> [CmdletBinding()] param( - [ValidateSet("install", "remove")] + # install: the boot task + the firewall rules, then start the node. + # remove: the boot task only -- switching to "at sign-in" or "only while + # MeshBay is open" from the Node page. The firewall rules serve + # every mode; removing them here left a node that silently + # accepted no connections (found by switching modes on a real + # install). + # uninstall: the boot task and the firewall rules -- the uninstaller. + [ValidateSet("install", "remove", "uninstall")] [string]$Action = "install" ) $here = $PSScriptRoot $log = Join-Path $env:TEMP "meshbay-firewall.log" -$firewallAction = if ($Action -eq "install") { "add" } else { "remove" } $failed = $false "[{0}] service-mode {1}" -f (Get-Date -Format s), $Action | Add-Content $log +# Elevated, so this reaches a node in any session. The desktop app has already +# asked it to stop properly; this only catches one that did not. Before +# install, a node still running would hold the control API's port and the +# service's own node would quit at once; before remove, deleting the task does +# not end its running instance, which would run on with nothing to stop it. +Get-Process -Name meshbay-node -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + +$serviceAction = if ($Action -eq "install") { "install" } else { "remove" } try { - & (Join-Path $here "service.ps1") $Action + & (Join-Path $here "service.ps1") $serviceAction } catch { - " service $Action failed: $_" | Add-Content $log + " service $serviceAction failed: $_" | Add-Content $log $failed = $true } -try { - & (Join-Path $here "firewall.ps1") $firewallAction -} -catch { - " firewall $firewallAction failed: $_" | Add-Content $log - $failed = $true +if ($Action -ne "remove") { + $firewallAction = if ($Action -eq "install") { "add" } else { "remove" } + try { + & (Join-Path $here "firewall.ps1") $firewallAction + } + catch { + " firewall $firewallAction failed: $_" | Add-Content $log + $failed = $true + } } # Register-ScheduledTask with -Trigger AtStartup does exactly that -- it does diff --git a/packaging/win/service.ps1 b/packaging/win/service.ps1 index 1f84a29..a46bb19 100644 --- a/packaging/win/service.ps1 +++ b/packaging/win/service.ps1 @@ -78,15 +78,28 @@ switch ($Action) { $taskAction = New-ScheduledTaskAction -Execute $node $bootTrigger = New-ScheduledTaskTrigger -AtStartup $taskPrincipal = New-ScheduledTaskPrincipal -UserId $user -LogonType S4U -RunLevel Limited + # Task Scheduler's defaults end a task after 72 hours, never start it on + # battery and stop it when the cable comes out. Kept identical to + # meshbay_node.platform.SERVICE_TASK_SETTINGS. + $taskSettings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) ` + -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries ` + -MultipleInstances IgnoreNew -StartWhenAvailable Register-ScheduledTask -TaskName $TASK_NAME -Action $taskAction -Trigger $bootTrigger ` - -Principal $taskPrincipal -Force -ErrorAction Stop | Out-Null + -Principal $taskPrincipal -Settings $taskSettings -Force -ErrorAction Stop | Out-Null Write-Host "service: installed ($user, runs at boot)" } "remove" { + # Deleting a task does not end its running instance. + & schtasks /end /tn $TASK_NAME 2>$null | Out-Null & schtasks /delete /tn $TASK_NAME /f 2>$null | Out-Null Write-Host "service: removed" } "status" { + # Exit 0: installed and current. 1: not installed. 2: installed but + # stale -- it runs another executable than this install's (an older + # install dir, a dev venv), or it still has the 72-hour / battery + # defaults. The installer re-registers a stale task; reading all of this + # needs no admin. $out = & schtasks /query /tn $TASK_NAME /fo list 2>$null if ($LASTEXITCODE -ne 0) { Write-Output "NOT_INSTALLED" @@ -94,6 +107,18 @@ switch ($Action) { } $line = $out | Select-String "^Status:" $state = if ($line) { ($line -replace "^Status:\s*", "").Trim() } else { "unknown" } + $task = Get-ScheduledTask -TaskName $TASK_NAME -ErrorAction SilentlyContinue + $stale = $true + if ($task) { + $exe = ([string]$task.Actions[0].Execute).Trim('"') + $s = $task.Settings + $stale = ($exe -ne $node) -or ($s.ExecutionTimeLimit -ne "PT0S") ` + -or $s.DisallowStartIfOnBatteries -or $s.StopIfGoingOnBatteries + } + if ($stale) { + Write-Output "INSTALLED_STALE:$state" + exit 2 + } Write-Output "INSTALLED:$state" exit 0 } diff --git a/packaging/win/smoke-node-runtime.ps1 b/packaging/win/smoke-node-runtime.ps1 new file mode 100644 index 0000000..bfcabac --- /dev/null +++ b/packaging/win/smoke-node-runtime.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + Start a frozen meshbay-node as a daemon and check it answers as the version + it claims to be. + +.DESCRIPTION + `meshbay-node --help` imports the parser and nothing else, so it passes for + a bundle that cannot start the daemon at all. This starts the real daemon, + in a throwaway profile (its own LOCALAPPDATA, an unused port, a hub URL + nothing listens on -- it never touches a real hub or the developer's own + node), waits for its control API, and checks: + - /api/status answers, with the expected version; + - the Windows log file was written (a service-mode daemon has no console, + and this file is the only place its errors go). + + Also useful against an installed build: + smoke-node-runtime.ps1 -Exe "$env:LOCALAPPDATA\Programs\MeshBay\resources\node-runtime\meshbay-node.exe" -ExpectVersion 0.16.0 + +.PARAMETER Exe + The meshbay-node.exe to start. + +.PARAMETER ExpectVersion + The version /api/status must report. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$Exe, + [Parameter(Mandatory = $true)][string]$ExpectVersion, + [int]$TimeoutSeconds = 45 +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$profileDir = Join-Path ([IO.Path]::GetTempPath()) ("meshbay-smoke-" + [guid]::NewGuid().ToString("N")) +$meshbay = Join-Path $profileDir "meshbay" +New-Item -ItemType Directory -Force $meshbay | Out-Null + +# A free loopback port, so a node already running here on 18000 is untouched. +$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0) +$listener.Start() +$port = $listener.LocalEndpoint.Port +$listener.Stop() + +$bytes = New-Object byte[] 32 +[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) +$unlock = Join-Path $meshbay "unlock.key" +Set-Content -Encoding ascii -LiteralPath $unlock ([Convert]::ToBase64String($bytes)) +$unlockToml = $unlock.Replace([char]92, [char]47) +Set-Content -Encoding ascii -LiteralPath (Join-Path $meshbay "node.toml") @" +[hub] +url = "http://127.0.0.1:1" +username = "smoke" + +[node] +quic_enabled = false +ui_port = $port + +[keystore] +unlock_file = "$unlockToml" +"@ + +$oldLocal = $env:LOCALAPPDATA +$env:LOCALAPPDATA = $profileDir +$stderr = Join-Path $profileDir "stderr.txt" +$proc = $null +try { + $proc = Start-Process -FilePath $Exe -PassThru -WindowStyle Hidden ` + -RedirectStandardError $stderr -RedirectStandardOutput (Join-Path $profileDir "stdout.txt") + $env:LOCALAPPDATA = $oldLocal + + $tokenFile = Join-Path $meshbay "data\ui-token" + $status = $null + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline -and -not $proc.HasExited) { + if (Test-Path -LiteralPath $tokenFile) { + try { + $token = (Get-Content -LiteralPath $tokenFile -Raw).Trim() + $status = Invoke-RestMethod "http://127.0.0.1:$port/api/status?t=$token" -TimeoutSec 3 + break + } catch { } + } + Start-Sleep -Milliseconds 500 + } + + $tail = if (Test-Path -LiteralPath $stderr) { (Get-Content -LiteralPath $stderr -Tail 30) -join "`n" } else { "" } + if ($proc.HasExited) { + throw "the frozen daemon exited (code $($proc.ExitCode)) before its control API answered:`n$tail" + } + if ($null -eq $status) { + throw "the frozen daemon's control API did not answer within ${TimeoutSeconds}s:`n$tail" + } + if ($status.version -ne $ExpectVersion) { + throw "the frozen daemon reports version '$($status.version)', expected '$ExpectVersion'" + } + $log = Join-Path $meshbay "state\node.log" + if (-not (Test-Path -LiteralPath $log) -or (Get-Item -LiteralPath $log).Length -eq 0) { + throw "the frozen daemon wrote no log file at $log" + } + Write-Host "OK daemon answered: version $($status.version), status $($status.status), log $log" -ForegroundColor Green +} +finally { + $env:LOCALAPPDATA = $oldLocal + if ($proc -and -not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } + if ($proc) { $proc.WaitForExit(10000) | Out-Null } + Remove-Item -LiteralPath $profileDir -Recurse -Force -ErrorAction SilentlyContinue +} |