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 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 server = opts.server || DEFAULT_SERVER;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightman-agent",
3
- "version": "1.0.8",
3
+ "version": "1.0.11",
4
4
  "description": "LIGHTMAN Agent - System-level daemon for museum display machines",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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 Chrome kiosk
61
- $chrome = Get-Process -Name "chrome" -ErrorAction SilentlyContinue
62
- if (-not $chrome) {
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
- $chromeRecheck = Get-Process -Name "chrome" -ErrorAction SilentlyContinue
67
- if (-not $chromeRecheck) {
68
- Write-GuardianLog "Chrome not running. Launching via VBS..."
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 Chrome..." -ForegroundColor Yellow
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
@@ -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 Chrome is already running (agent's KioskManager may have launched it)
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 = 'chrome.exe'")
45
+ Set colProcesses = objWMI.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = '" & browserExeName & "'")
41
46
  If colProcesses.Count > 0 Then
42
- ' Chrome already running - agent is handling it. Exit gracefully.
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 Chrome appeared (agent launched it)
60
- Set colProcesses2 = objWMI.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = 'chrome.exe'")
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 Chrome successfully. Exit.
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 Chrome yet - we'll do it
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: Chrome not found! >> "%LOG_FILE%"
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 Chrome: %URL% >> "%LOG_FILE%"
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%] Chrome exited (code: %errorlevel%). Restarting in 3s... >> "%LOG_FILE%"
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
- $BrowserPath = "C:\Program Files\Google\Chrome\Application\chrome.exe"
59
- if (-not (Test-Path $BrowserPath)) {
60
- $BrowserPath = "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
61
- }
62
- if (-not (Test-Path $BrowserPath)) {
63
- $BrowserPath = "chromium-browser"
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: 'chromium-browser',
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('chromium-browser'),
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),
@@ -1,6 +1,6 @@
1
1
  import { spawn, execSync } from 'child_process';
2
2
  import { writeFileSync, readFileSync, existsSync } from 'fs';
3
- import { resolve } from 'path';
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.killAllChrome();
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.killAllChrome();
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.killAllChrome();
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.isChromeRunning()) {
180
- this.logger.info('Shell mode: Chrome already running. Restarting once to apply sidecar URL.');
181
- this.killAllChrome();
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: Chrome not running. Shell BAT will launch it.');
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.isChromeRunning();
191
+ const running = this.isBrowserRunning();
192
192
  return {
193
193
  running,
194
- pid: running ? this.getChromePid() : null,
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 any chrome.exe process is running */
224
- private isChromeRunning(): boolean {
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 result = execSync('tasklist /FI "IMAGENAME eq chrome.exe" /NH', {
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('chrome.exe');
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 Chrome process */
246
- private getChromePid(): number | null {
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
- 'wmic process where "name=\'chrome.exe\' and CommandLine like \'%--kiosk%\'" get ProcessId /format:value',
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.killAllChrome();
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 killAllChrome(): void {
334
+ private killAllBrowser(): void {
319
335
  try {
320
336
  if (process.platform === 'win32') {
337
+ const browserExe = this.getWindowsBrowserExecutableName();
321
338
  try {
322
- execSync('taskkill /IM chrome.exe /F', { stdio: 'ignore', timeout: 5_000 });
323
- this.logger.info('Killed Chrome instances');
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 Chrome running, that's fine
342
+ // No browser running, that's fine
326
343
  }
327
344
  } else {
328
345
  try {