lightman-agent 1.0.5 → 1.0.6
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.template.json +30 -30
- package/package.json +52 -52
- package/public/assets/index-CcBNCz6h.css +1 -1
- package/public/assets/index-D9QHMG8k.js +1 -0
- package/public/assets/index-H-8HDl46.js +1 -1
- package/public/assets/index-YodeiCia.css +1 -0
- package/public/assets/index-legacy-DWtNM8y7.js +41 -0
- package/public/assets/museum-map-CwVDA2z1.svg +4182 -0
- package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -0
- package/public/index.html +7 -2
- package/public/templates/custom08/elements/back-button.svg +20 -0
- package/public/templates/custom08/elements/base-map-background.svg +37 -0
- package/public/templates/custom08/elements/base-map.svg +1191 -0
- package/public/templates/custom08/elements/gallery-1-2-3-info-panel.svg +236 -0
- package/public/templates/custom08/elements/gallery-4-5-6-7-info-panel.svg +266 -0
- package/public/templates/custom08/elements/gallery-8-9-info-panel.svg +274 -0
- package/public/templates/custom08/elements/gallery-labels/_nav-map-styles.css +554 -0
- package/public/templates/custom08/elements/gallery-labels/_styles.css +556 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-1.svg +35 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-2.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-3.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-4.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-5.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-6.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-7.svg +34 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-8.svg +37 -0
- package/public/templates/custom08/elements/gallery-labels/gallery-9.svg +34 -0
- package/public/templates/custom08/elements/hand-hint.png +0 -0
- package/public/templates/custom08/elements/idle-screen-bg.svg +5 -0
- package/public/templates/custom08/elements/idle-screen-map.svg +627 -0
- package/public/templates/custom08/elements/idle-screen-text.svg +350 -0
- package/public/templates/custom08/elements/key-map-1.svg +986 -0
- package/public/templates/custom08/elements/key-map-2.svg +1018 -0
- package/public/templates/custom08/elements/key-map-3.svg +1019 -0
- package/public/templates/custom08/elements/key-map-combined.svg +1001 -0
- package/public/templates/custom08/elements/map-highlight-marker.svg +11 -0
- package/public/templates/custom08/elements/map-pin-marker.svg +15 -0
- package/public/templates/custom08/elements/map-teardrop-star-marker.svg +13 -0
- package/public/templates/custom08/elements/nav-circle-galleries-1-3.svg +21 -0
- package/public/templates/custom08/elements/nav-circle-galleries-4-7.svg +24 -0
- package/public/templates/custom08/elements/nav-circle-galleries-8-9.svg +20 -0
- package/public/templates/custom08/elements/section1-map.svg +1435 -0
- package/public/templates/custom08/elements/section2-map.svg +1724 -0
- package/public/templates/custom08/elements/section3-map.svg +1295 -0
- package/public/templates/custom08/fonts/CabinetGrotesk-Variable.ttf +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.12_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.23.56_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-05_at_7.24.24_PM.png +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.31.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.36_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.48_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.32.59_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.15_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.27_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.34_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.42_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.50_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.33.58_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.04_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.11_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.20_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.34.57_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.03_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.16_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/Screenshot_2026-03-24_at_11.35.23_PM.jpg +0 -0
- package/public/templates/custom08/images/highlights/prologue-highlight.png +0 -0
- package/scripts/guardian.ps1 +75 -75
- package/scripts/install-linux.sh +134 -134
- package/scripts/install-rpi.sh +117 -117
- package/scripts/install-windows.ps1 +505 -505
- package/scripts/launch-kiosk.vbs +101 -101
- package/scripts/lightman-agent.logrotate +12 -12
- package/scripts/lightman-agent.service +38 -38
- package/scripts/lightman-shell.bat +107 -107
- package/scripts/reinstall-windows.ps1 +26 -26
- package/scripts/restore-desktop.ps1 +32 -32
- package/scripts/setup.ps1 +116 -116
- package/scripts/setup.sh +115 -115
- package/scripts/sync-display.mjs +20 -0
- package/scripts/uninstall-linux.sh +50 -50
- 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 +652 -652
- package/src/lib/config.ts +69 -69
- 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/screens.ts +128 -128
- package/src/lib/types.ts +176 -176
- package/src/services/commands.ts +107 -107
- package/src/services/health.ts +161 -161
- package/src/services/kiosk.ts +384 -384
- package/src/services/localEvents.ts +60 -60
- package/src/services/logForwarder.ts +72 -72
- package/src/services/multiScreenKiosk.ts +324 -324
- package/src/services/oscBridge.ts +186 -186
- package/src/services/powerScheduler.ts +260 -260
- package/src/services/provisioning.ts +120 -120
- 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
package/src/lib/rpi.ts
CHANGED
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import { existsSync, readFileSync, openSync, writeSync, closeSync } from 'fs';
|
|
3
|
-
|
|
4
|
-
// --- RPi Detection ---
|
|
5
|
-
|
|
6
|
-
let isRpiCached: boolean | null = null;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Detect if this machine is a Raspberry Pi by checking /proc/device-tree/model.
|
|
10
|
-
*/
|
|
11
|
-
export function isRaspberryPi(): boolean {
|
|
12
|
-
if (isRpiCached !== null) return isRpiCached;
|
|
13
|
-
try {
|
|
14
|
-
if (!existsSync('/proc/device-tree/model')) {
|
|
15
|
-
isRpiCached = false;
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
const model = readFileSync('/proc/device-tree/model', 'utf-8');
|
|
19
|
-
isRpiCached = model.toLowerCase().includes('raspberry pi');
|
|
20
|
-
return isRpiCached;
|
|
21
|
-
} catch {
|
|
22
|
-
isRpiCached = false;
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Reset detection cache (for testing). */
|
|
28
|
-
export function resetRpiCache(): void {
|
|
29
|
-
isRpiCached = null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// --- RPi Model Info ---
|
|
33
|
-
|
|
34
|
-
export interface RpiInfo {
|
|
35
|
-
model: string | null;
|
|
36
|
-
serial: string | null;
|
|
37
|
-
revision: string | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getRpiInfo(): RpiInfo {
|
|
41
|
-
const info: RpiInfo = { model: null, serial: null, revision: null };
|
|
42
|
-
try {
|
|
43
|
-
info.model = readFileSync('/proc/device-tree/model', 'utf-8').replace(/\0/g, '').trim();
|
|
44
|
-
} catch { /* not available */ }
|
|
45
|
-
try {
|
|
46
|
-
const cpuinfo = readFileSync('/proc/cpuinfo', 'utf-8');
|
|
47
|
-
const serialMatch = cpuinfo.match(/Serial\s*:\s*([0-9a-fA-F]+)/);
|
|
48
|
-
if (serialMatch) info.serial = serialMatch[1];
|
|
49
|
-
const revisionMatch = cpuinfo.match(/Revision\s*:\s*([0-9a-fA-F]+)/);
|
|
50
|
-
if (revisionMatch) info.revision = revisionMatch[1];
|
|
51
|
-
} catch { /* not available */ }
|
|
52
|
-
return info;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// --- GPU Temperature ---
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Read GPU temperature via vcgencmd. Returns null if not available.
|
|
59
|
-
*/
|
|
60
|
-
export function getGpuTemp(): number | null {
|
|
61
|
-
try {
|
|
62
|
-
const output = execFileSync('vcgencmd', ['measure_temp'], {
|
|
63
|
-
timeout: 3000,
|
|
64
|
-
stdio: 'pipe',
|
|
65
|
-
}).toString();
|
|
66
|
-
// Output format: temp=42.8'C
|
|
67
|
-
const match = output.match(/temp=([\d.]+)/);
|
|
68
|
-
if (match) return Math.round(parseFloat(match[1]) * 10) / 10;
|
|
69
|
-
return null;
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// --- Throttle Status ---
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Read throttle/undervoltage status via vcgencmd. Returns null if not available.
|
|
79
|
-
* See: https://www.raspberrypi.com/documentation/computers/os.html#get_throttled
|
|
80
|
-
*
|
|
81
|
-
* Bit meanings:
|
|
82
|
-
* 0: Under-voltage detected
|
|
83
|
-
* 1: Arm frequency capped
|
|
84
|
-
* 2: Currently throttled
|
|
85
|
-
* 3: Soft temperature limit active
|
|
86
|
-
* 16: Under-voltage has occurred
|
|
87
|
-
* 17: Arm frequency capping has occurred
|
|
88
|
-
* 18: Throttling has occurred
|
|
89
|
-
* 19: Soft temperature limit has occurred
|
|
90
|
-
*/
|
|
91
|
-
export function getThrottled(): number | null {
|
|
92
|
-
try {
|
|
93
|
-
const output = execFileSync('vcgencmd', ['get_throttled'], {
|
|
94
|
-
timeout: 3000,
|
|
95
|
-
stdio: 'pipe',
|
|
96
|
-
}).toString();
|
|
97
|
-
// Output format: throttled=0x0
|
|
98
|
-
const match = output.match(/throttled=(0x[0-9a-fA-F]+)/);
|
|
99
|
-
if (match) return parseInt(match[1], 16);
|
|
100
|
-
return null;
|
|
101
|
-
} catch {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// --- SD Card Read-Only Check ---
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Check if the root filesystem is mounted read-only.
|
|
110
|
-
*/
|
|
111
|
-
export function isSdCardReadOnly(): boolean {
|
|
112
|
-
try {
|
|
113
|
-
const mounts = readFileSync('/proc/mounts', 'utf-8');
|
|
114
|
-
const rootLine = mounts.split('\n').find((line) => {
|
|
115
|
-
const parts = line.split(' ');
|
|
116
|
-
return parts[1] === '/';
|
|
117
|
-
});
|
|
118
|
-
if (!rootLine) return false;
|
|
119
|
-
const options = rootLine.split(' ')[3] || '';
|
|
120
|
-
return options.split(',').includes('ro');
|
|
121
|
-
} catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// --- Hardware Watchdog ---
|
|
127
|
-
|
|
128
|
-
let watchdogFd: number | null = null;
|
|
129
|
-
let watchdogTimer: NodeJS.Timeout | null = null;
|
|
130
|
-
const WATCHDOG_DEVICE = '/dev/watchdog';
|
|
131
|
-
const WATCHDOG_INTERVAL_MS = 10_000; // Pet every 10 seconds
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Start the hardware watchdog. Writes to /dev/watchdog every 10s.
|
|
135
|
-
* If the agent dies, the kernel will reboot after ~15s.
|
|
136
|
-
* Returns true if started, false if not available.
|
|
137
|
-
*/
|
|
138
|
-
export function startWatchdog(): boolean {
|
|
139
|
-
if (watchdogFd !== null) return true; // Already running
|
|
140
|
-
try {
|
|
141
|
-
if (!existsSync(WATCHDOG_DEVICE)) return false;
|
|
142
|
-
watchdogFd = openSync(WATCHDOG_DEVICE, 'w');
|
|
143
|
-
// Pet immediately
|
|
144
|
-
petWatchdog();
|
|
145
|
-
// Set up periodic petting
|
|
146
|
-
watchdogTimer = setInterval(petWatchdog, WATCHDOG_INTERVAL_MS);
|
|
147
|
-
return true;
|
|
148
|
-
} catch {
|
|
149
|
-
watchdogFd = null;
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Stop the hardware watchdog gracefully.
|
|
156
|
-
* Writes 'V' (magic close character) to disable the watchdog before closing.
|
|
157
|
-
*/
|
|
158
|
-
export function stopWatchdog(): void {
|
|
159
|
-
if (watchdogTimer) {
|
|
160
|
-
clearInterval(watchdogTimer);
|
|
161
|
-
watchdogTimer = null;
|
|
162
|
-
}
|
|
163
|
-
if (watchdogFd !== null) {
|
|
164
|
-
try {
|
|
165
|
-
// Magic close character 'V' disables the watchdog
|
|
166
|
-
writeSync(watchdogFd, 'V');
|
|
167
|
-
closeSync(watchdogFd);
|
|
168
|
-
} catch { /* ignore close errors */ }
|
|
169
|
-
watchdogFd = null;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function petWatchdog(): void {
|
|
174
|
-
if (watchdogFd === null) return;
|
|
175
|
-
try {
|
|
176
|
-
writeSync(watchdogFd, '1');
|
|
177
|
-
} catch {
|
|
178
|
-
// If write fails, watchdog will trigger reboot — this is by design
|
|
179
|
-
}
|
|
180
|
-
}
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, openSync, writeSync, closeSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
// --- RPi Detection ---
|
|
5
|
+
|
|
6
|
+
let isRpiCached: boolean | null = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect if this machine is a Raspberry Pi by checking /proc/device-tree/model.
|
|
10
|
+
*/
|
|
11
|
+
export function isRaspberryPi(): boolean {
|
|
12
|
+
if (isRpiCached !== null) return isRpiCached;
|
|
13
|
+
try {
|
|
14
|
+
if (!existsSync('/proc/device-tree/model')) {
|
|
15
|
+
isRpiCached = false;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const model = readFileSync('/proc/device-tree/model', 'utf-8');
|
|
19
|
+
isRpiCached = model.toLowerCase().includes('raspberry pi');
|
|
20
|
+
return isRpiCached;
|
|
21
|
+
} catch {
|
|
22
|
+
isRpiCached = false;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Reset detection cache (for testing). */
|
|
28
|
+
export function resetRpiCache(): void {
|
|
29
|
+
isRpiCached = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- RPi Model Info ---
|
|
33
|
+
|
|
34
|
+
export interface RpiInfo {
|
|
35
|
+
model: string | null;
|
|
36
|
+
serial: string | null;
|
|
37
|
+
revision: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getRpiInfo(): RpiInfo {
|
|
41
|
+
const info: RpiInfo = { model: null, serial: null, revision: null };
|
|
42
|
+
try {
|
|
43
|
+
info.model = readFileSync('/proc/device-tree/model', 'utf-8').replace(/\0/g, '').trim();
|
|
44
|
+
} catch { /* not available */ }
|
|
45
|
+
try {
|
|
46
|
+
const cpuinfo = readFileSync('/proc/cpuinfo', 'utf-8');
|
|
47
|
+
const serialMatch = cpuinfo.match(/Serial\s*:\s*([0-9a-fA-F]+)/);
|
|
48
|
+
if (serialMatch) info.serial = serialMatch[1];
|
|
49
|
+
const revisionMatch = cpuinfo.match(/Revision\s*:\s*([0-9a-fA-F]+)/);
|
|
50
|
+
if (revisionMatch) info.revision = revisionMatch[1];
|
|
51
|
+
} catch { /* not available */ }
|
|
52
|
+
return info;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- GPU Temperature ---
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read GPU temperature via vcgencmd. Returns null if not available.
|
|
59
|
+
*/
|
|
60
|
+
export function getGpuTemp(): number | null {
|
|
61
|
+
try {
|
|
62
|
+
const output = execFileSync('vcgencmd', ['measure_temp'], {
|
|
63
|
+
timeout: 3000,
|
|
64
|
+
stdio: 'pipe',
|
|
65
|
+
}).toString();
|
|
66
|
+
// Output format: temp=42.8'C
|
|
67
|
+
const match = output.match(/temp=([\d.]+)/);
|
|
68
|
+
if (match) return Math.round(parseFloat(match[1]) * 10) / 10;
|
|
69
|
+
return null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Throttle Status ---
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read throttle/undervoltage status via vcgencmd. Returns null if not available.
|
|
79
|
+
* See: https://www.raspberrypi.com/documentation/computers/os.html#get_throttled
|
|
80
|
+
*
|
|
81
|
+
* Bit meanings:
|
|
82
|
+
* 0: Under-voltage detected
|
|
83
|
+
* 1: Arm frequency capped
|
|
84
|
+
* 2: Currently throttled
|
|
85
|
+
* 3: Soft temperature limit active
|
|
86
|
+
* 16: Under-voltage has occurred
|
|
87
|
+
* 17: Arm frequency capping has occurred
|
|
88
|
+
* 18: Throttling has occurred
|
|
89
|
+
* 19: Soft temperature limit has occurred
|
|
90
|
+
*/
|
|
91
|
+
export function getThrottled(): number | null {
|
|
92
|
+
try {
|
|
93
|
+
const output = execFileSync('vcgencmd', ['get_throttled'], {
|
|
94
|
+
timeout: 3000,
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
}).toString();
|
|
97
|
+
// Output format: throttled=0x0
|
|
98
|
+
const match = output.match(/throttled=(0x[0-9a-fA-F]+)/);
|
|
99
|
+
if (match) return parseInt(match[1], 16);
|
|
100
|
+
return null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- SD Card Read-Only Check ---
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if the root filesystem is mounted read-only.
|
|
110
|
+
*/
|
|
111
|
+
export function isSdCardReadOnly(): boolean {
|
|
112
|
+
try {
|
|
113
|
+
const mounts = readFileSync('/proc/mounts', 'utf-8');
|
|
114
|
+
const rootLine = mounts.split('\n').find((line) => {
|
|
115
|
+
const parts = line.split(' ');
|
|
116
|
+
return parts[1] === '/';
|
|
117
|
+
});
|
|
118
|
+
if (!rootLine) return false;
|
|
119
|
+
const options = rootLine.split(' ')[3] || '';
|
|
120
|
+
return options.split(',').includes('ro');
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Hardware Watchdog ---
|
|
127
|
+
|
|
128
|
+
let watchdogFd: number | null = null;
|
|
129
|
+
let watchdogTimer: NodeJS.Timeout | null = null;
|
|
130
|
+
const WATCHDOG_DEVICE = '/dev/watchdog';
|
|
131
|
+
const WATCHDOG_INTERVAL_MS = 10_000; // Pet every 10 seconds
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start the hardware watchdog. Writes to /dev/watchdog every 10s.
|
|
135
|
+
* If the agent dies, the kernel will reboot after ~15s.
|
|
136
|
+
* Returns true if started, false if not available.
|
|
137
|
+
*/
|
|
138
|
+
export function startWatchdog(): boolean {
|
|
139
|
+
if (watchdogFd !== null) return true; // Already running
|
|
140
|
+
try {
|
|
141
|
+
if (!existsSync(WATCHDOG_DEVICE)) return false;
|
|
142
|
+
watchdogFd = openSync(WATCHDOG_DEVICE, 'w');
|
|
143
|
+
// Pet immediately
|
|
144
|
+
petWatchdog();
|
|
145
|
+
// Set up periodic petting
|
|
146
|
+
watchdogTimer = setInterval(petWatchdog, WATCHDOG_INTERVAL_MS);
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
watchdogFd = null;
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Stop the hardware watchdog gracefully.
|
|
156
|
+
* Writes 'V' (magic close character) to disable the watchdog before closing.
|
|
157
|
+
*/
|
|
158
|
+
export function stopWatchdog(): void {
|
|
159
|
+
if (watchdogTimer) {
|
|
160
|
+
clearInterval(watchdogTimer);
|
|
161
|
+
watchdogTimer = null;
|
|
162
|
+
}
|
|
163
|
+
if (watchdogFd !== null) {
|
|
164
|
+
try {
|
|
165
|
+
// Magic close character 'V' disables the watchdog
|
|
166
|
+
writeSync(watchdogFd, 'V');
|
|
167
|
+
closeSync(watchdogFd);
|
|
168
|
+
} catch { /* ignore close errors */ }
|
|
169
|
+
watchdogFd = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function petWatchdog(): void {
|
|
174
|
+
if (watchdogFd === null) return;
|
|
175
|
+
try {
|
|
176
|
+
writeSync(watchdogFd, '1');
|
|
177
|
+
} catch {
|
|
178
|
+
// If write fails, watchdog will trigger reboot — this is by design
|
|
179
|
+
}
|
|
180
|
+
}
|
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
|
+
}
|