geenforce: relaunch the display kiosk when Edge goes away
Kiosks were updating, closing Edge, and never coming back - the display stayed dead until the next logon or reboot. The kiosk is launched by an all-users Startup shortcut, which runs ONCE at logon, and nothing supervised the browser afterwards. RelaunchNotification=2 was meant to cover the update case and does not: that policy drives Edge's own update-restart, which depends on session restore to return to where it was. Kiosk mode restores no session and has no UI to show the notification in, so Edge honours the close and never the relaunch. The same gap swallowed crashes and anyone closing the window. Adds a scope entry that registers a scheduled task in the INTERACTIVE session - SYSTEM cannot launch a visible browser, which is why the dispatcher writes a shortcut rather than calling Start-Process. The task relaunches from that same shortcut, so the target URL keeps one source of truth: retarget a subtype in DISPLAY_TYPE_TARGETS and the watchdog follows unchanged. Two details that matter. It matches on the COMMAND LINE, not the image name: Edge runs a crowd of msedge.exe children and only the parent carries --kiosk, so testing "is msedge running" would let a stray renderer mask a dead kiosk forever - verified against a real kiosk PC showing 7 processes and 1 match. And it avoids -RepetitionDuration [TimeSpan]::MaxValue, which serialises out of range and is rejected, exactly as the kiosk installer documents. A launch debounce stops a display that fails to start from spawning a browser every cycle, the log is size-bounded because this runs forever on a PC nobody watches, and it does nothing at all when no kiosk shortcut is present so it cannot put Edge on a PC that never asked for one. Verified on Windows: registers with the right principal and triggers, relaunches when the kiosk is gone, debounces an immediate re-run, and is idempotent across cycles (the staged script compare is trimmed - Set-Content adds a trailing newline the here-string lacks, so an untrimmed compare rewrote it every cycle).
This commit is contained in:
@@ -11,6 +11,7 @@ from plugins.geenforce.models import (
|
||||
)
|
||||
from plugins.geenforce.seed_display_scope import (
|
||||
seed_display_scope, build_display_manifest, build_dispatcher_script,
|
||||
build_watchdog_script,
|
||||
DISPLAY_TYPE_TARGETS, SCOPE_NAME, DISPATCHER_FILENAME, ALWAYSON_FILENAME,
|
||||
)
|
||||
|
||||
@@ -25,10 +26,11 @@ def test_seed_creates_display_scope(db):
|
||||
# and do NOT inherit common.
|
||||
assert scope.iscommon is False
|
||||
|
||||
# Four Registry drift-heal entries + two inline PS1 (dispatcher, always-on).
|
||||
assert summary['entrycount'] == 6
|
||||
# Four Registry drift-heal entries + three inline PS1 (dispatcher,
|
||||
# watchdog, always-on).
|
||||
assert summary['entrycount'] == 7
|
||||
assert summary['entrytypes'] == ['Registry', 'Registry', 'Registry',
|
||||
'Registry', 'PS1', 'PS1']
|
||||
'Registry', 'PS1', 'PS1', 'PS1']
|
||||
|
||||
|
||||
def test_registry_entries_use_valuematches_detection(db):
|
||||
@@ -112,14 +114,16 @@ def test_seed_draft_is_idempotent(db):
|
||||
assert first['entrycount'] == second['entrycount']
|
||||
assert first['dispatchersha256'] == second['dispatchersha256']
|
||||
assert first['alwaysonsha256'] == second['alwaysonsha256']
|
||||
assert first['watchdogsha256'] == second['watchdogsha256']
|
||||
|
||||
entries = ManifestEntry.query.filter_by(scopeid=second['scopeid']).all()
|
||||
assert len(entries) == 6
|
||||
# Exactly two inline payloads (dispatcher + always-on) exist after a rebuild,
|
||||
# not more - a payload-count invariant. (The underlying re-publish FK crash
|
||||
# only reproduces on MySQL, which enforces the manifestpayloads FK; it was
|
||||
# verified there directly. SQLite does not enforce it.)
|
||||
assert ManifestPayload.query.count() == 2
|
||||
assert len(entries) == 7
|
||||
# Exactly three inline payloads (dispatcher + watchdog + always-on) exist
|
||||
# after a rebuild, not more - a payload-count invariant. (The underlying
|
||||
# re-publish FK crash only reproduces on MySQL, which enforces the
|
||||
# manifestpayloads FK; it was verified there directly. SQLite does not
|
||||
# enforce it.)
|
||||
assert ManifestPayload.query.count() == 3
|
||||
|
||||
|
||||
def test_build_manifest_has_no_smb_exe_payloads(db):
|
||||
@@ -137,3 +141,42 @@ def test_dispatcher_script_is_ascii():
|
||||
# Plain ASCII only (no smart quotes / em-dashes) so the naming gate stays
|
||||
# green and the on-PC script parses cleanly.
|
||||
build_dispatcher_script().encode('ascii')
|
||||
|
||||
|
||||
def test_watchdog_entry_is_present_and_inline(db):
|
||||
"""The Startup shortcut runs once at logon, so an Edge update/crash leaves
|
||||
the display dead until the next logon. The watchdog is what recovers it."""
|
||||
manifest = build_display_manifest()
|
||||
entry = next(a for a in manifest['Applications']
|
||||
if a['Name'] == 'Display kiosk watchdog (relaunch Edge)')
|
||||
assert entry['Type'] == 'PS1'
|
||||
assert entry['PayloadSource'] == 'inline'
|
||||
assert entry['DetectionMethod'] == 'Always'
|
||||
|
||||
|
||||
def test_watchdog_script_registers_an_interactive_task():
|
||||
"""A SYSTEM task would start Edge in session 0 where nobody can see it, so
|
||||
the principal must be the interactive Users group."""
|
||||
script = build_watchdog_script()
|
||||
assert "New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545'" in script
|
||||
assert 'ShopDB Kiosk Watchdog' in script
|
||||
# The MaxValue repetition trap the kiosk installer documents. Checked
|
||||
# against CODE lines only - the script comments the trap on purpose, so a
|
||||
# naive substring test would fail on its own warning.
|
||||
code = [line for line in script.splitlines()
|
||||
if not line.lstrip().startswith('#')]
|
||||
assert not any('RepetitionDuration' in line for line in code)
|
||||
|
||||
|
||||
def test_watchdog_matches_on_the_command_line_not_the_image_name():
|
||||
"""Edge spawns many msedge.exe children; only the parent carries --kiosk.
|
||||
Testing 'is msedge running' would let a stray renderer mask a dead kiosk."""
|
||||
script = build_watchdog_script()
|
||||
assert "CommandLine -match '--kiosk'" in script
|
||||
|
||||
|
||||
def test_watchdog_does_nothing_without_a_kiosk_shortcut():
|
||||
"""It must never invent a browser launch on a PC that is not a kiosk."""
|
||||
script = build_watchdog_script()
|
||||
assert 'ShopDB Kiosk.lnk' in script
|
||||
assert 'shopdb-kiosk-lastlaunch' in script # debounce stamp
|
||||
|
||||
Reference in New Issue
Block a user