lightman-agent 1.0.18 → 1.0.21
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/agent.config.json +22 -23
- package/agent.config.template.json +30 -31
- package/bin/cms-agent.js +269 -248
- package/package.json +1 -1
- package/public/assets/index-CcBNCz6h.css +1 -1
- package/public/assets/index-D9QHMG8k.js +1 -1
- package/public/assets/index-H-8HDl46.js +1 -1
- package/public/assets/index-YodeiCia.css +1 -1
- package/public/assets/index-legacy-DWtNM8y7.js +41 -41
- package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -4
- package/scripts/guardian.ps1 +50 -124
- package/scripts/install-windows.ps1 +60 -116
- package/scripts/lightman-agent.logrotate +12 -12
- package/scripts/lightman-agent.service +38 -38
- package/scripts/reinstall-windows.ps1 +26 -26
- package/scripts/restore-desktop.ps1 +32 -32
- package/scripts/setup.ps1 +17 -22
- package/scripts/sync-display.mjs +20 -20
- package/scripts/uninstall-windows.ps1 +54 -54
- package/src/commands/display.ts +177 -177
- package/src/commands/kiosk.ts +113 -113
- package/src/commands/maintenance.ts +106 -106
- package/src/commands/network.ts +129 -129
- package/src/commands/power.ts +163 -163
- package/src/commands/rpi.ts +45 -45
- package/src/commands/screenshot.ts +166 -166
- package/src/commands/serial.ts +17 -17
- package/src/commands/update.ts +124 -124
- package/src/index.ts +173 -90
- package/src/lib/config.ts +2 -3
- package/src/lib/identity.ts +40 -40
- package/src/lib/logger.ts +137 -137
- package/src/lib/platform.ts +10 -10
- package/src/lib/rpi.ts +180 -180
- package/src/lib/screenMap.ts +135 -0
- package/src/lib/screens.ts +128 -128
- package/src/lib/types.ts +176 -177
- package/src/services/commands.ts +107 -107
- package/src/services/health.ts +161 -161
- package/src/services/localEvents.ts +60 -60
- package/src/services/logForwarder.ts +72 -72
- package/src/services/multiScreenKiosk.ts +116 -83
- package/src/services/oscBridge.ts +186 -186
- package/src/services/powerScheduler.ts +260 -260
- package/src/services/provisioning.ts +120 -122
- package/src/services/serialBridge.ts +230 -230
- package/src/services/serviceLauncher.ts +183 -183
- package/src/services/staticServer.ts +226 -226
- package/src/services/updater.ts +249 -249
- package/src/services/watchdog.ts +310 -310
- package/src/services/websocket.ts +152 -152
- package/tsconfig.json +28 -28
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
2
|
import type { ChildProcess } from 'child_process';
|
|
3
3
|
import type { KioskConfig, ScreenMapping, MultiScreenKioskStatus, SingleScreenStatus } from '../lib/types.js';
|
|
4
4
|
import type { DetectedScreen } from '../lib/screens.js';
|
|
5
5
|
import type { Logger } from '../lib/logger.js';
|
|
6
|
+
import { resolveDetectedScreen, resolveScreenMap } from '../lib/screenMap.js';
|
|
6
7
|
|
|
7
8
|
interface ScreenInstance {
|
|
8
9
|
hardwareId: string;
|
|
9
|
-
mappingId: string;
|
|
10
|
-
mappingUrl: string;
|
|
11
|
-
screenIndex: number;
|
|
12
|
-
url: string;
|
|
10
|
+
mappingId: string;
|
|
11
|
+
mappingUrl: string;
|
|
12
|
+
screenIndex: number;
|
|
13
|
+
url: string;
|
|
13
14
|
screen: DetectedScreen;
|
|
14
15
|
process: ChildProcess | null;
|
|
15
16
|
startedAt: number | null;
|
|
@@ -17,14 +18,15 @@ interface ScreenInstance {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* Manages multiple Chrome kiosk instances
|
|
21
|
-
* Each Chrome gets its own --user-data-dir and --window-position to target the correct screen.
|
|
21
|
+
* Manages multiple Chrome kiosk instances, one per physical display.
|
|
22
22
|
*/
|
|
23
23
|
export class MultiScreenKioskManager {
|
|
24
24
|
private config: KioskConfig;
|
|
25
25
|
private logger: Logger;
|
|
26
26
|
private instances: Map<string, ScreenInstance> = new Map();
|
|
27
27
|
private detectedScreens: DetectedScreen[] = [];
|
|
28
|
+
private desiredScreenMap: ScreenMapping[] = [];
|
|
29
|
+
private desiredIdentity: { deviceId: string; apiKey: string } | null = null;
|
|
28
30
|
private pollTimer: NodeJS.Timeout | null = null;
|
|
29
31
|
private applying = false;
|
|
30
32
|
|
|
@@ -45,11 +47,14 @@ export class MultiScreenKioskManager {
|
|
|
45
47
|
* and relaunches those whose URL changed.
|
|
46
48
|
*/
|
|
47
49
|
async applyScreenMap(screenMap: ScreenMapping[], identity: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
48
|
-
|
|
50
|
+
this.desiredScreenMap = screenMap.map((m) => ({ ...m }));
|
|
51
|
+
this.desiredIdentity = { ...identity };
|
|
52
|
+
|
|
49
53
|
if (this.applying) {
|
|
50
54
|
this.logger.warn('[MultiKiosk] applyScreenMap already in progress, skipping');
|
|
51
55
|
return this.getStatus();
|
|
52
56
|
}
|
|
57
|
+
|
|
53
58
|
this.applying = true;
|
|
54
59
|
try {
|
|
55
60
|
return await this._applyScreenMap(screenMap, identity);
|
|
@@ -59,47 +64,66 @@ export class MultiScreenKioskManager {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
private async _applyScreenMap(screenMap: ScreenMapping[], identity: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
62
|
-
|
|
67
|
+
const resolvedMap = resolveScreenMap({
|
|
68
|
+
requestedScreenMap: screenMap,
|
|
69
|
+
detectedScreens: this.detectedScreens,
|
|
70
|
+
totalScreens: screenMap.length,
|
|
71
|
+
});
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
this.logger.info(
|
|
74
|
+
`[MultiKiosk] Applying screen map: ${resolvedMap.screenMap.length} mapping(s), mode=${resolvedMap.mode}`
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const resolvedEntries: Array<{ mapping: ScreenMapping; screen: DetectedScreen; index: number }> = [];
|
|
78
|
+
const resolvedHardwareIds = new Set<string>();
|
|
79
|
+
|
|
80
|
+
for (let idx = 0; idx < resolvedMap.screenMap.length; idx++) {
|
|
81
|
+
const mapping = resolvedMap.screenMap[idx];
|
|
82
|
+
if (!mapping.hardwareId) {
|
|
83
|
+
this.logger.warn(`[MultiKiosk] Mapping index ${idx} has no hardwareId, skipping`);
|
|
84
|
+
continue;
|
|
71
85
|
}
|
|
72
|
-
}
|
|
73
86
|
|
|
74
|
-
// Launch or relaunch for each mapping
|
|
75
|
-
for (let idx = 0; idx < screenMap.length; idx++) {
|
|
76
|
-
const mapping = screenMap[idx];
|
|
77
|
-
// Match by display number: admin saves "1","2","3" — agent detects "\\.\DISPLAY1" etc.
|
|
78
87
|
const screen = this.findScreen(mapping.hardwareId);
|
|
79
88
|
if (!screen) {
|
|
80
89
|
this.logger.warn(`[MultiKiosk] Screen ${mapping.hardwareId} not detected, skipping`);
|
|
81
90
|
continue;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
if (resolvedHardwareIds.has(screen.hardwareId)) {
|
|
94
|
+
this.logger.warn(`[MultiKiosk] Screen ${screen.hardwareId} is assigned more than once, skipping duplicate`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
resolvedHardwareIds.add(screen.hardwareId);
|
|
99
|
+
resolvedEntries.push({ mapping, screen, index: idx });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const mappedIds = new Set(resolvedEntries.map((e) => e.screen.hardwareId));
|
|
103
|
+
for (const [hwId, instance] of this.instances) {
|
|
104
|
+
if (!mappedIds.has(hwId)) {
|
|
105
|
+
this.logger.info(`[MultiKiosk] Screen ${hwId} removed from map, killing Chrome`);
|
|
106
|
+
this.killInstance(instance);
|
|
107
|
+
this.instances.delete(hwId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const entry of resolvedEntries) {
|
|
112
|
+
const { mapping, screen, index } = entry;
|
|
86
113
|
const basePath = mapping.url || this.config.defaultUrl;
|
|
87
|
-
const url = this.buildUrl(basePath, identity,
|
|
114
|
+
const url = this.buildUrl(basePath, identity, index);
|
|
88
115
|
|
|
89
|
-
const existing = this.instances.get(
|
|
116
|
+
const existing = this.instances.get(screen.hardwareId);
|
|
90
117
|
if (existing && existing.url === url && existing.process && existing.process.exitCode === null) {
|
|
91
|
-
// Same URL, Chrome still running — skip
|
|
92
118
|
this.logger.debug(`[MultiKiosk] Screen ${mapping.hardwareId} unchanged, skipping`);
|
|
93
119
|
continue;
|
|
94
120
|
}
|
|
95
121
|
|
|
96
|
-
// Kill existing if URL changed
|
|
97
122
|
if (existing) {
|
|
98
123
|
this.killInstance(existing);
|
|
99
124
|
}
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
await this.launchOnScreen(screen.hardwareId, url, screen, identity, mapping.hardwareId, mapping.url || '', idx);
|
|
126
|
+
await this.launchOnScreen(screen.hardwareId, url, screen, identity, mapping.hardwareId, mapping.url || '', index);
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
this.startPoll();
|
|
@@ -116,11 +140,8 @@ export class MultiScreenKioskManager {
|
|
|
116
140
|
mappingUrl: string = '',
|
|
117
141
|
screenIndex: number = 0
|
|
118
142
|
): Promise<void> {
|
|
119
|
-
// Each screen gets its own user-data-dir to avoid profile lock conflicts
|
|
120
143
|
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
121
|
-
const basePath = process.platform === 'win32'
|
|
122
|
-
? 'C:\\ProgramData\\Lightman'
|
|
123
|
-
: '/tmp/lightman';
|
|
144
|
+
const basePath = process.platform === 'win32' ? 'C:\\ProgramData\\Lightman' : '/tmp/lightman';
|
|
124
145
|
const userDataDir = `${basePath}${sep}chrome-kiosk-screen-${screen.index}`;
|
|
125
146
|
|
|
126
147
|
const args = [
|
|
@@ -133,11 +154,13 @@ export class MultiScreenKioskManager {
|
|
|
133
154
|
`--window-position=${screen.x},${screen.y}`,
|
|
134
155
|
`--window-size=${screen.width},${screen.height}`,
|
|
135
156
|
`--user-data-dir=${userDataDir}`,
|
|
136
|
-
...this.config.extraArgs.filter(a => !a.startsWith('--user-data-dir')),
|
|
157
|
+
...this.config.extraArgs.filter((a) => !a.startsWith('--user-data-dir')),
|
|
137
158
|
url,
|
|
138
159
|
];
|
|
139
160
|
|
|
140
|
-
this.logger.info(
|
|
161
|
+
this.logger.info(
|
|
162
|
+
`[MultiKiosk] Launching Chrome on ${hardwareId} (${screen.width}x${screen.height} @ ${screen.x},${screen.y}): ${url}`
|
|
163
|
+
);
|
|
141
164
|
|
|
142
165
|
const proc = spawn(this.config.browserPath, args, {
|
|
143
166
|
stdio: 'ignore',
|
|
@@ -158,15 +181,15 @@ export class MultiScreenKioskManager {
|
|
|
158
181
|
};
|
|
159
182
|
|
|
160
183
|
proc.on('exit', (code) => {
|
|
161
|
-
// Only auto-restart if this instance is still current (not replaced by a newer applyScreenMap)
|
|
162
184
|
const current = this.instances.get(hardwareId);
|
|
163
185
|
if (!current || current.process !== proc) return;
|
|
186
|
+
|
|
164
187
|
this.logger.warn(`[MultiKiosk] Chrome on ${hardwareId} exited with code ${code}`);
|
|
165
188
|
setTimeout(() => {
|
|
166
189
|
const stillCurrent = this.instances.get(hardwareId);
|
|
167
190
|
if (stillCurrent && stillCurrent === instance) {
|
|
168
191
|
this.logger.info(`[MultiKiosk] Auto-restarting Chrome on ${hardwareId}`);
|
|
169
|
-
this.launchOnScreen(hardwareId, url, screen, identity, mappingId, mappingUrl, screenIndex).catch(err => {
|
|
192
|
+
this.launchOnScreen(hardwareId, url, screen, identity, mappingId, mappingUrl, screenIndex).catch((err) => {
|
|
170
193
|
this.logger.error(`[MultiKiosk] Failed to restart Chrome on ${hardwareId}:`, err);
|
|
171
194
|
});
|
|
172
195
|
}
|
|
@@ -182,12 +205,10 @@ export class MultiScreenKioskManager {
|
|
|
182
205
|
|
|
183
206
|
/** Build the full URL with device credentials and screenIndex */
|
|
184
207
|
private buildUrl(path: string, identity: { deviceId: string; apiKey: string }, screenIndex?: number): string {
|
|
185
|
-
// If path is already a full URL, use it; otherwise prepend the local static server
|
|
186
208
|
let fullUrl: string;
|
|
187
209
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
188
210
|
fullUrl = path;
|
|
189
211
|
} else {
|
|
190
|
-
// Ensure display URL format: /display/{slug}
|
|
191
212
|
const displayPath = path.startsWith('/display/') ? path : `/display/${path.replace(/^\//, '')}`;
|
|
192
213
|
fullUrl = `http://localhost:3403${displayPath}`;
|
|
193
214
|
}
|
|
@@ -198,73 +219,83 @@ export class MultiScreenKioskManager {
|
|
|
198
219
|
if (screenIndex !== undefined) {
|
|
199
220
|
url.searchParams.set('screenIndex', String(screenIndex));
|
|
200
221
|
}
|
|
222
|
+
|
|
201
223
|
return url.toString();
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
/** Navigate a single screen to a new URL */
|
|
205
227
|
async navigateScreen(hardwareId: string, url: string, identity: { deviceId: string; apiKey: string }): Promise<void> {
|
|
206
|
-
const
|
|
228
|
+
const resolvedHardwareId = resolveDetectedScreen(hardwareId, this.detectedScreens)?.hardwareId || hardwareId;
|
|
229
|
+
const existing = this.instances.get(resolvedHardwareId);
|
|
207
230
|
if (!existing) {
|
|
208
|
-
this.logger.warn(`[MultiKiosk] Cannot navigate ${hardwareId}
|
|
231
|
+
this.logger.warn(`[MultiKiosk] Cannot navigate ${hardwareId} - not in instance map`);
|
|
209
232
|
return;
|
|
210
233
|
}
|
|
211
234
|
|
|
212
235
|
this.killInstance(existing);
|
|
213
|
-
const fullUrl = this.buildUrl(url, identity);
|
|
214
|
-
await this.launchOnScreen(hardwareId, fullUrl, existing.screen, identity);
|
|
236
|
+
const fullUrl = this.buildUrl(url, identity, existing.screenIndex);
|
|
237
|
+
await this.launchOnScreen(existing.hardwareId, fullUrl, existing.screen, identity, existing.mappingId, url, existing.screenIndex);
|
|
215
238
|
}
|
|
216
239
|
|
|
217
240
|
/** Kill Chrome for a specific screen instance */
|
|
218
241
|
private killInstance(instance: ScreenInstance): void {
|
|
219
|
-
if (instance.process)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
instance.process = null;
|
|
242
|
+
if (!instance.process) return;
|
|
243
|
+
|
|
244
|
+
instance.process.removeAllListeners();
|
|
245
|
+
try {
|
|
246
|
+
instance.process.kill('SIGTERM');
|
|
247
|
+
} catch {
|
|
248
|
+
// Already dead.
|
|
227
249
|
}
|
|
250
|
+
instance.process = null;
|
|
228
251
|
}
|
|
229
252
|
|
|
230
253
|
/** Find a detected screen by display number or full hardware ID */
|
|
231
254
|
private findScreen(id: string): DetectedScreen | undefined {
|
|
232
|
-
|
|
233
|
-
const direct = this.detectedScreens.find(s => s.hardwareId === id);
|
|
234
|
-
if (direct) return direct;
|
|
235
|
-
|
|
236
|
-
// Match by display number ("1" → "\\.\DISPLAY1") — anchored to avoid "1" matching "DISPLAY11"
|
|
237
|
-
if (/^\d+$/.test(id)) {
|
|
238
|
-
const suffix = 'DISPLAY' + id;
|
|
239
|
-
return this.detectedScreens.find(s => {
|
|
240
|
-
const name = s.hardwareId.toUpperCase();
|
|
241
|
-
return name.endsWith(suffix) && (name.length === suffix.length || name[name.length - suffix.length - 1] === '\\');
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return undefined;
|
|
255
|
+
return resolveDetectedScreen(id, this.detectedScreens);
|
|
246
256
|
}
|
|
247
257
|
|
|
248
258
|
/** Kill all Chrome instances */
|
|
249
|
-
async killAll(): Promise<void> {
|
|
259
|
+
async killAll(options?: { clearDesired?: boolean }): Promise<void> {
|
|
250
260
|
for (const [, instance] of this.instances) {
|
|
251
261
|
this.killInstance(instance);
|
|
252
262
|
}
|
|
263
|
+
this.instances.clear();
|
|
253
264
|
this.stopPoll();
|
|
265
|
+
|
|
266
|
+
if (options?.clearDesired !== false) {
|
|
267
|
+
this.desiredScreenMap = [];
|
|
268
|
+
this.desiredIdentity = null;
|
|
269
|
+
}
|
|
254
270
|
}
|
|
255
271
|
|
|
256
272
|
/** Restart all Chrome instances */
|
|
257
273
|
async restartAll(identity: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
258
274
|
this.logger.info('[MultiKiosk] Restarting all Chrome instances');
|
|
275
|
+
|
|
259
276
|
const mappings: ScreenMapping[] = [];
|
|
260
277
|
for (const [, instance] of this.instances) {
|
|
261
|
-
mappings.push({ hardwareId: instance.mappingId, url: instance.mappingUrl });
|
|
278
|
+
mappings.push({ hardwareId: instance.mappingId, url: instance.mappingUrl, label: undefined });
|
|
262
279
|
}
|
|
263
|
-
|
|
264
|
-
await
|
|
280
|
+
|
|
281
|
+
await this.killAll({ clearDesired: false });
|
|
282
|
+
await new Promise((r) => setTimeout(r, 2_000));
|
|
265
283
|
return this.applyScreenMap(mappings, identity);
|
|
266
284
|
}
|
|
267
285
|
|
|
286
|
+
hasDesiredScreenMap(): boolean {
|
|
287
|
+
return this.desiredScreenMap.length > 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async reapplyDesiredMap(identity?: { deviceId: string; apiKey: string }): Promise<MultiScreenKioskStatus> {
|
|
291
|
+
const id = identity || this.desiredIdentity;
|
|
292
|
+
if (!id || this.desiredScreenMap.length === 0) {
|
|
293
|
+
return this.getStatus();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return this.applyScreenMap(this.desiredScreenMap, id);
|
|
297
|
+
}
|
|
298
|
+
|
|
268
299
|
/** Get status of all screen instances */
|
|
269
300
|
getStatus(): MultiScreenKioskStatus {
|
|
270
301
|
const screens: SingleScreenStatus[] = [];
|
|
@@ -278,6 +309,7 @@ export class MultiScreenKioskManager {
|
|
|
278
309
|
uptimeMs: running && instance.startedAt ? Date.now() - instance.startedAt : null,
|
|
279
310
|
});
|
|
280
311
|
}
|
|
312
|
+
|
|
281
313
|
return { screens };
|
|
282
314
|
}
|
|
283
315
|
|
|
@@ -290,35 +322,36 @@ export class MultiScreenKioskManager {
|
|
|
290
322
|
destroy(): void {
|
|
291
323
|
this.stopPoll();
|
|
292
324
|
for (const [, instance] of this.instances) {
|
|
293
|
-
if (instance.process)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
instance.process = null;
|
|
325
|
+
if (!instance.process) continue;
|
|
326
|
+
try {
|
|
327
|
+
instance.process.removeAllListeners();
|
|
328
|
+
instance.process.kill('SIGKILL');
|
|
329
|
+
} catch {
|
|
330
|
+
// Already dead.
|
|
301
331
|
}
|
|
332
|
+
instance.process = null;
|
|
302
333
|
}
|
|
303
334
|
this.instances.clear();
|
|
335
|
+
this.desiredScreenMap = [];
|
|
336
|
+
this.desiredIdentity = null;
|
|
304
337
|
}
|
|
305
338
|
|
|
306
339
|
private startPoll(): void {
|
|
307
340
|
if (this.pollTimer) return;
|
|
341
|
+
|
|
308
342
|
this.pollTimer = setInterval(() => {
|
|
309
343
|
for (const [, instance] of this.instances) {
|
|
310
344
|
if (instance.process && instance.process.exitCode !== null) {
|
|
311
345
|
this.logger.warn(`[MultiKiosk] Poll: Chrome on ${instance.hardwareId} died`);
|
|
312
|
-
//
|
|
346
|
+
// Exit handler will auto-restart.
|
|
313
347
|
}
|
|
314
348
|
}
|
|
315
349
|
}, this.config.pollIntervalMs);
|
|
316
350
|
}
|
|
317
351
|
|
|
318
352
|
private stopPoll(): void {
|
|
319
|
-
if (this.pollTimer)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
353
|
+
if (!this.pollTimer) return;
|
|
354
|
+
clearInterval(this.pollTimer);
|
|
355
|
+
this.pollTimer = null;
|
|
323
356
|
}
|
|
324
|
-
}
|
|
357
|
+
}
|