aboutsummaryrefslogtreecommitdiffstats
path: root/packaging
diff options
context:
space:
mode:
Diffstat (limited to 'packaging')
-rw-r--r--packaging/win/README.md83
-rw-r--r--packaging/win/build-node-runtime.ps17
-rw-r--r--packaging/win/service-mode.ps137
-rw-r--r--packaging/win/service.ps127
-rw-r--r--packaging/win/smoke-node-runtime.ps1107
5 files changed, 238 insertions, 23 deletions
diff --git a/packaging/win/README.md b/packaging/win/README.md
index 0598193..3c141d1 100644
--- a/packaging/win/README.md
+++ b/packaging/win/README.md
@@ -36,17 +36,47 @@ back out on uninstall. New shells only — a `WM_SETTINGCHANGE` broadcast nudges
open ones. It uses stock `WordFunc.nsh` (the `EnVar` plugin is not in
electron-builder's NSIS bundle).
-## Autostart: two modes, one choice at install time
+## When the node runs: three modes, chosen at install time
-**Per-user (default, no admin).** A `.vbs` in the Startup folder
-(`meshbay_node.platform._startup_vbs`), toggled from the Node page or
-`meshbay-node autostart install|remove`. Starts when *this user* signs in.
+Setup's radio page, and the Node page's *Start automatically* selector
+afterwards, offer the same three:
-**Service mode (one admin confirmation, at install time only).** A Scheduled
-Task, `meshbay_node.platform.service_install` / `packaging/win/service.ps1`,
-that starts **at boot, before anyone signs in**. A real Windows Service would
-run under LocalSystem/NetworkService — accounts with no normal user profile,
-so `%LOCALAPPDATA%\meshbay\` (config, keystore, data) would not exist for it.
+| Mode | What runs it | Stops when |
+|---|---|---|
+| **Only while MeshBay is open** | the desktop app, at launch (once the node has been set up) | the app quits — only a node the app started |
+| **At sign-in** (no admin) | a `.vbs` in the Startup folder (`meshbay_node.platform._startup_vbs`); `meshbay-node autostart install` / `remove` | sign-out (a hidden console of its own delivers CTRL_LOGOFF) |
+| **Background service** (one admin confirmation) | the boot-time Scheduled Task below | shutdown |
+
+The first mode used to do neither half: nothing started the node with the app
+(after a reboot a group stayed offline with MeshBay open) and nothing stopped it
+at quit. The two automatic ones are mutually exclusive — both would start the
+node twice — and `autostart install` refuses while the boot task exists.
+
+**Starting, stopping, restarting — one implementation.** The CLI's
+(`meshbay_node/cli/lifecycle.py`); the Node page, the tray, `node:start` and a
+terminal all go through it. A stop asks the node through its own control API
+first (`POST /api/shutdown`, loopback, per-run token): the only channel that
+reaches a node in any session with no elevation, and the one that runs its
+`_shutdown()` — WebRTC sessions closed, transcodes stopped. Then Task Scheduler,
+then `taskkill`. Before this, every stop of a Windows node was a
+TerminateProcess (nine in a row, not one shutdown logged), the CLI's own
+CTRL_BREAK reached every process on *its* console and killed itself, and the app
+reported a service node it could not reach as stopped. A start launches the node
+with nothing of the caller's inherited (a child of Electron held Electron's
+sockets after the app quit) and reports the version that answered.
+
+**Switching modes.** Leaving service mode stops the node first — deleting a
+task does not end its running instance, which ran on in session 0 with nothing
+able to stop it — removes the task, keeps the firewall rules (every mode needs
+them; removing them left a node that silently accepted no connections), and
+starts the node again in the new mode. Entering it stops the running node first,
+or the service's own finds the control API's port taken and quits.
+
+**Background service.** A Scheduled Task,
+`meshbay_node.platform.service_install` / `packaging/win/service.ps1`, that
+starts **at boot, before anyone signs in**. A real Windows Service would run
+under LocalSystem/NetworkService — accounts with no normal user profile, so
+`%LOCALAPPDATA%\meshbay\` (config, keystore, data) would not exist for it.
Relocating storage to make that work is real surgery, deliberately not this.
The alternative used instead: `schtasks /create ... /ru <user> /rp ""` with no
@@ -62,8 +92,37 @@ state — the same reason `/sc onlogon` needed it too, back when Task Scheduler
was tried for the per-user mode and abandoned for exactly that reason).
Querying, starting and stopping an *already-created* task does not — Task
Scheduler grants the owning user that much itself, which is what lets the Node
-page's Start/Stop/Restart drive it with no further UAC prompts
-(`src/main.js`'s `winServiceTaskStatus/Run/End`, mirroring `service.ps1`).
+page's Start/Stop/Restart drive it with no further UAC prompts.
+
+**The task's settings.** Registered with no execution time limit, allowed to
+start and keep running on battery, `MultipleInstances IgnoreNew` and
+`StartWhenAvailable`. Task Scheduler's defaults end a task after 72 hours,
+never start it on battery and stop it when the cable comes out — each one a
+node that was simply down. `service.ps1 status` reports a task that still has
+those defaults, or that runs another executable than this install's, as stale
+(exit 2), and setup registers it again (its one elevation).
+
+**Upgrading a running node.** A service node lives in the task's S4U session,
+so an unelevated `taskkill` from setup gets "Access is denied" — and
+electron-builder's `customInstall` only runs after the files are copied anyway.
+Left running, a node keeps `meshbay-node.exe` locked, the copy fails, and
+electron-builder's last-resort extract ignores that. So `customCheckAppRunning`,
+which electron-builder runs before `uninstallOldVersion` and before extraction,
+runs `build/stop-node.ps1` (embedded in the installer — the installed copy of
+anything may be what is being replaced): the control API first, then
+`schtasks /end`, then `taskkill`, until no `meshbay-node.exe` is left; if one
+will not stop, setup says so and quits rather than half-upgrade.
+An upgrade keeps the mode it finds (`customInit` reads the task, then the
+launcher, then a previous install), restores the sign-in launcher — the previous
+version's uninstaller deletes it — and starts the node again the way that mode
+runs it. A silent upgrade of an "at sign-in" install used to come out with no
+autostart at all and its node stopped.
+
+**Logs.** A daemon started by the task or the Startup launcher has no console,
+so it also logs to `%LOCALAPPDATA%\meshbay\state\node.log` (rotated at 5 MB,
+three kept). That file is where to look when the app says the node did not
+start. `meshbay-node service start` waits for the daemon's control API and
+reports the version that answered, or points at this file.
**One elevation, not two.** Choosing service mode needs admin for both the
Scheduled Task *and* the firewall rules; `service-mode.ps1` runs both from a
@@ -90,7 +149,7 @@ That runs [`build-win.ps1`](build-win.ps1):
| 2 | `npm ci` + download Electron's Chromium |
| 3 | bump Electron to the latest release (Chromium CVE policy; `-NoElectronBump` to skip) |
| 4 | `npm run sync-ui` — copy the interface from `meshbay-hub/.../static` |
-| 5 | [`build-node-runtime.ps1`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/` |
+| 5 | [`build-node-runtime.ps1`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/`, then [`smoke-node-runtime.ps1`](smoke-node-runtime.ps1) starts the frozen daemon in a throwaway profile and checks it answers with the right version and writes its log |
| 6 | `electron-builder --win nsis` → `packages/meshbay-client/dist/MeshBay-Setup-<version>.exe` |
### Video (ffmpeg)
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
+}