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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { DetectedScreen } from './screens.js';
|
|
2
|
+
import type { ScreenMapping } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type ScreenMapMode = 'none' | 'explicit' | 'explicit+autofill' | 'auto';
|
|
5
|
+
|
|
6
|
+
export interface ResolveScreenMapInput {
|
|
7
|
+
requestedScreenMap: ScreenMapping[];
|
|
8
|
+
detectedScreens: DetectedScreen[];
|
|
9
|
+
totalScreens?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ResolveScreenMapResult {
|
|
13
|
+
screenMap: ScreenMapping[];
|
|
14
|
+
mode: ScreenMapMode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveScreenMap(input: ResolveScreenMapInput): ResolveScreenMapResult {
|
|
18
|
+
const requested = (input.requestedScreenMap || []).map((m) => ({
|
|
19
|
+
hardwareId: String(m.hardwareId || '').trim(),
|
|
20
|
+
url: String(m.url || ''),
|
|
21
|
+
label: m.label,
|
|
22
|
+
}));
|
|
23
|
+
const detected = [...(input.detectedScreens || [])].sort((a, b) => a.index - b.index);
|
|
24
|
+
const targetCount = Math.max(requested.length, normalizePositiveInt(input.totalScreens));
|
|
25
|
+
|
|
26
|
+
if (targetCount === 0) {
|
|
27
|
+
return { screenMap: [], mode: 'none' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const explicitByIndex = new Map<number, string>();
|
|
31
|
+
const reservedForExplicit = new Set<string>();
|
|
32
|
+
for (let idx = 0; idx < targetCount; idx++) {
|
|
33
|
+
const requestedId = requested[idx]?.hardwareId || '';
|
|
34
|
+
if (!requestedId) continue;
|
|
35
|
+
const resolved = resolveDetectedScreen(requestedId, detected);
|
|
36
|
+
if (!resolved) continue;
|
|
37
|
+
const key = toKey(resolved.hardwareId);
|
|
38
|
+
if (reservedForExplicit.has(key)) continue;
|
|
39
|
+
explicitByIndex.set(idx, resolved.hardwareId);
|
|
40
|
+
reservedForExplicit.add(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const used = new Set<string>();
|
|
44
|
+
const screenMap: ScreenMapping[] = [];
|
|
45
|
+
let explicitEntries = 0;
|
|
46
|
+
let autofilledEntries = 0;
|
|
47
|
+
|
|
48
|
+
for (let idx = 0; idx < targetCount; idx++) {
|
|
49
|
+
const requestedEntry = requested[idx];
|
|
50
|
+
const requestedId = requestedEntry?.hardwareId || '';
|
|
51
|
+
const requestedUrl = requestedEntry?.url || '';
|
|
52
|
+
const requestedLabel = requestedEntry?.label;
|
|
53
|
+
|
|
54
|
+
let finalHardwareId = requestedId;
|
|
55
|
+
const explicitResolved = explicitByIndex.get(idx);
|
|
56
|
+
if (explicitResolved) {
|
|
57
|
+
explicitEntries++;
|
|
58
|
+
finalHardwareId = explicitResolved;
|
|
59
|
+
} else if (requestedId) {
|
|
60
|
+
explicitEntries++;
|
|
61
|
+
const fallbackPreferred = detected.find((s) => (
|
|
62
|
+
!used.has(toKey(s.hardwareId)) && !reservedForExplicit.has(toKey(s.hardwareId))
|
|
63
|
+
));
|
|
64
|
+
const fallbackAny = fallbackPreferred || detected.find((s) => !used.has(toKey(s.hardwareId)));
|
|
65
|
+
if (fallbackAny) {
|
|
66
|
+
finalHardwareId = fallbackAny.hardwareId;
|
|
67
|
+
autofilledEntries++;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
const fallbackPreferred = detected.find((s) => (
|
|
71
|
+
!used.has(toKey(s.hardwareId)) && !reservedForExplicit.has(toKey(s.hardwareId))
|
|
72
|
+
));
|
|
73
|
+
const fallbackAny = fallbackPreferred || detected.find((s) => !used.has(toKey(s.hardwareId)));
|
|
74
|
+
if (fallbackAny) {
|
|
75
|
+
finalHardwareId = fallbackAny.hardwareId;
|
|
76
|
+
autofilledEntries++;
|
|
77
|
+
} else {
|
|
78
|
+
finalHardwareId = '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (finalHardwareId) {
|
|
83
|
+
used.add(toKey(finalHardwareId));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const entry: ScreenMapping = {
|
|
87
|
+
hardwareId: finalHardwareId,
|
|
88
|
+
url: requestedUrl,
|
|
89
|
+
};
|
|
90
|
+
if (requestedLabel) {
|
|
91
|
+
entry.label = requestedLabel;
|
|
92
|
+
}
|
|
93
|
+
screenMap.push(entry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
screenMap,
|
|
98
|
+
mode: inferMode(explicitEntries, autofilledEntries),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveDetectedScreen(id: string, detectedScreens: DetectedScreen[]): DetectedScreen | undefined {
|
|
103
|
+
const requested = String(id || '').trim();
|
|
104
|
+
if (!requested) return undefined;
|
|
105
|
+
|
|
106
|
+
const direct = detectedScreens.find((s) => toKey(s.hardwareId) === toKey(requested));
|
|
107
|
+
if (direct) return direct;
|
|
108
|
+
|
|
109
|
+
if (/^\d+$/.test(requested)) {
|
|
110
|
+
const suffix = 'DISPLAY' + requested;
|
|
111
|
+
return detectedScreens.find((s) => {
|
|
112
|
+
const hw = s.hardwareId.toUpperCase();
|
|
113
|
+
return hw.endsWith(suffix) && (hw.length === suffix.length || hw[hw.length - suffix.length - 1] === '\\');
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizePositiveInt(value: number | undefined): number {
|
|
121
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return 0;
|
|
122
|
+
const n = Math.floor(value);
|
|
123
|
+
return n > 0 ? n : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function inferMode(explicitEntries: number, autofilledEntries: number): ScreenMapMode {
|
|
127
|
+
if (explicitEntries === 0 && autofilledEntries === 0) return 'none';
|
|
128
|
+
if (explicitEntries === 0 && autofilledEntries > 0) return 'auto';
|
|
129
|
+
if (autofilledEntries > 0) return 'explicit+autofill';
|
|
130
|
+
return 'explicit';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toKey(hardwareId: string): string {
|
|
134
|
+
return hardwareId.toUpperCase();
|
|
135
|
+
}
|
package/src/lib/screens.ts
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { getPlatform } from './platform.js';
|
|
3
|
-
import type { Logger } from './logger.js';
|
|
4
|
-
|
|
5
|
-
export interface DetectedScreen {
|
|
6
|
-
/** Windows display device ID, e.g. "\\.\DISPLAY1" */
|
|
7
|
-
hardwareId: string;
|
|
8
|
-
/** Friendly name / adapter description */
|
|
9
|
-
name: string;
|
|
10
|
-
/** Display index (0-based) */
|
|
11
|
-
index: number;
|
|
12
|
-
/** Screen bounds */
|
|
13
|
-
x: number;
|
|
14
|
-
y: number;
|
|
15
|
-
width: number;
|
|
16
|
-
height: number;
|
|
17
|
-
/** Whether this is the primary monitor */
|
|
18
|
-
primary: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Detect all connected displays on this machine.
|
|
23
|
-
* Windows: uses PowerShell + CIM to get monitor positions and IDs.
|
|
24
|
-
* Linux/macOS: uses xrandr / system_profiler (basic fallback).
|
|
25
|
-
*/
|
|
26
|
-
export function detectScreens(logger: Logger): DetectedScreen[] {
|
|
27
|
-
const platform = getPlatform();
|
|
28
|
-
|
|
29
|
-
if (platform === 'windows') {
|
|
30
|
-
return detectScreensWindows(logger);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (platform === 'linux') {
|
|
34
|
-
return detectScreensLinux(logger);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
logger.warn('[Screens] Screen detection not supported on this platform');
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function detectScreensWindows(logger: Logger): DetectedScreen[] {
|
|
42
|
-
try {
|
|
43
|
-
// PowerShell script — use regular string to avoid JS template literal eating $variables
|
|
44
|
-
const psScript = [
|
|
45
|
-
'Add-Type -AssemblyName System.Windows.Forms;',
|
|
46
|
-
'$screens = [System.Windows.Forms.Screen]::AllScreens;',
|
|
47
|
-
'$result = @();',
|
|
48
|
-
'$i = 0;',
|
|
49
|
-
'foreach ($s in $screens) {',
|
|
50
|
-
' $result += [PSCustomObject]@{',
|
|
51
|
-
' hardwareId = $s.DeviceName;',
|
|
52
|
-
' name = $s.DeviceName;',
|
|
53
|
-
' index = $i;',
|
|
54
|
-
' x = $s.Bounds.X;',
|
|
55
|
-
' y = $s.Bounds.Y;',
|
|
56
|
-
' width = $s.Bounds.Width;',
|
|
57
|
-
' height = $s.Bounds.Height;',
|
|
58
|
-
' primary = $s.Primary',
|
|
59
|
-
' };',
|
|
60
|
-
' $i++',
|
|
61
|
-
'};',
|
|
62
|
-
'$result | ConvertTo-Json -Compress',
|
|
63
|
-
].join(' ');
|
|
64
|
-
|
|
65
|
-
const result = execSync('powershell -NoProfile -Command "' + psScript + '"', {
|
|
66
|
-
encoding: 'utf-8',
|
|
67
|
-
timeout: 10_000,
|
|
68
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
69
|
-
}).trim();
|
|
70
|
-
|
|
71
|
-
if (!result) {
|
|
72
|
-
logger.warn('[Screens] PowerShell returned empty result');
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// PowerShell returns a single object (not array) when there's only 1 screen
|
|
77
|
-
const parsed = JSON.parse(result);
|
|
78
|
-
const screens: DetectedScreen[] = Array.isArray(parsed) ? parsed : [parsed];
|
|
79
|
-
|
|
80
|
-
logger.info(`[Screens] Detected ${screens.length} display(s)`);
|
|
81
|
-
for (const s of screens) {
|
|
82
|
-
logger.debug(`[Screens] ${s.hardwareId} — ${s.width}x${s.height} @ (${s.x},${s.y})${s.primary ? ' [PRIMARY]' : ''}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return screens;
|
|
86
|
-
} catch (err) {
|
|
87
|
-
logger.error('[Screens] Failed to detect screens on Windows:', err instanceof Error ? err.message : String(err));
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function detectScreensLinux(logger: Logger): DetectedScreen[] {
|
|
93
|
-
try {
|
|
94
|
-
const result = execSync('xrandr --query', {
|
|
95
|
-
encoding: 'utf-8',
|
|
96
|
-
timeout: 5_000,
|
|
97
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const screens: DetectedScreen[] = [];
|
|
101
|
-
const lines = result.split('\n');
|
|
102
|
-
let index = 0;
|
|
103
|
-
|
|
104
|
-
for (const line of lines) {
|
|
105
|
-
// Match lines like: "HDMI-1 connected primary 1920x1080+0+0"
|
|
106
|
-
const match = line.match(/^(\S+)\s+connected\s+(primary\s+)?(\d+)x(\d+)\+(\d+)\+(\d+)/);
|
|
107
|
-
if (match) {
|
|
108
|
-
screens.push({
|
|
109
|
-
hardwareId: match[1],
|
|
110
|
-
name: match[1],
|
|
111
|
-
index,
|
|
112
|
-
x: parseInt(match[5], 10),
|
|
113
|
-
y: parseInt(match[6], 10),
|
|
114
|
-
width: parseInt(match[3], 10),
|
|
115
|
-
height: parseInt(match[4], 10),
|
|
116
|
-
primary: !!match[2],
|
|
117
|
-
});
|
|
118
|
-
index++;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
logger.info(`[Screens] Detected ${screens.length} display(s) via xrandr`);
|
|
123
|
-
return screens;
|
|
124
|
-
} catch (err) {
|
|
125
|
-
logger.error('[Screens] Failed to detect screens on Linux:', err instanceof Error ? err.message : String(err));
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
}
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { getPlatform } from './platform.js';
|
|
3
|
+
import type { Logger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
export interface DetectedScreen {
|
|
6
|
+
/** Windows display device ID, e.g. "\\.\DISPLAY1" */
|
|
7
|
+
hardwareId: string;
|
|
8
|
+
/** Friendly name / adapter description */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Display index (0-based) */
|
|
11
|
+
index: number;
|
|
12
|
+
/** Screen bounds */
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
/** Whether this is the primary monitor */
|
|
18
|
+
primary: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect all connected displays on this machine.
|
|
23
|
+
* Windows: uses PowerShell + CIM to get monitor positions and IDs.
|
|
24
|
+
* Linux/macOS: uses xrandr / system_profiler (basic fallback).
|
|
25
|
+
*/
|
|
26
|
+
export function detectScreens(logger: Logger): DetectedScreen[] {
|
|
27
|
+
const platform = getPlatform();
|
|
28
|
+
|
|
29
|
+
if (platform === 'windows') {
|
|
30
|
+
return detectScreensWindows(logger);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (platform === 'linux') {
|
|
34
|
+
return detectScreensLinux(logger);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.warn('[Screens] Screen detection not supported on this platform');
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function detectScreensWindows(logger: Logger): DetectedScreen[] {
|
|
42
|
+
try {
|
|
43
|
+
// PowerShell script — use regular string to avoid JS template literal eating $variables
|
|
44
|
+
const psScript = [
|
|
45
|
+
'Add-Type -AssemblyName System.Windows.Forms;',
|
|
46
|
+
'$screens = [System.Windows.Forms.Screen]::AllScreens;',
|
|
47
|
+
'$result = @();',
|
|
48
|
+
'$i = 0;',
|
|
49
|
+
'foreach ($s in $screens) {',
|
|
50
|
+
' $result += [PSCustomObject]@{',
|
|
51
|
+
' hardwareId = $s.DeviceName;',
|
|
52
|
+
' name = $s.DeviceName;',
|
|
53
|
+
' index = $i;',
|
|
54
|
+
' x = $s.Bounds.X;',
|
|
55
|
+
' y = $s.Bounds.Y;',
|
|
56
|
+
' width = $s.Bounds.Width;',
|
|
57
|
+
' height = $s.Bounds.Height;',
|
|
58
|
+
' primary = $s.Primary',
|
|
59
|
+
' };',
|
|
60
|
+
' $i++',
|
|
61
|
+
'};',
|
|
62
|
+
'$result | ConvertTo-Json -Compress',
|
|
63
|
+
].join(' ');
|
|
64
|
+
|
|
65
|
+
const result = execSync('powershell -NoProfile -Command "' + psScript + '"', {
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
timeout: 10_000,
|
|
68
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
69
|
+
}).trim();
|
|
70
|
+
|
|
71
|
+
if (!result) {
|
|
72
|
+
logger.warn('[Screens] PowerShell returned empty result');
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// PowerShell returns a single object (not array) when there's only 1 screen
|
|
77
|
+
const parsed = JSON.parse(result);
|
|
78
|
+
const screens: DetectedScreen[] = Array.isArray(parsed) ? parsed : [parsed];
|
|
79
|
+
|
|
80
|
+
logger.info(`[Screens] Detected ${screens.length} display(s)`);
|
|
81
|
+
for (const s of screens) {
|
|
82
|
+
logger.debug(`[Screens] ${s.hardwareId} — ${s.width}x${s.height} @ (${s.x},${s.y})${s.primary ? ' [PRIMARY]' : ''}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return screens;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
logger.error('[Screens] Failed to detect screens on Windows:', err instanceof Error ? err.message : String(err));
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function detectScreensLinux(logger: Logger): DetectedScreen[] {
|
|
93
|
+
try {
|
|
94
|
+
const result = execSync('xrandr --query', {
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
timeout: 5_000,
|
|
97
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const screens: DetectedScreen[] = [];
|
|
101
|
+
const lines = result.split('\n');
|
|
102
|
+
let index = 0;
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
// Match lines like: "HDMI-1 connected primary 1920x1080+0+0"
|
|
106
|
+
const match = line.match(/^(\S+)\s+connected\s+(primary\s+)?(\d+)x(\d+)\+(\d+)\+(\d+)/);
|
|
107
|
+
if (match) {
|
|
108
|
+
screens.push({
|
|
109
|
+
hardwareId: match[1],
|
|
110
|
+
name: match[1],
|
|
111
|
+
index,
|
|
112
|
+
x: parseInt(match[5], 10),
|
|
113
|
+
y: parseInt(match[6], 10),
|
|
114
|
+
width: parseInt(match[3], 10),
|
|
115
|
+
height: parseInt(match[4], 10),
|
|
116
|
+
primary: !!match[2],
|
|
117
|
+
});
|
|
118
|
+
index++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.info(`[Screens] Detected ${screens.length} display(s) via xrandr`);
|
|
123
|
+
return screens;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.error('[Screens] Failed to detect screens on Linux:', err instanceof Error ? err.message : String(err));
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|