display kiosk watchdog: relaunch Edge when it goes away

This commit is contained in:
2026-08-10 07:54:03 -04:00
parent 8a2a3ee216
commit e68f522132
2 changed files with 275 additions and 9 deletions

View File

@@ -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