lightman-agent 1.0.23 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/install-windows.ps1 +19 -9
- package/scripts/launch-multi-kiosk.ps1 +185 -0
- package/scripts/lightman-shell.bat +27 -10
- package/src/index.ts +129 -64
- package/src/services/multiScreenKiosk.ts +182 -58
package/package.json
CHANGED
|
@@ -529,15 +529,25 @@ Set-ItemProperty -Path $SP -Name "InactivityTimeoutSecs" -Value 0 -ErrorAction S
|
|
|
529
529
|
try { Disable-ScheduledTask -TaskName "\Microsoft\Windows\Shell\CreateObjectTask" -ErrorAction SilentlyContinue | Out-Null } catch { }
|
|
530
530
|
Write-Host " Lock screen fully disabled"
|
|
531
531
|
|
|
532
|
-
# --- 15. Sleep ---
|
|
533
|
-
Write-Host "[15/19] Disabling sleep..." -ForegroundColor Yellow
|
|
534
|
-
powercfg /change monitor-timeout-ac 0 2>&1 | Out-Null
|
|
535
|
-
powercfg /change standby-timeout-ac 0 2>&1 | Out-Null
|
|
536
|
-
powercfg /change hibernate-timeout-ac 0 2>&1 | Out-Null
|
|
537
|
-
|
|
538
|
-
# ---
|
|
539
|
-
Write-Host "[
|
|
540
|
-
|
|
532
|
+
# --- 15. Sleep ---
|
|
533
|
+
Write-Host "[15/19] Disabling sleep..." -ForegroundColor Yellow
|
|
534
|
+
powercfg /change monitor-timeout-ac 0 2>&1 | Out-Null
|
|
535
|
+
powercfg /change standby-timeout-ac 0 2>&1 | Out-Null
|
|
536
|
+
powercfg /change hibernate-timeout-ac 0 2>&1 | Out-Null
|
|
537
|
+
|
|
538
|
+
# --- 15b. Display topology ---
|
|
539
|
+
Write-Host "[15b/19] Enforcing display extend mode..." -ForegroundColor Yellow
|
|
540
|
+
if (Test-Path "$env:SystemRoot\System32\DisplaySwitch.exe") {
|
|
541
|
+
& "$env:SystemRoot\System32\DisplaySwitch.exe" /extend 2>&1 | Out-Null
|
|
542
|
+
Start-Sleep -Seconds 2
|
|
543
|
+
Write-Host " Display mode set to Extend"
|
|
544
|
+
} else {
|
|
545
|
+
Write-Host " DisplaySwitch.exe not found - skipped" -ForegroundColor DarkYellow
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
# --- 16. Harden ---
|
|
549
|
+
Write-Host "[16/19] Hardening Windows..." -ForegroundColor Yellow
|
|
550
|
+
$WU = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
|
|
541
551
|
if (-not (Test-Path $WU)) { New-Item -Path $WU -Force | Out-Null }
|
|
542
552
|
Set-ItemProperty -Path $WU -Name "NoAutoRebootWithLoggedOnUsers" -Value 1
|
|
543
553
|
Set-ItemProperty -Path $WU -Name "AUOptions" -Value 2
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# LIGHTMAN shell multi-screen launcher
|
|
2
|
+
# Runs in the interactive shell session and launches one browser window per display.
|
|
3
|
+
|
|
4
|
+
param(
|
|
5
|
+
[Parameter(Mandatory = $true)][string]$BrowserPath,
|
|
6
|
+
[Parameter(Mandatory = $true)][string]$MultiConfigPath,
|
|
7
|
+
[Parameter(Mandatory = $true)][string]$FallbackUrl,
|
|
8
|
+
[string]$LogFile = "C:\ProgramData\Lightman\logs\shell.log"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
$ErrorActionPreference = "Stop"
|
|
12
|
+
|
|
13
|
+
function Write-Log {
|
|
14
|
+
param([string]$Message)
|
|
15
|
+
try {
|
|
16
|
+
Add-Content -Path $LogFile -Value "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')] $Message"
|
|
17
|
+
} catch {
|
|
18
|
+
# Logging should never block kiosk startup.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Resolve-DisplayNumber {
|
|
23
|
+
param([string]$HardwareId)
|
|
24
|
+
if ([string]::IsNullOrWhiteSpace($HardwareId)) { return $null }
|
|
25
|
+
$trimmed = $HardwareId.Trim()
|
|
26
|
+
if ($trimmed -match '^\d+$') { return [int]$trimmed }
|
|
27
|
+
if ($trimmed -match 'DISPLAY(\d+)$') { return [int]$matches[1] }
|
|
28
|
+
return $null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Pick-Screen {
|
|
32
|
+
param(
|
|
33
|
+
[array]$Screens,
|
|
34
|
+
[string]$HardwareId,
|
|
35
|
+
[int]$ScreenIndex,
|
|
36
|
+
[System.Collections.Generic.HashSet[string]]$Used
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
$displayNo = Resolve-DisplayNumber -HardwareId $HardwareId
|
|
40
|
+
if ($displayNo) {
|
|
41
|
+
$suffix = "DISPLAY$displayNo"
|
|
42
|
+
foreach ($s in $Screens) {
|
|
43
|
+
if ($Used.Contains($s.DeviceName)) { continue }
|
|
44
|
+
if ($s.DeviceName.ToUpperInvariant().EndsWith($suffix)) { return $s }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (-not [string]::IsNullOrWhiteSpace($HardwareId)) {
|
|
49
|
+
foreach ($s in $Screens) {
|
|
50
|
+
if ($Used.Contains($s.DeviceName)) { continue }
|
|
51
|
+
if ($s.DeviceName.Equals($HardwareId, [System.StringComparison]::OrdinalIgnoreCase)) { return $s }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if ($ScreenIndex -ge 0 -and $ScreenIndex -lt $Screens.Count) {
|
|
56
|
+
$preferred = $Screens[$ScreenIndex]
|
|
57
|
+
if ($preferred -and -not $Used.Contains($preferred.DeviceName)) { return $preferred }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
foreach ($s in $Screens) {
|
|
61
|
+
if (-not $Used.Contains($s.DeviceName)) { return $s }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return $null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (-not (Test-Path $BrowserPath)) {
|
|
68
|
+
Write-Log "ERROR: Browser path not found: $BrowserPath"
|
|
69
|
+
exit 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
73
|
+
$screens = [System.Windows.Forms.Screen]::AllScreens | Sort-Object { $_.DeviceName }
|
|
74
|
+
Write-Log "Detected $($screens.Count) display(s) in shell session"
|
|
75
|
+
|
|
76
|
+
if (-not (Test-Path $MultiConfigPath)) {
|
|
77
|
+
Write-Log "Multi config not found. Launching fallback URL."
|
|
78
|
+
$fallback = Start-Process -FilePath $BrowserPath -ArgumentList @(
|
|
79
|
+
"--kiosk",
|
|
80
|
+
"--noerrdialogs",
|
|
81
|
+
"--disable-infobars",
|
|
82
|
+
"--disable-session-crashed-bubble",
|
|
83
|
+
"--no-first-run",
|
|
84
|
+
"--no-default-browser-check",
|
|
85
|
+
"--autoplay-policy=no-user-gesture-required",
|
|
86
|
+
"--disable-features=TranslateUI",
|
|
87
|
+
"--user-data-dir=C:\ProgramData\Lightman\chrome-kiosk",
|
|
88
|
+
$FallbackUrl
|
|
89
|
+
) -PassThru
|
|
90
|
+
if ($fallback) { Wait-Process -Id $fallback.Id -ErrorAction SilentlyContinue }
|
|
91
|
+
exit 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
$json = Get-Content -Raw $MultiConfigPath | ConvertFrom-Json
|
|
95
|
+
$entries = @($json.entries)
|
|
96
|
+
if ($entries.Count -le 1) {
|
|
97
|
+
Write-Log "Multi config has $($entries.Count) entry. Launching fallback URL."
|
|
98
|
+
$fallback = Start-Process -FilePath $BrowserPath -ArgumentList @(
|
|
99
|
+
"--kiosk",
|
|
100
|
+
"--noerrdialogs",
|
|
101
|
+
"--disable-infobars",
|
|
102
|
+
"--disable-session-crashed-bubble",
|
|
103
|
+
"--no-first-run",
|
|
104
|
+
"--no-default-browser-check",
|
|
105
|
+
"--autoplay-policy=no-user-gesture-required",
|
|
106
|
+
"--disable-features=TranslateUI",
|
|
107
|
+
"--user-data-dir=C:\ProgramData\Lightman\chrome-kiosk",
|
|
108
|
+
$FallbackUrl
|
|
109
|
+
) -PassThru
|
|
110
|
+
if ($fallback) { Wait-Process -Id $fallback.Id -ErrorAction SilentlyContinue }
|
|
111
|
+
exit 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
$usedScreens = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
|
115
|
+
$processes = @()
|
|
116
|
+
|
|
117
|
+
for ($i = 0; $i -lt $entries.Count; $i++) {
|
|
118
|
+
$entry = $entries[$i]
|
|
119
|
+
$entryIndex = 0
|
|
120
|
+
try { $entryIndex = [int]$entry.screenIndex } catch { $entryIndex = $i }
|
|
121
|
+
$entryHardwareId = [string]$entry.hardwareId
|
|
122
|
+
$entryUrl = [string]$entry.url
|
|
123
|
+
if ([string]::IsNullOrWhiteSpace($entryUrl)) { $entryUrl = $FallbackUrl }
|
|
124
|
+
|
|
125
|
+
$screen = Pick-Screen -Screens $screens -HardwareId $entryHardwareId -ScreenIndex $entryIndex -Used $usedScreens
|
|
126
|
+
if (-not $screen) {
|
|
127
|
+
Write-Log "WARN: No available screen for mapping index $i (hardwareId='$entryHardwareId')."
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
$null = $usedScreens.Add($screen.DeviceName)
|
|
132
|
+
|
|
133
|
+
$screenSlot = [Array]::IndexOf($screens, $screen)
|
|
134
|
+
if ($screenSlot -lt 0) { $screenSlot = $entryIndex }
|
|
135
|
+
$userDataDir = "C:\ProgramData\Lightman\chrome-kiosk-screen-$screenSlot"
|
|
136
|
+
New-Item -ItemType Directory -Path $userDataDir -Force | Out-Null
|
|
137
|
+
|
|
138
|
+
$x = [int]$screen.Bounds.X
|
|
139
|
+
$y = [int]$screen.Bounds.Y
|
|
140
|
+
$w = [int]$screen.Bounds.Width
|
|
141
|
+
$h = [int]$screen.Bounds.Height
|
|
142
|
+
|
|
143
|
+
Write-Log "Launching $($screen.DeviceName) at ${w}x${h}@${x},${y} -> $entryUrl"
|
|
144
|
+
|
|
145
|
+
$proc = Start-Process -FilePath $BrowserPath -ArgumentList @(
|
|
146
|
+
"--kiosk",
|
|
147
|
+
"--noerrdialogs",
|
|
148
|
+
"--disable-infobars",
|
|
149
|
+
"--disable-session-crashed-bubble",
|
|
150
|
+
"--no-first-run",
|
|
151
|
+
"--no-default-browser-check",
|
|
152
|
+
"--autoplay-policy=no-user-gesture-required",
|
|
153
|
+
"--disable-features=TranslateUI",
|
|
154
|
+
"--window-position=$x,$y",
|
|
155
|
+
"--window-size=$w,$h",
|
|
156
|
+
"--user-data-dir=$userDataDir",
|
|
157
|
+
$entryUrl
|
|
158
|
+
) -PassThru
|
|
159
|
+
|
|
160
|
+
if ($proc) { $processes += $proc }
|
|
161
|
+
Start-Sleep -Milliseconds 300
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ($processes.Count -eq 0) {
|
|
165
|
+
Write-Log "WARN: Multi launch created no browser process. Starting fallback."
|
|
166
|
+
$fallback = Start-Process -FilePath $BrowserPath -ArgumentList @(
|
|
167
|
+
"--kiosk",
|
|
168
|
+
"--noerrdialogs",
|
|
169
|
+
"--disable-infobars",
|
|
170
|
+
"--disable-session-crashed-bubble",
|
|
171
|
+
"--no-first-run",
|
|
172
|
+
"--no-default-browser-check",
|
|
173
|
+
"--autoplay-policy=no-user-gesture-required",
|
|
174
|
+
"--disable-features=TranslateUI",
|
|
175
|
+
"--user-data-dir=C:\ProgramData\Lightman\chrome-kiosk",
|
|
176
|
+
$FallbackUrl
|
|
177
|
+
) -PassThru
|
|
178
|
+
if ($fallback) { Wait-Process -Id $fallback.Id -ErrorAction SilentlyContinue }
|
|
179
|
+
exit 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
$pids = $processes | ForEach-Object { $_.Id }
|
|
183
|
+
Write-Log "Waiting on multi-screen browser processes: $($pids -join ', ')"
|
|
184
|
+
Wait-Process -Id $pids -ErrorAction SilentlyContinue
|
|
185
|
+
Write-Log "Multi-screen browser session ended"
|
|
@@ -13,11 +13,13 @@ REM 5. Launches Chrome fullscreen
|
|
|
13
13
|
REM 6. If Chrome crashes, relaunches in 3 seconds (infinite loop)
|
|
14
14
|
REM ================================================================
|
|
15
15
|
|
|
16
|
-
set INSTALL_DIR=C:\Program Files\Lightman\Agent
|
|
17
|
-
set CONFIG_FILE=%INSTALL_DIR%\agent.config.json
|
|
18
|
-
set URL_SIDECAR=C:\ProgramData\Lightman\kiosk-url.txt
|
|
19
|
-
set
|
|
20
|
-
set
|
|
16
|
+
set INSTALL_DIR=C:\Program Files\Lightman\Agent
|
|
17
|
+
set CONFIG_FILE=%INSTALL_DIR%\agent.config.json
|
|
18
|
+
set URL_SIDECAR=C:\ProgramData\Lightman\kiosk-url.txt
|
|
19
|
+
set MULTI_SIDECAR=C:\ProgramData\Lightman\kiosk-multi.json
|
|
20
|
+
set MULTI_LAUNCHER=%INSTALL_DIR%\scripts\launch-multi-kiosk.ps1
|
|
21
|
+
set CHROME_DATA=C:\ProgramData\Lightman\chrome-kiosk
|
|
22
|
+
set LOG_FILE=C:\ProgramData\Lightman\logs\shell.log
|
|
21
23
|
|
|
22
24
|
REM Ensure directories exist
|
|
23
25
|
if not exist "C:\ProgramData\Lightman\logs" mkdir "C:\ProgramData\Lightman\logs"
|
|
@@ -116,8 +118,14 @@ set MAX_WAIT=60
|
|
|
116
118
|
timeout /t 1 /nobreak >nul
|
|
117
119
|
goto wait_for_agent
|
|
118
120
|
|
|
119
|
-
:agent_ready
|
|
120
|
-
echo [%date% %time%] Agent ready >> "%LOG_FILE%"
|
|
121
|
+
:agent_ready
|
|
122
|
+
echo [%date% %time%] Agent ready >> "%LOG_FILE%"
|
|
123
|
+
|
|
124
|
+
REM Force Windows into Extend mode so all connected displays are usable.
|
|
125
|
+
if exist "%SystemRoot%\System32\DisplaySwitch.exe" (
|
|
126
|
+
"%SystemRoot%\System32\DisplaySwitch.exe" /extend >nul 2>&1
|
|
127
|
+
timeout /t 2 /nobreak >nul
|
|
128
|
+
)
|
|
121
129
|
|
|
122
130
|
REM ----------------------------------------------------------------
|
|
123
131
|
REM Infinite Chrome loop
|
|
@@ -139,9 +147,18 @@ REM ----------------------------------------------------------------
|
|
|
139
147
|
)
|
|
140
148
|
)
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
set MULTI_COUNT=0
|
|
151
|
+
if exist "%MULTI_SIDECAR%" (
|
|
152
|
+
for /f "delims=" %%c in ('node -e "try{const fs=require('fs');const p=String.raw`%MULTI_SIDECAR%`;const j=JSON.parse(fs.readFileSync(p,'utf8'));const n=Array.isArray(j&&j.entries)?j.entries.length:0;console.log(n)}catch(e){console.log(0)}" 2^>nul') do set MULTI_COUNT=%%c
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if %MULTI_COUNT% gtr 1 if exist "%MULTI_LAUNCHER%" (
|
|
156
|
+
echo [%date% %time%] Launching multi-screen shell session (%MULTI_COUNT% screens) >> "%LOG_FILE%"
|
|
157
|
+
powershell -ExecutionPolicy Bypass -NoProfile -File "%MULTI_LAUNCHER%" -BrowserPath "%BROWSER%" -MultiConfigPath "%MULTI_SIDECAR%" -FallbackUrl "%URL%" -LogFile "%LOG_FILE%"
|
|
158
|
+
) else (
|
|
159
|
+
echo [%date% %time%] Launching browser: %URL% >> "%LOG_FILE%"
|
|
160
|
+
start /wait "" "%BROWSER%" --kiosk --noerrdialogs --disable-infobars --disable-session-crashed-bubble --no-first-run --no-default-browser-check --start-fullscreen --disable-translate --disable-extensions --autoplay-policy=no-user-gesture-required --disable-features=TranslateUI --user-data-dir="%CHROME_DATA%" "%URL%"
|
|
161
|
+
)
|
|
145
162
|
|
|
146
163
|
echo [%date% %time%] Browser exited (code: %errorlevel%). Restarting in 3s... >> "%LOG_FILE%"
|
|
147
164
|
timeout /t 3 /nobreak >nul
|
package/src/index.ts
CHANGED
|
@@ -111,10 +111,26 @@ async function main(): Promise<void> {
|
|
|
111
111
|
serverUrl: config.serverUrl,
|
|
112
112
|
identity,
|
|
113
113
|
logger,
|
|
114
|
-
onMessage: (msg: WsMessage) => {
|
|
115
|
-
handleServerMessage(
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
onMessage: (msg: WsMessage) => {
|
|
115
|
+
handleServerMessage(
|
|
116
|
+
msg,
|
|
117
|
+
commandExecutor,
|
|
118
|
+
logger,
|
|
119
|
+
powerScheduler,
|
|
120
|
+
startSerialBridge,
|
|
121
|
+
stopSerialBridge,
|
|
122
|
+
startOscBridge,
|
|
123
|
+
stopOscBridge,
|
|
124
|
+
multiScreenKiosk,
|
|
125
|
+
getIdentity,
|
|
126
|
+
kioskManager,
|
|
127
|
+
watchdog,
|
|
128
|
+
startPresenceSensor,
|
|
129
|
+
stopPresenceSensor,
|
|
130
|
+
() => lastKnownTotalScreens
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
118
134
|
|
|
119
135
|
// 4. Start health monitor
|
|
120
136
|
const healthMonitor = new HealthMonitor(
|
|
@@ -163,14 +179,15 @@ async function main(): Promise<void> {
|
|
|
163
179
|
const kioskManager = new KioskManager(kioskConfig, logger);
|
|
164
180
|
registerKioskCommands(commandExecutor.register.bind(commandExecutor), kioskManager, logger);
|
|
165
181
|
|
|
166
|
-
// Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
|
|
167
|
-
const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
|
|
168
|
-
const getIdentity = () => identity!;
|
|
169
|
-
registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
|
|
170
|
-
|
|
171
|
-
// Detect physical screens and keep them fresh (multi-display setups can change after boot).
|
|
172
|
-
let
|
|
173
|
-
|
|
182
|
+
// Multi-screen kiosk manager — handles multiple Chrome instances on multi-display devices
|
|
183
|
+
const multiScreenKiosk = new MultiScreenKioskManager(kioskConfig, logger);
|
|
184
|
+
const getIdentity = () => identity!;
|
|
185
|
+
registerMultiScreenKioskCommands(commandExecutor.register.bind(commandExecutor), multiScreenKiosk, getIdentity, logger);
|
|
186
|
+
|
|
187
|
+
// Detect physical screens and keep them fresh (multi-display setups can change after boot).
|
|
188
|
+
let lastKnownTotalScreens = 0;
|
|
189
|
+
let detectedScreens = detectScreens(logger);
|
|
190
|
+
multiScreenKiosk.setDetectedScreens(detectedScreens);
|
|
174
191
|
|
|
175
192
|
const toScreenPayload = (screens: DetectedScreen[]) => (
|
|
176
193
|
screens.map((s) => ({
|
|
@@ -502,24 +519,30 @@ async function main(): Promise<void> {
|
|
|
502
519
|
// Multi-screen handling:
|
|
503
520
|
// 1) Use explicit screenMap when present.
|
|
504
521
|
// 2) For multi-screen apps without a saved map, auto-create placeholders.
|
|
505
|
-
const requestedScreenMap = deviceCfg?.screenMap || [];
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
522
|
+
const requestedScreenMap = deviceCfg?.screenMap || [];
|
|
523
|
+
const totalScreens = Math.max(deviceCfg?.totalScreens || 0, requestedScreenMap.length);
|
|
524
|
+
lastKnownTotalScreens = totalScreens;
|
|
525
|
+
const effectiveRequestedMap = normalizeScreenMapForTotalScreens(requestedScreenMap, totalScreens);
|
|
526
|
+
|
|
527
|
+
if (totalScreens > 1 && detectedScreens.length < totalScreens) {
|
|
528
|
+
logger.warn(
|
|
529
|
+
`[MultiKiosk] App expects ${totalScreens} screen(s) but agent detected ${detectedScreens.length}. ` +
|
|
530
|
+
'Remaining screens may stay black until Windows reports all displays.'
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (effectiveRequestedMap.length > 0) {
|
|
535
|
+
const resolved = resolveScreenMap({
|
|
536
|
+
requestedScreenMap: effectiveRequestedMap,
|
|
537
|
+
detectedScreens,
|
|
538
|
+
totalScreens,
|
|
539
|
+
});
|
|
540
|
+
logger.info(
|
|
541
|
+
`[MultiKiosk] Effective map ready: requested=${requestedScreenMap.length}, effective=${effectiveRequestedMap.length}, mode=${resolved.mode}, totalScreens=${totalScreens}`
|
|
542
|
+
);
|
|
543
|
+
watchdog.setMultiScreenActive(true);
|
|
544
|
+
multiScreenKiosk.applyScreenMap(effectiveRequestedMap, identity).catch((err) => {
|
|
545
|
+
logger.error('[MultiKiosk] Failed to apply effective screenMap from config:', err);
|
|
523
546
|
});
|
|
524
547
|
return;
|
|
525
548
|
}
|
|
@@ -606,10 +629,33 @@ async function main(): Promise<void> {
|
|
|
606
629
|
logger.info('LIGHTMAN Agent running.');
|
|
607
630
|
}
|
|
608
631
|
|
|
609
|
-
function createPlaceholderScreenMap(totalScreens: number): ScreenMapping[] {
|
|
610
|
-
const count = Math.max(0, Math.floor(totalScreens || 0));
|
|
611
|
-
return Array.from({ length: count }, () => ({ hardwareId: '', url: '' }));
|
|
612
|
-
}
|
|
632
|
+
function createPlaceholderScreenMap(totalScreens: number): ScreenMapping[] {
|
|
633
|
+
const count = Math.max(0, Math.floor(totalScreens || 0));
|
|
634
|
+
return Array.from({ length: count }, () => ({ hardwareId: '', url: '' }));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizeScreenMapForTotalScreens(
|
|
638
|
+
screenMap: ScreenMapping[] | undefined,
|
|
639
|
+
totalScreens: number
|
|
640
|
+
): ScreenMapping[] {
|
|
641
|
+
const requested = Array.isArray(screenMap)
|
|
642
|
+
? screenMap.map((m) => ({
|
|
643
|
+
hardwareId: String(m.hardwareId || ''),
|
|
644
|
+
url: String(m.url || ''),
|
|
645
|
+
...(m.label ? { label: String(m.label) } : {}),
|
|
646
|
+
}))
|
|
647
|
+
: [];
|
|
648
|
+
|
|
649
|
+
const targetCount = Math.max(requested.length, Math.max(0, Math.floor(totalScreens || 0)));
|
|
650
|
+
if (targetCount === 0) return [];
|
|
651
|
+
|
|
652
|
+
if (requested.length >= targetCount) return requested;
|
|
653
|
+
|
|
654
|
+
return [
|
|
655
|
+
...requested,
|
|
656
|
+
...createPlaceholderScreenMap(targetCount - requested.length),
|
|
657
|
+
];
|
|
658
|
+
}
|
|
613
659
|
|
|
614
660
|
function haveScreensChanged(prev: DetectedScreen[], next: DetectedScreen[]): boolean {
|
|
615
661
|
if (prev.length !== next.length) return true;
|
|
@@ -631,22 +677,23 @@ function haveScreensChanged(prev: DetectedScreen[], next: DetectedScreen[]): boo
|
|
|
631
677
|
return false;
|
|
632
678
|
}
|
|
633
679
|
|
|
634
|
-
function handleServerMessage(
|
|
635
|
-
msg: WsMessage,
|
|
636
|
-
commandExecutor: CommandExecutor,
|
|
680
|
+
function handleServerMessage(
|
|
681
|
+
msg: WsMessage,
|
|
682
|
+
commandExecutor: CommandExecutor,
|
|
637
683
|
logger: Logger,
|
|
638
684
|
powerScheduler?: PowerScheduler,
|
|
639
685
|
startSerialBridge?: (comPort: string, controllerId: string, baudRate?: number) => void,
|
|
640
686
|
stopSerialBridge?: () => void,
|
|
641
687
|
startOscBridgeFn?: (oscPort: number, oscAddress: string, oscHost?: string) => void,
|
|
642
688
|
stopOscBridgeFn?: () => void,
|
|
643
|
-
multiScreenKiosk?: MultiScreenKioskManager,
|
|
644
|
-
getIdentity?: () => Identity,
|
|
645
|
-
kioskManager?: KioskManager,
|
|
646
|
-
watchdog?: Watchdog,
|
|
647
|
-
startPresenceSensorFn?: (port?: string) => void,
|
|
648
|
-
stopPresenceSensorFn?: () => void
|
|
649
|
-
)
|
|
689
|
+
multiScreenKiosk?: MultiScreenKioskManager,
|
|
690
|
+
getIdentity?: () => Identity,
|
|
691
|
+
kioskManager?: KioskManager,
|
|
692
|
+
watchdog?: Watchdog,
|
|
693
|
+
startPresenceSensorFn?: (port?: string) => void,
|
|
694
|
+
stopPresenceSensorFn?: () => void,
|
|
695
|
+
getTotalScreensHint?: () => number
|
|
696
|
+
): void {
|
|
650
697
|
switch (msg.type) {
|
|
651
698
|
case 'connected':
|
|
652
699
|
logger.info('Server acknowledged connection');
|
|
@@ -698,16 +745,25 @@ function handleServerMessage(
|
|
|
698
745
|
}
|
|
699
746
|
|
|
700
747
|
// Admin pushed updated screenMap via device config save
|
|
701
|
-
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
702
|
-
if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
748
|
+
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
749
|
+
if (screenMap && Array.isArray(screenMap) && multiScreenKiosk && getIdentity) {
|
|
750
|
+
const payloadTotalScreens = Number(msg.payload.totalScreens || 0);
|
|
751
|
+
const hintTotalScreens = Math.max(
|
|
752
|
+
Number.isFinite(payloadTotalScreens) ? payloadTotalScreens : 0,
|
|
753
|
+
getTotalScreensHint ? getTotalScreensHint() : 0
|
|
754
|
+
);
|
|
755
|
+
const effectiveScreenMap = screenMap.length > 0
|
|
756
|
+
? normalizeScreenMapForTotalScreens(screenMap, hintTotalScreens)
|
|
757
|
+
: screenMap;
|
|
758
|
+
|
|
759
|
+
if (screenMap.length > 0) {
|
|
760
|
+
logger.info(`[MultiKiosk] Received screenMap update: requested=${screenMap.length}, effective=${effectiveScreenMap.length}, totalScreens=${hintTotalScreens} — killing single kiosk`);
|
|
761
|
+
if (kioskManager) kioskManager.kill().catch(() => {});
|
|
762
|
+
if (watchdog) watchdog.setMultiScreenActive(true);
|
|
763
|
+
multiScreenKiosk.applyScreenMap(effectiveScreenMap, getIdentity()).catch((err) => {
|
|
764
|
+
logger.error('[MultiKiosk] Failed to apply screenMap:', err);
|
|
765
|
+
});
|
|
766
|
+
} else {
|
|
711
767
|
// Empty screenMap — deactivate multi-screen, resume single kiosk
|
|
712
768
|
logger.info('[MultiKiosk] Empty screenMap received — deactivating multi-screen');
|
|
713
769
|
multiScreenKiosk.killAll().catch(() => {});
|
|
@@ -719,16 +775,25 @@ function handleServerMessage(
|
|
|
719
775
|
case 'agent:screenMap':
|
|
720
776
|
// Direct screenMap push from server
|
|
721
777
|
if (msg.payload && multiScreenKiosk && getIdentity) {
|
|
722
|
-
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
723
|
-
if (screenMap && Array.isArray(screenMap)) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
778
|
+
const screenMap = msg.payload.screenMap as ScreenMapping[] | undefined;
|
|
779
|
+
if (screenMap && Array.isArray(screenMap)) {
|
|
780
|
+
const payloadTotalScreens = Number(msg.payload.totalScreens || 0);
|
|
781
|
+
const hintTotalScreens = Math.max(
|
|
782
|
+
Number.isFinite(payloadTotalScreens) ? payloadTotalScreens : 0,
|
|
783
|
+
getTotalScreensHint ? getTotalScreensHint() : 0
|
|
784
|
+
);
|
|
785
|
+
const effectiveScreenMap = screenMap.length > 0
|
|
786
|
+
? normalizeScreenMapForTotalScreens(screenMap, hintTotalScreens)
|
|
787
|
+
: screenMap;
|
|
788
|
+
|
|
789
|
+
if (screenMap.length > 0) {
|
|
790
|
+
logger.info(`[MultiKiosk] Received agent:screenMap: requested=${screenMap.length}, effective=${effectiveScreenMap.length}, totalScreens=${hintTotalScreens} — killing single kiosk`);
|
|
791
|
+
if (kioskManager) kioskManager.kill().catch(() => {});
|
|
792
|
+
if (watchdog) watchdog.setMultiScreenActive(true);
|
|
793
|
+
multiScreenKiosk.applyScreenMap(effectiveScreenMap, getIdentity()).catch((err) => {
|
|
794
|
+
logger.error('[MultiKiosk] Failed to apply screenMap:', err);
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
732
797
|
logger.info('[MultiKiosk] Empty agent:screenMap — deactivating multi-screen');
|
|
733
798
|
multiScreenKiosk.killAll().catch(() => {});
|
|
734
799
|
if (watchdog) watchdog.setMultiScreenActive(false);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ChildProcess } from 'child_process';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import type { ChildProcess } from 'child_process';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { basename, dirname } from 'path';
|
|
5
|
+
import type { KioskConfig, ScreenMapping, MultiScreenKioskStatus, SingleScreenStatus } from '../lib/types.js';
|
|
6
|
+
import type { DetectedScreen } from '../lib/screens.js';
|
|
7
|
+
import type { Logger } from '../lib/logger.js';
|
|
8
|
+
import { resolveDetectedScreen, resolveScreenMap } from '../lib/screenMap.js';
|
|
9
|
+
|
|
10
|
+
const SHELL_MULTI_CONFIG_PATH = process.platform === 'win32'
|
|
11
|
+
? 'C:\\ProgramData\\Lightman\\kiosk-multi.json'
|
|
12
|
+
: '/tmp/lightman-kiosk-multi.json';
|
|
13
|
+
|
|
14
|
+
interface ScreenInstance {
|
|
9
15
|
hardwareId: string;
|
|
10
16
|
mappingId: string;
|
|
11
17
|
mappingUrl: string;
|
|
@@ -30,10 +36,14 @@ export class MultiScreenKioskManager {
|
|
|
30
36
|
private pollTimer: NodeJS.Timeout | null = null;
|
|
31
37
|
private applying = false;
|
|
32
38
|
|
|
33
|
-
constructor(config: KioskConfig, logger: Logger) {
|
|
34
|
-
this.config = config;
|
|
35
|
-
this.logger = logger;
|
|
36
|
-
}
|
|
39
|
+
constructor(config: KioskConfig, logger: Logger) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.logger = logger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private isShellModeOnWindows(): boolean {
|
|
45
|
+
return process.platform === 'win32' && !!this.config.shellMode;
|
|
46
|
+
}
|
|
37
47
|
|
|
38
48
|
/** Update the list of detected physical screens */
|
|
39
49
|
setDetectedScreens(screens: DetectedScreen[]): void {
|
|
@@ -63,12 +73,18 @@ export class MultiScreenKioskManager {
|
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
private async _applyScreenMap(screenMap: ScreenMapping[], identity: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
76
|
+
private async _applyScreenMap(screenMap: ScreenMapping[], identity: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
77
|
+
if (this.isShellModeOnWindows()) {
|
|
78
|
+
this.applyShellMultiConfig(screenMap, identity);
|
|
79
|
+
this.stopPoll();
|
|
80
|
+
return this.getStatus();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const resolvedMap = resolveScreenMap({
|
|
84
|
+
requestedScreenMap: screenMap,
|
|
85
|
+
detectedScreens: this.detectedScreens,
|
|
86
|
+
totalScreens: screenMap.length,
|
|
87
|
+
});
|
|
72
88
|
|
|
73
89
|
this.logger.info(
|
|
74
90
|
`[MultiKiosk] Applying screen map: ${resolvedMap.screenMap.length} mapping(s), mode=${resolvedMap.mode}`
|
|
@@ -204,7 +220,7 @@ export class MultiScreenKioskManager {
|
|
|
204
220
|
}
|
|
205
221
|
|
|
206
222
|
/** Build the full URL with device credentials and screenIndex */
|
|
207
|
-
private buildUrl(path: string, identity: { deviceId: string; apiKey: string }, screenIndex?: number): string {
|
|
223
|
+
private buildUrl(path: string, identity: { deviceId: string; apiKey: string }, screenIndex?: number): string {
|
|
208
224
|
let fullUrl: string;
|
|
209
225
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
210
226
|
fullUrl = path;
|
|
@@ -220,16 +236,95 @@ export class MultiScreenKioskManager {
|
|
|
220
236
|
url.searchParams.set('screenIndex', String(screenIndex));
|
|
221
237
|
}
|
|
222
238
|
|
|
223
|
-
return url.toString();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
239
|
+
return url.toString();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private applyShellMultiConfig(
|
|
243
|
+
screenMap: ScreenMapping[],
|
|
244
|
+
identity: { deviceId: string; apiKey: string }
|
|
245
|
+
): void {
|
|
246
|
+
const entries = screenMap.map((mapping, index) => {
|
|
247
|
+
const resolvedHardwareId = String(mapping.hardwareId || '').trim() || String(index + 1);
|
|
248
|
+
const basePath = mapping.url || this.config.defaultUrl;
|
|
249
|
+
return {
|
|
250
|
+
hardwareId: resolvedHardwareId,
|
|
251
|
+
screenIndex: index,
|
|
252
|
+
url: this.buildUrl(basePath, identity, index),
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const dir = dirname(SHELL_MULTI_CONFIG_PATH);
|
|
258
|
+
if (!existsSync(dir)) {
|
|
259
|
+
mkdirSync(dir, { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
writeFileSync(
|
|
263
|
+
SHELL_MULTI_CONFIG_PATH,
|
|
264
|
+
JSON.stringify({ updatedAt: Date.now(), entries }, null, 2),
|
|
265
|
+
'utf-8'
|
|
266
|
+
);
|
|
267
|
+
this.logger.info(`[MultiKiosk] Shell multi config written (${entries.length} screen(s))`);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
this.logger.error('[MultiKiosk] Failed to write shell multi config:', err);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.killShellManagedBrowsers();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private clearShellMultiConfig(): void {
|
|
277
|
+
try {
|
|
278
|
+
if (existsSync(SHELL_MULTI_CONFIG_PATH)) {
|
|
279
|
+
rmSync(SHELL_MULTI_CONFIG_PATH, { force: true });
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
this.logger.warn('[MultiKiosk] Failed to clear shell multi config:', err);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private killShellManagedBrowsers(): void {
|
|
287
|
+
if (process.platform !== 'win32') return;
|
|
288
|
+
|
|
289
|
+
const browserExe = basename(this.config.browserPath || '').toLowerCase();
|
|
290
|
+
const targets = new Set<string>(['chrome.exe', 'msedge.exe']);
|
|
291
|
+
if (browserExe.endsWith('.exe')) {
|
|
292
|
+
targets.add(browserExe);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const exeName of targets) {
|
|
296
|
+
try {
|
|
297
|
+
const killer = spawn('taskkill', ['/IM', exeName, '/F'], {
|
|
298
|
+
stdio: 'ignore',
|
|
299
|
+
windowsHide: true,
|
|
300
|
+
});
|
|
301
|
+
killer.unref();
|
|
302
|
+
} catch {
|
|
303
|
+
// Ignore process-kill failures; shell loop can still recover.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Navigate a single screen to a new URL */
|
|
309
|
+
async navigateScreen(hardwareId: string, url: string, identity: { deviceId: string; apiKey: string }): Promise<void> {
|
|
310
|
+
if (this.isShellModeOnWindows()) {
|
|
311
|
+
const idx = this.desiredScreenMap.findIndex((m) => String(m.hardwareId || '').trim() === String(hardwareId || '').trim());
|
|
312
|
+
if (idx === -1) {
|
|
313
|
+
this.logger.warn(`[MultiKiosk] Cannot navigate ${hardwareId} - not in desired map`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const nextMap = this.desiredScreenMap.map((m) => ({ ...m }));
|
|
318
|
+
nextMap[idx] = { ...nextMap[idx], url };
|
|
319
|
+
await this.applyScreenMap(nextMap, identity);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const resolvedHardwareId = resolveDetectedScreen(hardwareId, this.detectedScreens)?.hardwareId || hardwareId;
|
|
324
|
+
const existing = this.instances.get(resolvedHardwareId);
|
|
325
|
+
if (!existing) {
|
|
326
|
+
this.logger.warn(`[MultiKiosk] Cannot navigate ${hardwareId} - not in instance map`);
|
|
327
|
+
return;
|
|
233
328
|
}
|
|
234
329
|
|
|
235
330
|
this.killInstance(existing);
|
|
@@ -256,27 +351,41 @@ export class MultiScreenKioskManager {
|
|
|
256
351
|
}
|
|
257
352
|
|
|
258
353
|
/** Kill all Chrome instances */
|
|
259
|
-
async killAll(options?: { clearDesired?: boolean }): Promise<void> {
|
|
260
|
-
for (const [, instance] of this.instances) {
|
|
261
|
-
this.killInstance(instance);
|
|
262
|
-
}
|
|
263
|
-
this.instances.clear();
|
|
264
|
-
this.stopPoll();
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
this.
|
|
268
|
-
this.
|
|
269
|
-
}
|
|
354
|
+
async killAll(options?: { clearDesired?: boolean }): Promise<void> {
|
|
355
|
+
for (const [, instance] of this.instances) {
|
|
356
|
+
this.killInstance(instance);
|
|
357
|
+
}
|
|
358
|
+
this.instances.clear();
|
|
359
|
+
this.stopPoll();
|
|
360
|
+
|
|
361
|
+
if (this.isShellModeOnWindows()) {
|
|
362
|
+
this.clearShellMultiConfig();
|
|
363
|
+
this.killShellManagedBrowsers();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (options?.clearDesired !== false) {
|
|
367
|
+
this.desiredScreenMap = [];
|
|
368
|
+
this.desiredIdentity = null;
|
|
369
|
+
}
|
|
270
370
|
}
|
|
271
371
|
|
|
272
|
-
/** Restart all Chrome instances */
|
|
273
|
-
async restartAll(identity: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
274
|
-
this.logger.info('[MultiKiosk] Restarting all Chrome instances');
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
372
|
+
/** Restart all Chrome instances */
|
|
373
|
+
async restartAll(identity: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
374
|
+
this.logger.info('[MultiKiosk] Restarting all Chrome instances');
|
|
375
|
+
|
|
376
|
+
if (this.isShellModeOnWindows()) {
|
|
377
|
+
if (this.desiredScreenMap.length === 0) {
|
|
378
|
+
return this.getStatus();
|
|
379
|
+
}
|
|
380
|
+
this.killShellManagedBrowsers();
|
|
381
|
+
await new Promise((r) => setTimeout(r, 2_000));
|
|
382
|
+
return this.applyScreenMap(this.desiredScreenMap, identity);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const mappings: ScreenMapping[] = [];
|
|
386
|
+
for (const [, instance] of this.instances) {
|
|
387
|
+
mappings.push({ hardwareId: instance.mappingId, url: instance.mappingUrl, label: undefined });
|
|
388
|
+
}
|
|
280
389
|
|
|
281
390
|
await this.killAll({ clearDesired: false });
|
|
282
391
|
await new Promise((r) => setTimeout(r, 2_000));
|
|
@@ -296,12 +405,24 @@ export class MultiScreenKioskManager {
|
|
|
296
405
|
return this.applyScreenMap(this.desiredScreenMap, id);
|
|
297
406
|
}
|
|
298
407
|
|
|
299
|
-
/** Get status of all screen instances */
|
|
300
|
-
getStatus(): MultiScreenKioskStatus {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
408
|
+
/** Get status of all screen instances */
|
|
409
|
+
getStatus(): MultiScreenKioskStatus {
|
|
410
|
+
if (this.isShellModeOnWindows() && this.desiredIdentity && this.desiredScreenMap.length > 0) {
|
|
411
|
+
return {
|
|
412
|
+
screens: this.desiredScreenMap.map((mapping, index) => ({
|
|
413
|
+
hardwareId: String(mapping.hardwareId || '').trim() || String(index + 1),
|
|
414
|
+
url: this.buildUrl(mapping.url || this.config.defaultUrl, this.desiredIdentity!, index),
|
|
415
|
+
running: false,
|
|
416
|
+
pid: null,
|
|
417
|
+
uptimeMs: null,
|
|
418
|
+
})),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const screens: SingleScreenStatus[] = [];
|
|
423
|
+
for (const [, instance] of this.instances) {
|
|
424
|
+
const running = instance.process !== null && instance.process.exitCode === null;
|
|
425
|
+
screens.push({
|
|
305
426
|
hardwareId: instance.hardwareId,
|
|
306
427
|
url: instance.url,
|
|
307
428
|
running,
|
|
@@ -313,10 +434,13 @@ export class MultiScreenKioskManager {
|
|
|
313
434
|
return { screens };
|
|
314
435
|
}
|
|
315
436
|
|
|
316
|
-
/** Check if any screens are actively managed */
|
|
317
|
-
isActive(): boolean {
|
|
318
|
-
|
|
319
|
-
|
|
437
|
+
/** Check if any screens are actively managed */
|
|
438
|
+
isActive(): boolean {
|
|
439
|
+
if (this.isShellModeOnWindows()) {
|
|
440
|
+
return this.desiredScreenMap.length > 0;
|
|
441
|
+
}
|
|
442
|
+
return this.instances.size > 0;
|
|
443
|
+
}
|
|
320
444
|
|
|
321
445
|
/** Cleanup on agent shutdown */
|
|
322
446
|
destroy(): void {
|