<# .SYNOPSIS Elevated helper: set up (or tear down) service mode in ONE UAC prompt, not two. .DESCRIPTION "Run as a background service" is two things -- the boot-time Scheduled Task and the firewall rules -- and needs one elevation, not one each. build/installer.nsh runs this single script via ExecShellWait "runas" for both the install-time choice and the uninstaller's cleanup, instead of elevating service.ps1 and firewall.ps1 separately. Each stays a script of its own rather than being folded together, so both remain independently callable and testable -- the CLI does, through meshbay-node service, and so does a later "just fix the firewall rules" retry that has nothing to do with the service task. Logs to the same file firewall.ps1 already uses, so both are visible in one place: %TEMP%\meshbay-firewall.log. .PARAMETER Action install service.ps1 install, then firewall.ps1 add, then service.ps1 run remove service.ps1 remove, then firewall.ps1 remove #> [CmdletBinding()] param( # 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" $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") $serviceAction } catch { " service $serviceAction 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 # not launch the task now. Every caller of this script (the installer's own # "background service" choice, and the Node page's later toggle) expects the # node to actually be running by the time the one UAC prompt they were shown # returns; without this, nothing is listening until the next reboot, with no # error and no indication that anything is still needed. `run` needs no # further elevation (Task Scheduler grants the owning user that much once the # task exists) -- doing it here, inside the same elevated pass, is only about # timing: the daemon comes up before this script's own exit code reaches the # caller, not because starting it needs the admin token this script is # holding. A no-op, harmlessly, if the task is already running (2026-09-14). if ($Action -eq "install" -and -not $failed) { try { & (Join-Path $here "service.ps1") "run" } catch { " service run failed: $_" | Add-Content $log $failed = $true } } if ($failed) { exit 1 } exit 0