aboutsummaryrefslogtreecommitdiffstats
path: root/packaging/win/service.ps1
blob: a46bb1964d0df3b5f38579fb94dab466a34e9238 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<#
.SYNOPSIS
    Install/remove/query/run/end the "MeshBay Node" boot-time Scheduled Task.

.DESCRIPTION
    A real Windows Service runs under LocalSystem/NetworkService before anyone
    signs in -- but those accounts have no normal user profile, and this app's
    entire design keeps node.toml, the keystore and all data under the signed-in
    user's own %LOCALAPPDATA%\meshbay\. Running as LocalSystem would not find
    any of it.

    The middle ground, and what this script sets up: a Scheduled Task that runs
    AS THIS USER at system boot, without needing them to sign in first -- an
    S4U (Service For User) logon: no password stored anywhere, and unlike
    LocalSystem it loads this account's own profile, so %LOCALAPPDATA%\meshbay\
    keeps working with zero changes. The cost: S4U carries no network
    credential (no reaching a domain share as this user), which the node
    never needed -- everything it touches is local disk plus outbound
    internet.

    Install uses Register-ScheduledTask with -LogonType S4U, not
    `schtasks /create`: schtasks only infers the logon type from whether /rp
    is present, and both readings were tried and broke on this exact machine's
    blank-password account (common on a personal PC) -- `/rp ""` fails
    credential validation ("the user name or password is incorrect", even
    though nothing is wrong), and omitting /rp registers "Interactive only"
    instead of S4U, which never runs at boot and doesn't launch anything even
    run on demand while signed in (both confirmed live, 2026-09-05).
    -LogonType S4U is explicit, no inference. remove/status/run/end have no
    such ambiguity and stay on schtasks.exe.

    Mirrors meshbay_node.platform.service_install/_remove/_status/_run/_end --
    same TASK_NAME, same flags -- so the CLI and the installer agree on what
    "installed" means. install/remove need admin (a boot trigger touches
    system-wide scheduler state); status/run/end do not, once the task exists,
    because Task Scheduler grants the owning user that much by default -- which
    is what lets the Node page's Start/Stop/Restart drive it with no further
    UAC prompts.

    Shipped as an extraResource at <install>\resources\service.ps1, so it
    locates meshbay-node.exe from its own path.

.PARAMETER Action
    install | remove | status | run | end
#>
[CmdletBinding()]
param(
    [ValidateSet("install", "remove", "status", "run", "end")]
    [string]$Action = "status"
)

# Not "Stop": schtasks writes its normal "task not found" outcome to stderr,
# and with ErrorActionPreference=Stop that promotes to a terminating error
# even through a 2>$null redirect (a native command's stderr is converted to
# an ErrorRecord before the redirect discards it). Every exit path below
# checks $LASTEXITCODE explicitly instead.
$TASK_NAME = "MeshBay Node"
$resources = $PSScriptRoot
$node      = Join-Path $resources "node-runtime\meshbay-node.exe"

function Get-CurrentUser {
    $domain = $env:USERDOMAIN
    if (-not $domain) { $domain = $env:COMPUTERNAME }
    return "$domain\$env:USERNAME"
}

switch ($Action) {
    "install" {
        if (-not (Test-Path $node)) { throw "meshbay-node.exe not found at $node" }
        $user = Get-CurrentUser
        # NOT $action: PowerShell variable names are case-insensitive, so $action
        # is this script's own [ValidateSet("install",...)][string]$Action
        # parameter. Assigning the New-ScheduledTaskAction CimInstance to it
        # runs the ValidateSet check (fails) and coerces the object to the
        # string "MSFT_TaskExecAction", which Register-ScheduledTask -Action
        # then rejects with "MSFT_TaskExecAction is not a valid value for the
        # Action variable" -- the install path never actually created the task.
        $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 -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"
            exit 1
        }
        $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
    }
    "run" {
        & schtasks /run /tn $TASK_NAME
        if ($LASTEXITCODE -ne 0) { throw "schtasks /run failed (exit $LASTEXITCODE)" }
    }
    "end" {
        & schtasks /end /tn $TASK_NAME
        if ($LASTEXITCODE -ne 0) { throw "schtasks /end failed (exit $LASTEXITCODE)" }
    }
}