Vestibule/scripts/kiosk-launch.ps1

226 lines
8.7 KiB
PowerShell

# 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
}