226 lines
8.7 KiB
PowerShell
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
|
|
}
|