lightman-agent 1.0.24 → 1.0.27
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/bin/cms-agent.js +46 -31
- 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 +7 -0
- package/src/services/multiScreenKiosk.ts +182 -58
package/bin/cms-agent.js
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
4
|
-
import { networkInterfaces } from 'os';
|
|
5
|
-
import { resolve, dirname } from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { networkInterfaces } from 'os';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
8
|
import { createInterface } from 'readline/promises';
|
|
9
9
|
import { stdin as input, stdout as output, cwd, platform, exit } from 'process';
|
|
10
10
|
|
|
11
|
-
const DEFAULT_SERVER = 'http://192.168.10.100:3401';
|
|
12
|
-
const INSTALL_CONFIG_PATH = 'C:\\Program Files\\Lightman\\Agent\\agent.config.json';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
const DEFAULT_SERVER = 'http://192.168.10.100:3401';
|
|
12
|
+
const INSTALL_CONFIG_PATH = 'C:\\Program Files\\Lightman\\Agent\\agent.config.json';
|
|
13
|
+
const PACKAGE_JSON_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
14
|
+
|
|
15
|
+
function getVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
|
|
18
|
+
return String(pkg.version || '0.0.0');
|
|
19
|
+
} catch {
|
|
20
|
+
return '0.0.0';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function printUsage() {
|
|
25
|
+
console.log(`
|
|
26
|
+
cms-agent <command> [options]
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
install Prompt slug and server IP, install agent with ShellReplace, reboot
|
|
30
|
+
setup Alias of install
|
|
31
|
+
update Reinstall/update using installed config, reboot
|
|
32
|
+
version Print package version
|
|
22
33
|
|
|
23
34
|
Options:
|
|
24
35
|
--slug <value> Device slug (example: C-AV01)
|
|
@@ -262,28 +273,32 @@ async function runUpdate(opts) {
|
|
|
262
273
|
installUsingPowerShell({ scriptPath, slug, server, timezone, pairingTimeoutSeconds, noRestart });
|
|
263
274
|
}
|
|
264
275
|
|
|
265
|
-
async function main() {
|
|
266
|
-
const [, , commandRaw, ...rest] = process.argv;
|
|
267
|
-
const command = commandRaw || '';
|
|
268
|
-
const opts = parseArgs(rest);
|
|
276
|
+
async function main() {
|
|
277
|
+
const [, , commandRaw, ...rest] = process.argv;
|
|
278
|
+
const command = commandRaw || '';
|
|
279
|
+
const opts = parseArgs(rest);
|
|
269
280
|
|
|
270
281
|
if (!command || opts.help || command === 'help' || command === '--help' || command === '-h') {
|
|
271
282
|
printUsage();
|
|
272
283
|
return;
|
|
273
284
|
}
|
|
274
285
|
|
|
275
|
-
if (command === 'install' || command === 'setup') {
|
|
276
|
-
await runInstall(opts);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (command === 'update') {
|
|
280
|
-
await runUpdate(opts);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
286
|
+
if (command === 'install' || command === 'setup') {
|
|
287
|
+
await runInstall(opts);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (command === 'update') {
|
|
291
|
+
await runUpdate(opts);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
295
|
+
console.log(`lightman-agent v${getVersion()}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
printUsage();
|
|
300
|
+
throw new Error(`Unknown command: ${command}`);
|
|
301
|
+
}
|
|
287
302
|
|
|
288
303
|
main().catch((err) => {
|
|
289
304
|
console.error(err instanceof Error ? err.message : String(err));
|
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
|
@@ -524,6 +524,13 @@ async function main(): Promise<void> {
|
|
|
524
524
|
lastKnownTotalScreens = totalScreens;
|
|
525
525
|
const effectiveRequestedMap = normalizeScreenMapForTotalScreens(requestedScreenMap, totalScreens);
|
|
526
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
|
+
|
|
527
534
|
if (effectiveRequestedMap.length > 0) {
|
|
528
535
|
const resolved = resolveScreenMap({
|
|
529
536
|
requestedScreenMap: effectiveRequestedMap,
|
|
@@ -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 {
|