displays: clear a pending Edge update without waiting for 02:00

Every kiosk was sitting on Edge's restart-to-update prompt. The scope
already sets RelaunchNotification=2 so Edge restarts unattended, but
RelaunchWindow defers that restart to 02:00-04:00, so during the day the
update waits and there is nobody on site to dismiss the prompt.

A one-shot entry runs the Edge updater and stops the browser. It does not
relaunch it - the enforce task is SYSTEM in session 0, where a launched
browser is invisible - so it leans on the watchdog that already relaunches
the kiosk from the Startup shortcut. That relaunch is what applies the
staged update.

One-shot is DetectionMethod=MarkerFile. The engine writes the marker only
after a 0 exit, so a failed run retries next cycle instead of being
recorded as done, and the script exits 0 when no Edge was running - that
is a success, and failing it would withhold the marker and re-kill Edge on
every cycle from then on. The marker path carries a date, which is the
re-arm mechanism for a future update.

Ordered after the watchdog entry: a display seeing both for the first time
must have its relauncher registered before anything stops the browser.
This commit is contained in:
cproudlock
2026-08-12 12:26:29 -04:00
parent 457349d258
commit 787f475208
2 changed files with 164 additions and 8 deletions

View File

@@ -58,6 +58,14 @@ DISPLAY_TYPE_TARGETS = {
DISPATCHER_FILENAME = 'Invoke-DisplayKioskDispatch.ps1'
# One-shot: apply a pending Edge update and bounce the kiosk browser.
# The marker path carries a DATE. That is the whole re-arm mechanism: change the
# date and every display runs it once more. Leave it alone and each display runs
# it exactly once, ever.
FORCE_EDGE_UPDATE_FILENAME = 'Invoke-EdgeForceUpdate.ps1'
FORCE_EDGE_UPDATE_MARKER = (
r'C:\ProgramData\ShopDB\markers\edge-force-update-2026-08-12.done')
def _relaunch_window_targets_comment():
"""Human note that lists the data-driven targets, for the manifest comment."""
@@ -576,6 +584,68 @@ Write-Host 'kiosk always-on enforced (power never-off + screensaver/lock disable
'''
def build_forceedgeupdate_script():
"""Return the inline one-shot Edge force-update PowerShell as text.
WHY THIS EXISTS: a pending Edge update parks a restart prompt on every
display and RelaunchWindow defers the forced restart to 02:00-04:00, so the
prompt can sit on screen all day with nobody on site to dismiss it.
This does not relaunch the browser itself. It cannot: the enforce task runs
as SYSTEM in session 0, where a launched browser is invisible (the same
constraint the dispatcher and the watchdog are built around). It stops Edge
and lets the EXISTING kiosk watchdog relaunch it from the Startup shortcut
within its interval, which is also what applies the staged update.
One-shot via DetectionMethod=MarkerFile: the engine writes the marker after
a 0 exit, so this runs once per display and is skipped on every later cycle.
A FAILED run writes no marker and is retried next cycle. To force another
round later, bump the date in FORCE_EDGE_UPDATE_MARKER.
"""
return r'''# Invoke-EdgeForceUpdate.ps1 -- one-shot: apply a pending Edge update now.
#
# Runs as SYSTEM under the enforce task. Stops Edge; the kiosk watchdog
# relaunches it (that relaunch is what completes a staged update).
$ErrorActionPreference = 'Continue'
$updater = Join-Path ${env:ProgramFiles(x86)} 'Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe'
if (Test-Path $updater) {
# /ua = update-all check; runs the same path Edge's own scheduled task uses,
# so a staged update is downloaded rather than waited for.
try {
Start-Process -FilePath $updater -ArgumentList '/ua','/installsource','scheduler' `
-NoNewWindow -Wait -ErrorAction Stop
Write-Host 'edge updater ran'
} catch {
Write-Host "edge updater failed: $_"
}
} else {
Write-Host "edge updater not found at $updater"
}
# Stop every Edge process, kiosk or not. The watchdog notices no --kiosk
# process and relaunches from the Startup shortcut, which is the ONE source of
# truth for the target URL.
$edge = Get-Process -Name msedge -ErrorAction SilentlyContinue
if ($edge) {
try {
$edge | Stop-Process -Force -ErrorAction Stop
Write-Host "stopped $($edge.Count) edge process(es); watchdog will relaunch the kiosk"
} catch {
# Do NOT exit non-zero here. A process that vanished between the Get and
# the Stop is a success, not a failure - failing would withhold the
# marker and re-kill Edge on every cycle from now on.
Write-Host "stop-process reported: $_"
}
} else {
Write-Host 'no edge process running; watchdog will relaunch the kiosk'
}
exit 0
'''
def _registry_drift_heal_entry(name, regname, regvalue, regtype, comment):
"""One Type=Registry entry that writes a value and detects drift via
ValueMatches against that same path/name.
@@ -670,6 +740,26 @@ def build_display_manifest():
'PayloadRef': WATCHDOG_FILENAME,
'DetectionMethod': 'Always',
},
{
'_comment': (
'ONE-SHOT. Applies a pending Edge update now instead of waiting '
'for the 02:00-04:00 RelaunchWindow, which can leave the update '
'prompt on screen all day with nobody on site to dismiss it. '
'Ordered AFTER the watchdog entry on purpose: this stops Edge '
'and relies on the watchdog to bring the kiosk back, so the '
'watchdog must be registered first on a display seeing both for '
'the first time. DetectionMethod=MarkerFile makes it run once '
'per display - the engine writes the marker only after a 0 exit, '
'so a failed run retries next cycle. Re-arm for a future update '
'by bumping the date in FORCE_EDGE_UPDATE_MARKER.'),
'Name': 'Force pending Edge update and bounce the kiosk (one-shot)',
'Type': 'PS1',
'Script': FORCE_EDGE_UPDATE_FILENAME,
'PayloadSource': 'inline',
'PayloadRef': FORCE_EDGE_UPDATE_FILENAME,
'DetectionMethod': 'MarkerFile',
'DetectionPath': FORCE_EDGE_UPDATE_MARKER,
},
{
'_comment': (
'Keep the display awake 24/7 so the kiosk page stays visible: '
@@ -733,6 +823,13 @@ def seed_display_scope(publish=False, notes='seed gea-shopfloor-display'):
watchdog, WATCHDOG_FILENAME,
'text/plain; charset=utf-8', watchdogbytes)
forceupdate = next(entry for entry in scope.entries
if entry.name.startswith('Force pending Edge update'))
forceupdatebytes = build_forceedgeupdate_script().encode('utf-8')
forceupdatepayload = service.store_inline_payload(
forceupdate, FORCE_EDGE_UPDATE_FILENAME,
'text/plain; charset=utf-8', forceupdatebytes)
publishedversion = None
if publish:
publishedversion = service.publish_scope(
@@ -747,5 +844,6 @@ def seed_display_scope(publish=False, notes='seed gea-shopfloor-display'):
'dispatchersha256': payload.payloadsha256,
'alwaysonsha256': alwaysonpayload.payloadsha256,
'watchdogsha256': watchdogpayload.payloadsha256,
'forceupdatesha256': forceupdatepayload.payloadsha256,
'publishedversion': publishedversion,
}