lightman-agent 1.0.8 → 1.0.11
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 +53 -4
- package/package.json +1 -1
- package/scripts/guardian.ps1 +25 -6
- package/scripts/install-windows.ps1 +88 -2
- package/scripts/launch-kiosk.vbs +23 -7
- package/scripts/lightman-shell.bat +7 -3
- package/scripts/setup.ps1 +23 -7
- package/src/index.ts +2 -2
- package/src/lib/config.ts +22 -1
- package/src/services/kiosk.ts +39 -22
package/bin/cms-agent.js
CHANGED
|
@@ -16,13 +16,13 @@ function printUsage() {
|
|
|
16
16
|
cms-agent <command> [options]
|
|
17
17
|
|
|
18
18
|
Commands:
|
|
19
|
-
install Prompt slug, install agent with ShellReplace, reboot
|
|
19
|
+
install Prompt slug and server IP, install agent with ShellReplace, reboot
|
|
20
20
|
setup Alias of install
|
|
21
21
|
update Reinstall/update using installed config, reboot
|
|
22
22
|
|
|
23
23
|
Options:
|
|
24
24
|
--slug <value> Device slug (example: C-AV01)
|
|
25
|
-
--server <url> Server URL (example: http://192.168.1.100:3401)
|
|
25
|
+
--server <url> Server URL or IP (example: 192.168.1.100 or http://192.168.1.100:3401)
|
|
26
26
|
--timezone <tz> Timezone override (default: Asia/Kolkata)
|
|
27
27
|
--pair-timeout <s> Wait time for pairing in seconds (default: 900, 0 = no timeout)
|
|
28
28
|
--no-restart Skip reboot after successful install/update
|
|
@@ -79,6 +79,33 @@ function isValidSlug(slug) {
|
|
|
79
79
|
return /^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(slug);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function normalizeServer(server) {
|
|
83
|
+
const value = (server || '').trim();
|
|
84
|
+
if (!value) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hasProtocol = /^[a-z]+:\/\//i.test(value);
|
|
89
|
+
const candidate = hasProtocol ? value : `http://${value}`;
|
|
90
|
+
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = new URL(candidate);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error('Invalid server IP/URL. Use an IP like 192.168.1.100 or a URL like http://192.168.1.100:3401.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!parsed.port) {
|
|
99
|
+
parsed.port = '3401';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (parsed.pathname === '/') {
|
|
103
|
+
parsed.pathname = '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parsed.toString().replace(/\/$/, '');
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
function collectMacAddresses() {
|
|
83
110
|
const nets = networkInterfaces();
|
|
84
111
|
const macs = new Set();
|
|
@@ -129,6 +156,27 @@ async function promptSlug(defaultSlug) {
|
|
|
129
156
|
}
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
async function promptServer(defaultServer) {
|
|
160
|
+
const rl = createInterface({ input, output });
|
|
161
|
+
try {
|
|
162
|
+
while (true) {
|
|
163
|
+
const prompt = defaultServer
|
|
164
|
+
? `Enter server IP or URL [${defaultServer}]: `
|
|
165
|
+
: 'Enter server IP or URL (example 192.168.1.100 or http://192.168.1.100:3401): ';
|
|
166
|
+
const answer = (await rl.question(prompt)).trim();
|
|
167
|
+
const server = answer || defaultServer || '';
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return normalizeServer(server);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
rl.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
function resolveInstallScript() {
|
|
133
181
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
134
182
|
const packaged = resolve(here, '../scripts/install-windows.ps1');
|
|
@@ -166,7 +214,7 @@ async function runInstall(opts) {
|
|
|
166
214
|
const localConfig = safeReadJson(resolve(cwd(), 'agent.config.json')) || {};
|
|
167
215
|
const installedConfig = safeReadJson(INSTALL_CONFIG_PATH) || {};
|
|
168
216
|
const defaultSlug = opts.slug || localConfig.deviceSlug || installedConfig.deviceSlug || '';
|
|
169
|
-
const
|
|
217
|
+
const defaultServer = localConfig.serverUrl || installedConfig.serverUrl || DEFAULT_SERVER;
|
|
170
218
|
const timezone = opts.timezone || localConfig?.powerSchedule?.timezone || installedConfig?.powerSchedule?.timezone || 'Asia/Kolkata';
|
|
171
219
|
const pairingTimeoutSeconds = Number.isFinite(Number(opts.pairTimeout)) ? Number.parseInt(String(opts.pairTimeout), 10) : 900;
|
|
172
220
|
const noRestart = Boolean(opts.noRestart);
|
|
@@ -183,6 +231,7 @@ async function runInstall(opts) {
|
|
|
183
231
|
console.log('');
|
|
184
232
|
|
|
185
233
|
const slug = opts.slug || await promptSlug(defaultSlug);
|
|
234
|
+
const server = opts.server ? normalizeServer(opts.server) : await promptServer(defaultServer);
|
|
186
235
|
const scriptPath = resolveInstallScript();
|
|
187
236
|
|
|
188
237
|
console.log(`Installing with slug=${slug}, server=${server}, shellReplace=true`);
|
|
@@ -199,7 +248,7 @@ async function runUpdate(opts) {
|
|
|
199
248
|
|
|
200
249
|
const installedConfig = safeReadJson(INSTALL_CONFIG_PATH) || {};
|
|
201
250
|
const slug = opts.slug || installedConfig.deviceSlug;
|
|
202
|
-
const server = opts.server || DEFAULT_SERVER;
|
|
251
|
+
const server = normalizeServer(opts.server || installedConfig.serverUrl || DEFAULT_SERVER);
|
|
203
252
|
const timezone = opts.timezone || installedConfig?.powerSchedule?.timezone || 'Asia/Kolkata';
|
|
204
253
|
const pairingTimeoutSeconds = Number.isFinite(Number(opts.pairTimeout)) ? Number.parseInt(String(opts.pairTimeout), 10) : 900;
|
|
205
254
|
const noRestart = Boolean(opts.noRestart);
|
package/package.json
CHANGED
package/scripts/guardian.ps1
CHANGED
|
@@ -6,6 +6,7 @@ $LogDir = "C:\ProgramData\Lightman\logs"
|
|
|
6
6
|
$LogFile = Join-Path $LogDir "guardian.log"
|
|
7
7
|
$ServiceName = "LightmanAgent"
|
|
8
8
|
$NssmExe = "C:\ProgramData\Lightman\nssm\nssm.exe"
|
|
9
|
+
$ConfigPath = "C:\Program Files\Lightman\Agent\agent.config.json"
|
|
9
10
|
|
|
10
11
|
function Write-GuardianLog($msg) {
|
|
11
12
|
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
@@ -20,6 +21,23 @@ function Write-GuardianLog($msg) {
|
|
|
20
21
|
} catch { }
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function Get-BrowserProcessName {
|
|
25
|
+
try {
|
|
26
|
+
if (Test-Path $ConfigPath) {
|
|
27
|
+
$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
|
28
|
+
$browserPath = $config.kiosk.browserPath
|
|
29
|
+
if ($browserPath) {
|
|
30
|
+
$browserLeaf = Split-Path $browserPath -Leaf
|
|
31
|
+
if ($browserLeaf) {
|
|
32
|
+
return [System.IO.Path]::GetFileNameWithoutExtension($browserLeaf)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch { }
|
|
37
|
+
|
|
38
|
+
return "chrome"
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
try {
|
|
24
42
|
# 1. Check LIGHTMAN service
|
|
25
43
|
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
|
@@ -57,15 +75,16 @@ try {
|
|
|
57
75
|
}
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
# 2. Check
|
|
61
|
-
$
|
|
62
|
-
|
|
78
|
+
# 2. Check kiosk browser
|
|
79
|
+
$browserProcess = Get-BrowserProcessName
|
|
80
|
+
$browser = Get-Process -Name $browserProcess -ErrorAction SilentlyContinue
|
|
81
|
+
if (-not $browser) {
|
|
63
82
|
$vbsPath = "C:\Program Files\Lightman\Agent\launch-kiosk.vbs"
|
|
64
83
|
if (Test-Path $vbsPath) {
|
|
65
84
|
Start-Sleep -Seconds 10
|
|
66
|
-
$
|
|
67
|
-
if (-not $
|
|
68
|
-
Write-GuardianLog "
|
|
85
|
+
$browserRecheck = Get-Process -Name $browserProcess -ErrorAction SilentlyContinue
|
|
86
|
+
if (-not $browserRecheck) {
|
|
87
|
+
Write-GuardianLog "Browser '$browserProcess' not running. Launching via VBS..."
|
|
69
88
|
Start-Process "wscript.exe" -ArgumentList """$vbsPath""" -WindowStyle Hidden
|
|
70
89
|
}
|
|
71
90
|
}
|
|
@@ -34,6 +34,80 @@ $AgentDir = Split-Path -Parent $ScriptDir
|
|
|
34
34
|
|
|
35
35
|
if (-not $Username) { $Username = $env:USERNAME }
|
|
36
36
|
|
|
37
|
+
function Get-ChromePath {
|
|
38
|
+
$candidates = @(
|
|
39
|
+
"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
40
|
+
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
41
|
+
(Join-Path $env:LOCALAPPDATA "Google\Chrome\Application\chrome.exe")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
foreach ($candidate in $candidates) {
|
|
45
|
+
if ($candidate -and (Test-Path $candidate)) {
|
|
46
|
+
return $candidate
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return $null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Install-ChromeFromMsi {
|
|
54
|
+
$is64Bit = [Environment]::Is64BitOperatingSystem
|
|
55
|
+
$downloadUrl = if ($is64Bit) {
|
|
56
|
+
"https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise64.msi"
|
|
57
|
+
} else {
|
|
58
|
+
"https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise.msi"
|
|
59
|
+
}
|
|
60
|
+
$installerName = if ($is64Bit) { "googlechromestandaloneenterprise64.msi" } else { "googlechromestandaloneenterprise.msi" }
|
|
61
|
+
$installerPath = Join-Path $env:TEMP $installerName
|
|
62
|
+
|
|
63
|
+
Write-Host " Chrome not found. Downloading official Google Chrome MSI..." -ForegroundColor Yellow
|
|
64
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
65
|
+
Invoke-WebRequest -Uri $downloadUrl -OutFile $installerPath -UseBasicParsing -TimeoutSec 180
|
|
66
|
+
|
|
67
|
+
if (-not (Test-Path $installerPath)) {
|
|
68
|
+
throw "Chrome MSI was not downloaded."
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$msiArgs = "/i `"$installerPath`" /qn /norestart"
|
|
72
|
+
Start-Process msiexec.exe -ArgumentList $msiArgs -Wait -NoNewWindow
|
|
73
|
+
Remove-Item $installerPath -Force -ErrorAction SilentlyContinue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function Install-ChromeFromWinget {
|
|
77
|
+
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
|
78
|
+
throw "winget is not available."
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Write-Host " MSI install failed. Trying winget for Google Chrome..." -ForegroundColor Yellow
|
|
82
|
+
& winget install --id Google.Chrome --exact --silent --accept-package-agreements --accept-source-agreements 2>&1 | Out-Host
|
|
83
|
+
if ($LASTEXITCODE -ne 0) {
|
|
84
|
+
throw "winget install failed with exit code $LASTEXITCODE."
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Ensure-ChromeInstalled {
|
|
89
|
+
$existingChrome = Get-ChromePath
|
|
90
|
+
if ($existingChrome) {
|
|
91
|
+
Write-Host " Found Chrome: $existingChrome" -ForegroundColor Green
|
|
92
|
+
return $existingChrome
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
Install-ChromeFromMsi
|
|
97
|
+
} catch {
|
|
98
|
+
Write-Host " Chrome MSI install failed: $($_.Exception.Message)" -ForegroundColor DarkYellow
|
|
99
|
+
Install-ChromeFromWinget
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
$installedChrome = Get-ChromePath
|
|
103
|
+
if (-not $installedChrome) {
|
|
104
|
+
throw "Chrome install did not produce chrome.exe."
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Write-Host " Chrome installed: $installedChrome" -ForegroundColor Green
|
|
108
|
+
return $installedChrome
|
|
109
|
+
}
|
|
110
|
+
|
|
37
111
|
Write-Host ""
|
|
38
112
|
Write-Host "=============================================" -ForegroundColor Cyan
|
|
39
113
|
Write-Host " LIGHTMAN Agent - Complete Windows Installer" -ForegroundColor Cyan
|
|
@@ -70,7 +144,7 @@ foreach ($tn in @($AgentTask, $KioskTask, $GuardianTask)) {
|
|
|
70
144
|
}
|
|
71
145
|
|
|
72
146
|
# Kill processes
|
|
73
|
-
Write-Host "[0c] Killing node.exe and
|
|
147
|
+
Write-Host "[0c] Killing node.exe and kiosk browser..." -ForegroundColor Yellow
|
|
74
148
|
# IMPORTANT:
|
|
75
149
|
# If install-windows.ps1 is launched from the npm CLI wrapper (node.exe),
|
|
76
150
|
# killing all node.exe would terminate the installer mid-run.
|
|
@@ -86,6 +160,7 @@ Get-Process -Name "node" -ErrorAction SilentlyContinue | ForEach-Object {
|
|
|
86
160
|
}
|
|
87
161
|
}
|
|
88
162
|
Get-Process -Name "chrome" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
163
|
+
Get-Process -Name "msedge" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
89
164
|
Start-Sleep -Seconds 2
|
|
90
165
|
|
|
91
166
|
# Remove old files (keep NSSM and logs)
|
|
@@ -155,6 +230,16 @@ if ($LASTEXITCODE -ne 0) { & npm install --omit=dev --ignore-scripts 2>&1 | Out-
|
|
|
155
230
|
$ErrorActionPreference = "Stop"
|
|
156
231
|
Pop-Location
|
|
157
232
|
|
|
233
|
+
# --- 5b. Chrome ---
|
|
234
|
+
Write-Host "[5b/19] Ensuring Google Chrome is installed..." -ForegroundColor Yellow
|
|
235
|
+
try {
|
|
236
|
+
$chromePath = Ensure-ChromeInstalled
|
|
237
|
+
} catch {
|
|
238
|
+
Write-Host " FATAL: $($_.Exception.Message)" -ForegroundColor Red
|
|
239
|
+
Write-Host " Install Google Chrome manually, then re-run the installer." -ForegroundColor Yellow
|
|
240
|
+
exit 1
|
|
241
|
+
}
|
|
242
|
+
|
|
158
243
|
# --- 6. Generate config ---
|
|
159
244
|
Write-Host "[6/19] Generating config..." -ForegroundColor Yellow
|
|
160
245
|
if ($ShellReplace) {
|
|
@@ -551,7 +636,7 @@ $svcStatus = if ($finalSvc) { "$($finalSvc.Status)" } else { "NOT FOUND" }
|
|
|
551
636
|
$cfgOk = $false
|
|
552
637
|
try {
|
|
553
638
|
Push-Location $InstallDir
|
|
554
|
-
$cfgResult = & node -e "const c=JSON.parse(require('fs').readFileSync('agent.config.json','utf8'));console.log(JSON.stringify({slug:c.deviceSlug,shell:c.kiosk&&c.kiosk.shellMode||false}))" 2>&1
|
|
639
|
+
$cfgResult = & node -e "const c=JSON.parse(require('fs').readFileSync('agent.config.json','utf8'));console.log(JSON.stringify({slug:c.deviceSlug,shell:c.kiosk&&c.kiosk.shellMode||false,browser:c.kiosk&&c.kiosk.browserPath||''}))" 2>&1
|
|
555
640
|
$cfgData = $cfgResult | ConvertFrom-Json
|
|
556
641
|
Pop-Location
|
|
557
642
|
$cfgOk = $true
|
|
@@ -572,6 +657,7 @@ Write-Host " Service : $svcStatus" -ForegroundColor $(if ($svcStatus -eq 'Ru
|
|
|
572
657
|
if ($cfgOk) {
|
|
573
658
|
Write-Host " Config slug: $($cfgData.slug)" -ForegroundColor $(if ($cfgData.slug -eq $Slug) { 'Green' } else { 'Red' })
|
|
574
659
|
Write-Host " Shell mode : $($cfgData.shell)" -ForegroundColor $(if ($cfgData.shell -eq $ShellReplace.IsPresent) { 'Green' } else { 'Red' })
|
|
660
|
+
Write-Host " Browser : $($cfgData.browser)" -ForegroundColor Green
|
|
575
661
|
}
|
|
576
662
|
Write-Host ""
|
|
577
663
|
Write-Host " Manage:" -ForegroundColor DarkGray
|
package/scripts/launch-kiosk.vbs
CHANGED
|
@@ -26,6 +26,11 @@ objFile.Close
|
|
|
26
26
|
|
|
27
27
|
browserPath = ExtractJsonValue(jsonText, "browserPath")
|
|
28
28
|
defaultUrl = ExtractJsonValue(jsonText, "defaultUrl")
|
|
29
|
+
browserExeName = LCase(ExtractFileName(browserPath))
|
|
30
|
+
|
|
31
|
+
If browserExeName = "" Then
|
|
32
|
+
browserExeName = "chrome.exe"
|
|
33
|
+
End If
|
|
29
34
|
|
|
30
35
|
If browserPath = "" Or defaultUrl = "" Then
|
|
31
36
|
WScript.Quit 1
|
|
@@ -35,11 +40,11 @@ End If
|
|
|
35
40
|
' Try to ping the server (extract hostname from URL)
|
|
36
41
|
waitedMs = 0
|
|
37
42
|
Do While waitedMs < (maxWaitSeconds * 1000)
|
|
38
|
-
' Check if
|
|
43
|
+
' Check if the browser is already running (agent's KioskManager may have launched it)
|
|
39
44
|
Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
|
|
40
|
-
Set colProcesses = objWMI.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = '
|
|
45
|
+
Set colProcesses = objWMI.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = '" & browserExeName & "'")
|
|
41
46
|
If colProcesses.Count > 0 Then
|
|
42
|
-
'
|
|
47
|
+
' Browser already running - agent is handling it. Exit gracefully.
|
|
43
48
|
WScript.Quit 0
|
|
44
49
|
End If
|
|
45
50
|
|
|
@@ -56,14 +61,14 @@ Do While waitedMs < (maxWaitSeconds * 1000)
|
|
|
56
61
|
' Service is running - give it a few more seconds to launch Chrome itself
|
|
57
62
|
WScript.Sleep 15000
|
|
58
63
|
|
|
59
|
-
' Re-check if
|
|
60
|
-
Set colProcesses2 = objWMI.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = '
|
|
64
|
+
' Re-check if the browser appeared (agent launched it)
|
|
65
|
+
Set colProcesses2 = objWMI.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = '" & browserExeName & "'")
|
|
61
66
|
If colProcesses2.Count > 0 Then
|
|
62
|
-
' Agent launched
|
|
67
|
+
' Agent launched the browser successfully. Exit.
|
|
63
68
|
WScript.Quit 0
|
|
64
69
|
End If
|
|
65
70
|
|
|
66
|
-
' Agent is running but hasn't launched
|
|
71
|
+
' Agent is running but hasn't launched the browser yet - we'll do it
|
|
67
72
|
Exit Do
|
|
68
73
|
End If
|
|
69
74
|
|
|
@@ -99,3 +104,14 @@ Function ExtractJsonValue(json, key)
|
|
|
99
104
|
If endPos = 0 Then Exit Function
|
|
100
105
|
ExtractJsonValue = Mid(json, pos, endPos - pos)
|
|
101
106
|
End Function
|
|
107
|
+
|
|
108
|
+
Function ExtractFileName(path)
|
|
109
|
+
ExtractFileName = ""
|
|
110
|
+
If path = "" Then Exit Function
|
|
111
|
+
On Error Resume Next
|
|
112
|
+
ExtractFileName = objFSO.GetFileName(path)
|
|
113
|
+
If ExtractFileName = "" Then
|
|
114
|
+
ExtractFileName = path
|
|
115
|
+
End If
|
|
116
|
+
On Error GoTo 0
|
|
117
|
+
End Function
|
|
@@ -84,8 +84,12 @@ if "%BROWSER%"=="" (
|
|
|
84
84
|
set "BROWSER=C:\Program Files\Google\Chrome\Application\chrome.exe"
|
|
85
85
|
) else if exist "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" (
|
|
86
86
|
set "BROWSER=C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
|
|
87
|
+
) else if exist "C:\Program Files\Microsoft\Edge\Application\msedge.exe" (
|
|
88
|
+
set "BROWSER=C:\Program Files\Microsoft\Edge\Application\msedge.exe"
|
|
89
|
+
) else if exist "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" (
|
|
90
|
+
set "BROWSER=C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
|
87
91
|
) else (
|
|
88
|
-
echo [%date% %time%] ERROR:
|
|
92
|
+
echo [%date% %time%] ERROR: No supported kiosk browser found! >> "%LOG_FILE%"
|
|
89
93
|
start explorer.exe
|
|
90
94
|
exit /b 1
|
|
91
95
|
)
|
|
@@ -135,11 +139,11 @@ REM ----------------------------------------------------------------
|
|
|
135
139
|
)
|
|
136
140
|
)
|
|
137
141
|
|
|
138
|
-
echo [%date% %time%] Launching
|
|
142
|
+
echo [%date% %time%] Launching browser: %URL% >> "%LOG_FILE%"
|
|
139
143
|
|
|
140
144
|
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%"
|
|
141
145
|
|
|
142
|
-
echo [%date% %time%]
|
|
146
|
+
echo [%date% %time%] Browser exited (code: %errorlevel%). Restarting in 3s... >> "%LOG_FILE%"
|
|
143
147
|
timeout /t 3 /nobreak >nul
|
|
144
148
|
|
|
145
149
|
goto loop
|
package/scripts/setup.ps1
CHANGED
|
@@ -42,6 +42,23 @@ Write-Host " Install dir: $InstallDir"
|
|
|
42
42
|
Write-Host " Timezone: $Timezone"
|
|
43
43
|
Write-Host ""
|
|
44
44
|
|
|
45
|
+
function Get-KioskBrowserPath {
|
|
46
|
+
$candidates = @(
|
|
47
|
+
"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
48
|
+
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
49
|
+
(Join-Path $env:LOCALAPPDATA "Google\Chrome\Application\chrome.exe"),
|
|
50
|
+
"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
|
51
|
+
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
|
52
|
+
(Join-Path $env:LOCALAPPDATA "Microsoft\Edge\Application\msedge.exe")
|
|
53
|
+
) | Where-Object { $_ -and (Test-Path $_) }
|
|
54
|
+
|
|
55
|
+
if ($candidates.Count -gt 0) {
|
|
56
|
+
return $candidates[0]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw "Neither Google Chrome nor Microsoft Edge was found. Install one of them and re-run setup."
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
# 1. Clear cached identity (CRITICAL - prevents old device credentials leaking)
|
|
46
63
|
$IdentityFile = Join-Path $InstallDir ".lightman-identity.json"
|
|
47
64
|
if (Test-Path $IdentityFile) {
|
|
@@ -55,13 +72,12 @@ if (Test-Path $IdentityFile) {
|
|
|
55
72
|
$KioskUrl = "http://localhost:3403/display/$Slug"
|
|
56
73
|
|
|
57
74
|
# 3. Detect browser path
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
$BrowserPath
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Write-Host "[WARN] Chrome not found - using 'chromium-browser'" -ForegroundColor Yellow
|
|
75
|
+
try {
|
|
76
|
+
$BrowserPath = Get-KioskBrowserPath
|
|
77
|
+
Write-Host "[OK] Using kiosk browser: $BrowserPath" -ForegroundColor Green
|
|
78
|
+
} catch {
|
|
79
|
+
Write-Host "[ERROR] $($_.Exception.Message)" -ForegroundColor Red
|
|
80
|
+
exit 1
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
$ChromeDataDir = "C:\ProgramData\Lightman\chrome-kiosk"
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { loadConfig } from './lib/config.js';
|
|
3
|
+
import { getDefaultKioskBrowserPath, loadConfig } from './lib/config.js';
|
|
4
4
|
import { Logger } from './lib/logger.js';
|
|
5
5
|
import { provision } from './services/provisioning.js';
|
|
6
6
|
import { WsClient } from './services/websocket.js';
|
|
@@ -135,7 +135,7 @@ async function main(): Promise<void> {
|
|
|
135
135
|
|
|
136
136
|
// Create KioskManager if kiosk config is present
|
|
137
137
|
const baseKioskConfig: KioskConfig = config.kiosk || {
|
|
138
|
-
browserPath:
|
|
138
|
+
browserPath: getDefaultKioskBrowserPath(),
|
|
139
139
|
defaultUrl: `${config.serverUrl.replace(/:\d+$/, ':3401')}/display`,
|
|
140
140
|
extraArgs: [],
|
|
141
141
|
pollIntervalMs: 10_000,
|
package/src/lib/config.ts
CHANGED
|
@@ -3,8 +3,29 @@ import { resolve } from 'path';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { AgentConfig } from './types.js';
|
|
5
5
|
|
|
6
|
+
export function getDefaultKioskBrowserPath(): string {
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
const candidates = [
|
|
9
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
10
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
11
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
12
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
if (existsSync(candidate)) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return 'chromium-browser';
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
const kioskSchema = z.object({
|
|
7
|
-
browserPath: z.string().default(
|
|
28
|
+
browserPath: z.string().default(getDefaultKioskBrowserPath()),
|
|
8
29
|
defaultUrl: z.string().url().default('http://localhost:3401/display'),
|
|
9
30
|
extraArgs: z.array(z.string()).default([]),
|
|
10
31
|
pollIntervalMs: z.number().int().min(1000).default(10_000),
|
package/src/services/kiosk.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execSync } from 'child_process';
|
|
2
2
|
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
3
|
-
import {
|
|
3
|
+
import { basename } from 'path';
|
|
4
4
|
import type { ChildProcess } from 'child_process';
|
|
5
5
|
import type { KioskConfig, KioskStatus } from '../lib/types.js';
|
|
6
6
|
import type { Logger } from '../lib/logger.js';
|
|
@@ -57,7 +57,7 @@ export class KioskManager {
|
|
|
57
57
|
async kill(): Promise<void> {
|
|
58
58
|
if (this.shellMode) {
|
|
59
59
|
// In shell mode, we just kill Chrome — the shell BAT will relaunch it
|
|
60
|
-
this.
|
|
60
|
+
this.killAllBrowser();
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -110,7 +110,7 @@ export class KioskManager {
|
|
|
110
110
|
// Write new URL → kill Chrome → shell relaunches with new URL
|
|
111
111
|
this.writeUrlSidecar(url);
|
|
112
112
|
this.currentUrl = url;
|
|
113
|
-
this.
|
|
113
|
+
this.killAllBrowser();
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -123,7 +123,7 @@ export class KioskManager {
|
|
|
123
123
|
|
|
124
124
|
if (this.shellMode) {
|
|
125
125
|
// Just kill Chrome — shell relaunches it with same URL from sidecar
|
|
126
|
-
this.
|
|
126
|
+
this.killAllBrowser();
|
|
127
127
|
// Give shell time to relaunch
|
|
128
128
|
await new Promise((r) => setTimeout(r, 5_000));
|
|
129
129
|
return this.getStatus();
|
|
@@ -176,11 +176,11 @@ export class KioskManager {
|
|
|
176
176
|
|
|
177
177
|
// Shell mode: Chrome is managed by lightman-shell.bat.
|
|
178
178
|
// Shell prefers sidecar URL (if present), then falls back to slug in config.
|
|
179
|
-
if (this.
|
|
180
|
-
this.logger.info('Shell mode:
|
|
181
|
-
this.
|
|
179
|
+
if (this.isBrowserRunning()) {
|
|
180
|
+
this.logger.info('Shell mode: browser already running. Restarting once to apply sidecar URL.');
|
|
181
|
+
this.killAllBrowser();
|
|
182
182
|
} else {
|
|
183
|
-
this.logger.info('Shell mode:
|
|
183
|
+
this.logger.info('Shell mode: browser not running. Shell BAT will launch it.');
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
this.startedAt = this.startedAt || Date.now();
|
|
@@ -188,10 +188,10 @@ export class KioskManager {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
private getShellModeStatus(): KioskStatus {
|
|
191
|
-
const running = this.
|
|
191
|
+
const running = this.isBrowserRunning();
|
|
192
192
|
return {
|
|
193
193
|
running,
|
|
194
|
-
pid: running ? this.
|
|
194
|
+
pid: running ? this.getBrowserPid() : null,
|
|
195
195
|
url: this.currentUrl || this.readUrlSidecar(),
|
|
196
196
|
crashCount: 0, // Shell handles crash recovery, not us
|
|
197
197
|
crashLoopDetected: false,
|
|
@@ -220,16 +220,17 @@ export class KioskManager {
|
|
|
220
220
|
return null;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/** Check if
|
|
224
|
-
private
|
|
223
|
+
/** Check if the configured kiosk browser process is running */
|
|
224
|
+
private isBrowserRunning(): boolean {
|
|
225
225
|
try {
|
|
226
226
|
if (process.platform === 'win32') {
|
|
227
|
-
const
|
|
227
|
+
const browserExe = this.getWindowsBrowserExecutableName();
|
|
228
|
+
const result = execSync(`tasklist /FI "IMAGENAME eq ${browserExe}" /NH`, {
|
|
228
229
|
encoding: 'utf-8',
|
|
229
230
|
timeout: 5_000,
|
|
230
231
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
231
232
|
});
|
|
232
|
-
return result.toLowerCase().includes(
|
|
233
|
+
return result.toLowerCase().includes(browserExe.toLowerCase());
|
|
233
234
|
} else {
|
|
234
235
|
execSync('pgrep -x chrome || pgrep -x chromium-browser', {
|
|
235
236
|
stdio: 'ignore',
|
|
@@ -242,12 +243,13 @@ export class KioskManager {
|
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
/** Get PID of main
|
|
246
|
-
private
|
|
246
|
+
/** Get PID of main kiosk browser process */
|
|
247
|
+
private getBrowserPid(): number | null {
|
|
247
248
|
try {
|
|
248
249
|
if (process.platform === 'win32') {
|
|
250
|
+
const browserExe = this.getWindowsBrowserExecutableName();
|
|
249
251
|
const result = execSync(
|
|
250
|
-
|
|
252
|
+
`wmic process where "name='${browserExe}' and CommandLine like '%--kiosk%'" get ProcessId /format:value`,
|
|
251
253
|
{ encoding: 'utf-8', timeout: 5_000, stdio: ['pipe', 'pipe', 'ignore'] }
|
|
252
254
|
);
|
|
253
255
|
const match = result.match(/ProcessId=(\d+)/);
|
|
@@ -259,6 +261,20 @@ export class KioskManager {
|
|
|
259
261
|
return null;
|
|
260
262
|
}
|
|
261
263
|
|
|
264
|
+
private getWindowsBrowserExecutableName(): string {
|
|
265
|
+
const browserPath = this.config.browserPath?.trim();
|
|
266
|
+
if (!browserPath) {
|
|
267
|
+
return 'chrome.exe';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const fileName = basename(browserPath).trim().toLowerCase();
|
|
271
|
+
if (!fileName) {
|
|
272
|
+
return 'chrome.exe';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return fileName.endsWith('.exe') ? fileName : `${fileName}.exe`;
|
|
276
|
+
}
|
|
277
|
+
|
|
262
278
|
// =====================================================================
|
|
263
279
|
// Standard Mode Methods (original behavior)
|
|
264
280
|
// =====================================================================
|
|
@@ -270,7 +286,7 @@ export class KioskManager {
|
|
|
270
286
|
}
|
|
271
287
|
|
|
272
288
|
// Kill any leftover Chrome kiosk instances
|
|
273
|
-
this.
|
|
289
|
+
this.killAllBrowser();
|
|
274
290
|
|
|
275
291
|
// Delay to let Chrome fully release profile lock
|
|
276
292
|
await new Promise((r) => setTimeout(r, 2_000));
|
|
@@ -315,14 +331,15 @@ export class KioskManager {
|
|
|
315
331
|
return this.getStatus();
|
|
316
332
|
}
|
|
317
333
|
|
|
318
|
-
private
|
|
334
|
+
private killAllBrowser(): void {
|
|
319
335
|
try {
|
|
320
336
|
if (process.platform === 'win32') {
|
|
337
|
+
const browserExe = this.getWindowsBrowserExecutableName();
|
|
321
338
|
try {
|
|
322
|
-
execSync(
|
|
323
|
-
this.logger.info(
|
|
339
|
+
execSync(`taskkill /IM ${browserExe} /F`, { stdio: 'ignore', timeout: 5_000 });
|
|
340
|
+
this.logger.info(`Killed kiosk browser instances (${browserExe})`);
|
|
324
341
|
} catch {
|
|
325
|
-
// No
|
|
342
|
+
// No browser running, that's fine
|
|
326
343
|
}
|
|
327
344
|
} else {
|
|
328
345
|
try {
|