# kiosk-launch.ps1 — per-user bootstrap + kiosk browser supervisor (Windows) # # This is the process Windows launches in AssignedAccess single-app kiosk # mode (the "Vestibule Kiosk" Start Menu shortcut points here). It runs as # the kiosk user, NOT as an administrator, and must never prompt. # # Responsibilities, in order: # 1. Read deployment config (C:\ProgramData\Vestibule\kiosk.env) # 2. Locate the browser (LibreWolf or Firefox, per kiosk.env) # 3. Ensure usher.exe is installed per-user (%LOCALAPPDATA%\Vestibule) # 4. Ensure the Native Messaging host is registered under HKCU # 5. Ensure the dedicated vestibule-profile exists # 6. Launch the browser --kiosk, wait, relaunch on exit (crash recovery) # # Everything is logged to %LOCALAPPDATA%\Vestibule\kiosk-launch.log so a # headless kiosk can be diagnosed after the fact. The log rotates at # 512 KiB so a crash-loop cannot fill the disk. # # Why per-user bootstrap instead of provisioning-time HKU writes: the # kiosk account's profile (and HKCU hive) does not exist until first # logon, and AssignedAccess kiosk sessions never run RunOnce entries. # Registering from inside the kiosk session is the only path that needs # no admin rights — it matches the project's no-elevation philosophy. # # Params: # -InstallRoot Vestibule install dir (default: C:\Program Files\Vestibule) # -MaxRestarts Browser relaunches before this launcher exits and lets # AssignedAccess restart it (default: 50) param( [string]$InstallRoot = "C:\Program Files\Vestibule", [int]$MaxRestarts = 50 ) $ErrorActionPreference = "Continue" # kiosk must never die on a soft error $hostName = "com.vestibule.usher" $extId = "vestibule@vestibule.kiosk" $profileName = "vestibule-profile" $logMaxBytes = 524288 # ─── Logging ────────────────────────────────────────────────────────── function Get-LogPath { $dir = Join-Path $env:LOCALAPPDATA "Vestibule" New-Item -ItemType Directory -Force -Path $dir | Out-Null return (Join-Path $dir "kiosk-launch.log") } function Log($msg) { $line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $msg Write-Host $line try { $path = Get-LogPath if ((Get-Item $path -ErrorAction SilentlyContinue).Length -gt $logMaxBytes) { Move-Item $path "$path.old" -Force } Add-Content -Path $path -Value $line -Encoding UTF8 } catch { } } # ─── Config ─────────────────────────────────────────────────────────── function Read-KioskEnv { # KEY=VALUE lines from C:\ProgramData\Vestibule\kiosk.env, written by # provision-kiosk.ps1. Operators may edit it to change the home URL # without re-running provisioning. $env_ = @{} $envFile = Join-Path $env:ProgramData "Vestibule\kiosk.env" if (Test-Path $envFile) { foreach ($line in Get-Content $envFile) { if ($line -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.*)\s*$') { $env_[$matches[1]] = $matches[2] } } } return $env_ } # ─── Browser discovery (LibreWolf or Firefox) ─────────────────────── $librewolfSearchPaths = @( "${env:ProgramFiles}\LibreWolf\librewolf.exe", "${env:ProgramFiles(x86)}\LibreWolf\librewolf.exe", "${env:LOCALAPPDATA}\Programs\LibreWolf\librewolf.exe" ) $firefoxSearchPaths = @( "${env:ProgramFiles}\Mozilla Firefox\firefox.exe", "${env:ProgramFiles}\Mozilla Firefox ESR\firefox.exe", "${env:ProgramFiles(x86)}\Mozilla Firefox\firefox.exe", "${env:ProgramFiles(x86)}\Mozilla Firefox ESR\firefox.exe", "${env:LOCALAPPDATA}\Mozilla Firefox\firefox.exe" ) function Find-InPaths($paths, $exeName) { $hit = $paths | Where-Object { Test-Path $_ } | Select-Object -First 1 if ($hit) { return $hit } # Registry App Paths step-down: per-machine first, then per-user. $regRoots = @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths", "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths") return $regRoots | ForEach-Object { (Get-ItemProperty (Join-Path $_ $exeName) -ErrorAction SilentlyContinue)."(default)" } | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 } function Find-LibreWolf { return Find-InPaths $librewolfSearchPaths "librewolf.exe" } function Find-Firefox { return Find-InPaths $firefoxSearchPaths "firefox.exe" } function Resolve-Browser([string]$preference) { # Returns @{ Kind = ...; Exe = ... } or $null. The preference comes # from kiosk.env (VESTIBULE_BROWSER); "auto" steps down LibreWolf -> # Firefox — same order as provision-kiosk.ps1. $lw = Find-LibreWolf $ff = Find-Firefox switch ($preference) { "firefox" { if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } } "librewolf" { if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } } default { if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } } } # Preference not satisfiable: step down to whatever exists. if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } return $null } # ─── Per-user bootstrap steps ───────────────────────────────────────── function Ensure-Usher { $src = Join-Path $InstallRoot "bin\usher.exe" $dir = Join-Path $env:LOCALAPPDATA "Vestibule" $dst = Join-Path $dir "usher.exe" if (-not (Test-Path $src)) { Log "usher source missing: $src — native messaging will not work" return $false } if (-not (Test-Path $dst)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null Copy-Item $src $dst -Force Log "installed usher per-user: $dst" } return $true } function Ensure-NativeHostRegistration([string]$usherPath) { $manifestPath = Join-Path $env:LOCALAPPDATA "Vestibule\$hostName.json" $dir = Split-Path -Parent $manifestPath if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } if (-not (Test-Path $manifestPath)) { $manifest = @{ name = $hostName description = "Vestibule native helper" path = $usherPath type = "stdio" allowed_extensions = @($extId) } $manifest | ConvertTo-Json -Depth 5 | Set-Content $manifestPath -Encoding UTF8 Log "wrote native host manifest: $manifestPath" } $regKey = "HKCU:\Software\Mozilla\NativeMessagingHosts\$hostName" if (-not (Test-Path $regKey)) { New-Item -Path $regKey -Force | Out-Null } $current = (Get-ItemProperty $regKey -ErrorAction SilentlyContinue)."(default)" if ($current -ne $manifestPath) { Set-ItemProperty -Path $regKey -Name "(default)" -Value $manifestPath Log "registered native host: $regKey -> $manifestPath" } } function Ensure-Profile([string]$browserExe) { # -CreateProfile is idempotent: an existing profile is left untouched. # It writes to profiles.ini in the kiosk user's own profile dir. & $browserExe -CreateProfile $profileName 2>$null | Out-Null Log "ensured profile: $profileName" } # ─── Main ───────────────────────────────────────────────────────────── Log "=== vestibule kiosk-launch starting (pid $PID) ===" $config = Read-KioskEnv $homeUrl = $config["VESTIBULE_HOME_URL"] if (-not $homeUrl) { $homeUrl = "about:blank" } $browserPref = $config["VESTIBULE_BROWSER"] if (-not $browserPref) { $browserPref = "auto" } $browser = Resolve-Browser $browserPref if (-not $browser) { Log "FATAL: no LibreWolf or Firefox found in any known location" Start-Sleep -Seconds 30 # let AssignedAccess's watchdog see us exit exit 3 } $browserExe = $browser.Exe Log "browser ($($browser.Kind)): $browserExe" if (Ensure-Usher) { Ensure-NativeHostRegistration (Join-Path $env:LOCALAPPDATA "Vestibule\usher.exe") } Ensure-Profile $browserExe Log "home url: $homeUrl" $restarts = 0 while ($true) { # Supervision loop: relaunch the browser until the restart budget is # spent, then hand the job to AssignedAccess's own watchdog. Log "launching $($browser.Kind) (kiosk mode, restart #$restarts)" try { $proc = Start-Process -FilePath $browserExe ` -ArgumentList @("--kiosk", "-P", $profileName, "-no-remote", $homeUrl) ` -PassThru -Wait Log "$($browser.Kind) exited with code $($proc.ExitCode)" } catch { Log "launch failed: $($_.Exception.Message)" } $restarts++ if ($restarts -gt $MaxRestarts) { Log "restart budget exhausted — exiting so AssignedAccess takes over" exit 0 } Start-Sleep -Seconds 5 }