lightman-agent 1.0.4 → 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 +513 -512
- 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/types.ts
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
// --- Agent Configuration ---
|
|
2
|
-
export interface AgentConfig {
|
|
3
|
-
serverUrl: string;
|
|
4
|
-
deviceSlug: string;
|
|
5
|
-
healthIntervalMs: number;
|
|
6
|
-
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
7
|
-
logFile: string;
|
|
8
|
-
identityFile: string;
|
|
9
|
-
/** When false, agent runs in kiosk-only mode — no local server/display processes. Default: true */
|
|
10
|
-
localServices: boolean;
|
|
11
|
-
kiosk?: KioskConfig;
|
|
12
|
-
screenshot?: ScreenshotConfig;
|
|
13
|
-
powerSchedule?: PowerScheduleConfig;
|
|
14
|
-
/** Port for the local hardware event WebSocket server (default: 3402) */
|
|
15
|
-
localEventsPort?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// --- Device Identity (persisted locally) ---
|
|
19
|
-
export interface Identity {
|
|
20
|
-
deviceId: string;
|
|
21
|
-
apiKey: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// --- WebSocket Messages ---
|
|
25
|
-
export interface WsMessage {
|
|
26
|
-
type: string;
|
|
27
|
-
payload?: Record<string, unknown>;
|
|
28
|
-
timestamp: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// --- Health Report ---
|
|
32
|
-
export interface HealthReport {
|
|
33
|
-
cpuUsage: number;
|
|
34
|
-
memTotal: number;
|
|
35
|
-
memUsed: number;
|
|
36
|
-
memPercent: number;
|
|
37
|
-
diskTotal: number;
|
|
38
|
-
diskUsed: number;
|
|
39
|
-
diskPercent: number;
|
|
40
|
-
cpuTemp: number | null;
|
|
41
|
-
uptime: number;
|
|
42
|
-
agentVersion: string;
|
|
43
|
-
// RPi-specific fields (optional, only present on Raspberry Pi)
|
|
44
|
-
gpuTemp?: number | null;
|
|
45
|
-
throttled?: number | null;
|
|
46
|
-
sdCardReadOnly?: boolean;
|
|
47
|
-
// Network info (optional)
|
|
48
|
-
network?: {
|
|
49
|
-
interface: string;
|
|
50
|
-
ip: string;
|
|
51
|
-
mac: string;
|
|
52
|
-
serverLatencyMs: number | null;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// --- Command Execution ---
|
|
57
|
-
export interface CommandRequest {
|
|
58
|
-
id: string;
|
|
59
|
-
command: string;
|
|
60
|
-
args?: Record<string, unknown>;
|
|
61
|
-
timeout?: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface CommandResult {
|
|
65
|
-
id: string;
|
|
66
|
-
command: string;
|
|
67
|
-
success: boolean;
|
|
68
|
-
data?: Record<string, unknown>;
|
|
69
|
-
error?: string;
|
|
70
|
-
durationMs: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// --- Kiosk Configuration ---
|
|
74
|
-
export interface KioskConfig {
|
|
75
|
-
browserPath: string;
|
|
76
|
-
defaultUrl: string;
|
|
77
|
-
extraArgs: string[];
|
|
78
|
-
pollIntervalMs: number;
|
|
79
|
-
maxCrashesInWindow: number;
|
|
80
|
-
crashWindowMs: number;
|
|
81
|
-
/** Shell replacement mode: Chrome is launched by the Windows shell (lightman-shell.bat),
|
|
82
|
-
* not by the agent. Agent only manages URL changes and monitors Chrome via process list. */
|
|
83
|
-
shellMode?: boolean;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// --- Multi-Screen Configuration ---
|
|
87
|
-
|
|
88
|
-
/** Mapping of a physical screen to a URL (stored in device config, pushed from admin) */
|
|
89
|
-
export interface ScreenMapping {
|
|
90
|
-
/** Hardware display ID, e.g. "\\\\.\\DISPLAY1" or "HDMI-1" */
|
|
91
|
-
hardwareId: string;
|
|
92
|
-
/** URL to open on this screen */
|
|
93
|
-
url: string;
|
|
94
|
-
/** Optional label for admin display */
|
|
95
|
-
label?: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// --- Kiosk Status ---
|
|
99
|
-
export interface KioskStatus {
|
|
100
|
-
running: boolean;
|
|
101
|
-
pid: number | null;
|
|
102
|
-
url: string | null;
|
|
103
|
-
crashCount: number;
|
|
104
|
-
crashLoopDetected: boolean;
|
|
105
|
-
uptimeMs: number | null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Status for multi-screen kiosk */
|
|
109
|
-
export interface MultiScreenKioskStatus {
|
|
110
|
-
screens: SingleScreenStatus[];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface SingleScreenStatus {
|
|
114
|
-
hardwareId: string;
|
|
115
|
-
url: string | null;
|
|
116
|
-
running: boolean;
|
|
117
|
-
pid: number | null;
|
|
118
|
-
uptimeMs: number | null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// --- Screenshot Configuration ---
|
|
122
|
-
export interface ScreenshotConfig {
|
|
123
|
-
captureCommand: string;
|
|
124
|
-
quality: number;
|
|
125
|
-
uploadEndpoint: string;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// --- Command Handler Function ---
|
|
129
|
-
export type CommandHandler = (
|
|
130
|
-
args?: Record<string, unknown>
|
|
131
|
-
) => Promise<Record<string, unknown> | void>;
|
|
132
|
-
|
|
133
|
-
// --- Log Forwarding ---
|
|
134
|
-
export interface LogEntry {
|
|
135
|
-
timestamp: string;
|
|
136
|
-
level: 'debug' | 'info' | 'warn' | 'error';
|
|
137
|
-
message: string;
|
|
138
|
-
source: string;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// --- Power Schedule ---
|
|
142
|
-
export interface PowerScheduleConfig {
|
|
143
|
-
/** Cron expression for shutdown (e.g., "0 19 * * *" = 7 PM daily) */
|
|
144
|
-
shutdownCron?: string;
|
|
145
|
-
/** Cron expression for startup prep — agent uses this only for logging; actual wake is via WOL */
|
|
146
|
-
startupCron?: string;
|
|
147
|
-
/** Timezone for cron expressions (e.g., "Asia/Kolkata"). Defaults to system timezone. */
|
|
148
|
-
timezone?: string;
|
|
149
|
-
/** Seconds before shutdown to warn via WebSocket (default: 60) */
|
|
150
|
-
shutdownWarningSeconds?: number;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// --- Watchdog & Self-Healing ---
|
|
154
|
-
export interface WatchdogConfig {
|
|
155
|
-
checkIntervalMs: number;
|
|
156
|
-
kioskCrashCooldownMs: number;
|
|
157
|
-
highMemoryThresholdMb: number;
|
|
158
|
-
highMemoryCooldownMs: number;
|
|
159
|
-
highDiskThresholdPercent: number;
|
|
160
|
-
highDiskCooldownMs: number;
|
|
161
|
-
wsDisconnectedThresholdMs: number;
|
|
162
|
-
wsDisconnectedCooldownMs: number;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export interface CrashReport {
|
|
166
|
-
process: string;
|
|
167
|
-
exitCode: number | null;
|
|
168
|
-
signal: string | null;
|
|
169
|
-
timestamp: string;
|
|
170
|
-
system: {
|
|
171
|
-
memPercent: number;
|
|
172
|
-
diskPercent: number;
|
|
173
|
-
cpuUsage: number;
|
|
174
|
-
uptime: number;
|
|
175
|
-
};
|
|
176
|
-
}
|
|
1
|
+
// --- Agent Configuration ---
|
|
2
|
+
export interface AgentConfig {
|
|
3
|
+
serverUrl: string;
|
|
4
|
+
deviceSlug: string;
|
|
5
|
+
healthIntervalMs: number;
|
|
6
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
7
|
+
logFile: string;
|
|
8
|
+
identityFile: string;
|
|
9
|
+
/** When false, agent runs in kiosk-only mode — no local server/display processes. Default: true */
|
|
10
|
+
localServices: boolean;
|
|
11
|
+
kiosk?: KioskConfig;
|
|
12
|
+
screenshot?: ScreenshotConfig;
|
|
13
|
+
powerSchedule?: PowerScheduleConfig;
|
|
14
|
+
/** Port for the local hardware event WebSocket server (default: 3402) */
|
|
15
|
+
localEventsPort?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Device Identity (persisted locally) ---
|
|
19
|
+
export interface Identity {
|
|
20
|
+
deviceId: string;
|
|
21
|
+
apiKey: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- WebSocket Messages ---
|
|
25
|
+
export interface WsMessage {
|
|
26
|
+
type: string;
|
|
27
|
+
payload?: Record<string, unknown>;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Health Report ---
|
|
32
|
+
export interface HealthReport {
|
|
33
|
+
cpuUsage: number;
|
|
34
|
+
memTotal: number;
|
|
35
|
+
memUsed: number;
|
|
36
|
+
memPercent: number;
|
|
37
|
+
diskTotal: number;
|
|
38
|
+
diskUsed: number;
|
|
39
|
+
diskPercent: number;
|
|
40
|
+
cpuTemp: number | null;
|
|
41
|
+
uptime: number;
|
|
42
|
+
agentVersion: string;
|
|
43
|
+
// RPi-specific fields (optional, only present on Raspberry Pi)
|
|
44
|
+
gpuTemp?: number | null;
|
|
45
|
+
throttled?: number | null;
|
|
46
|
+
sdCardReadOnly?: boolean;
|
|
47
|
+
// Network info (optional)
|
|
48
|
+
network?: {
|
|
49
|
+
interface: string;
|
|
50
|
+
ip: string;
|
|
51
|
+
mac: string;
|
|
52
|
+
serverLatencyMs: number | null;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Command Execution ---
|
|
57
|
+
export interface CommandRequest {
|
|
58
|
+
id: string;
|
|
59
|
+
command: string;
|
|
60
|
+
args?: Record<string, unknown>;
|
|
61
|
+
timeout?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CommandResult {
|
|
65
|
+
id: string;
|
|
66
|
+
command: string;
|
|
67
|
+
success: boolean;
|
|
68
|
+
data?: Record<string, unknown>;
|
|
69
|
+
error?: string;
|
|
70
|
+
durationMs: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- Kiosk Configuration ---
|
|
74
|
+
export interface KioskConfig {
|
|
75
|
+
browserPath: string;
|
|
76
|
+
defaultUrl: string;
|
|
77
|
+
extraArgs: string[];
|
|
78
|
+
pollIntervalMs: number;
|
|
79
|
+
maxCrashesInWindow: number;
|
|
80
|
+
crashWindowMs: number;
|
|
81
|
+
/** Shell replacement mode: Chrome is launched by the Windows shell (lightman-shell.bat),
|
|
82
|
+
* not by the agent. Agent only manages URL changes and monitors Chrome via process list. */
|
|
83
|
+
shellMode?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Multi-Screen Configuration ---
|
|
87
|
+
|
|
88
|
+
/** Mapping of a physical screen to a URL (stored in device config, pushed from admin) */
|
|
89
|
+
export interface ScreenMapping {
|
|
90
|
+
/** Hardware display ID, e.g. "\\\\.\\DISPLAY1" or "HDMI-1" */
|
|
91
|
+
hardwareId: string;
|
|
92
|
+
/** URL to open on this screen */
|
|
93
|
+
url: string;
|
|
94
|
+
/** Optional label for admin display */
|
|
95
|
+
label?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Kiosk Status ---
|
|
99
|
+
export interface KioskStatus {
|
|
100
|
+
running: boolean;
|
|
101
|
+
pid: number | null;
|
|
102
|
+
url: string | null;
|
|
103
|
+
crashCount: number;
|
|
104
|
+
crashLoopDetected: boolean;
|
|
105
|
+
uptimeMs: number | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Status for multi-screen kiosk */
|
|
109
|
+
export interface MultiScreenKioskStatus {
|
|
110
|
+
screens: SingleScreenStatus[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface SingleScreenStatus {
|
|
114
|
+
hardwareId: string;
|
|
115
|
+
url: string | null;
|
|
116
|
+
running: boolean;
|
|
117
|
+
pid: number | null;
|
|
118
|
+
uptimeMs: number | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Screenshot Configuration ---
|
|
122
|
+
export interface ScreenshotConfig {
|
|
123
|
+
captureCommand: string;
|
|
124
|
+
quality: number;
|
|
125
|
+
uploadEndpoint: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Command Handler Function ---
|
|
129
|
+
export type CommandHandler = (
|
|
130
|
+
args?: Record<string, unknown>
|
|
131
|
+
) => Promise<Record<string, unknown> | void>;
|
|
132
|
+
|
|
133
|
+
// --- Log Forwarding ---
|
|
134
|
+
export interface LogEntry {
|
|
135
|
+
timestamp: string;
|
|
136
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
137
|
+
message: string;
|
|
138
|
+
source: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Power Schedule ---
|
|
142
|
+
export interface PowerScheduleConfig {
|
|
143
|
+
/** Cron expression for shutdown (e.g., "0 19 * * *" = 7 PM daily) */
|
|
144
|
+
shutdownCron?: string;
|
|
145
|
+
/** Cron expression for startup prep — agent uses this only for logging; actual wake is via WOL */
|
|
146
|
+
startupCron?: string;
|
|
147
|
+
/** Timezone for cron expressions (e.g., "Asia/Kolkata"). Defaults to system timezone. */
|
|
148
|
+
timezone?: string;
|
|
149
|
+
/** Seconds before shutdown to warn via WebSocket (default: 60) */
|
|
150
|
+
shutdownWarningSeconds?: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Watchdog & Self-Healing ---
|
|
154
|
+
export interface WatchdogConfig {
|
|
155
|
+
checkIntervalMs: number;
|
|
156
|
+
kioskCrashCooldownMs: number;
|
|
157
|
+
highMemoryThresholdMb: number;
|
|
158
|
+
highMemoryCooldownMs: number;
|
|
159
|
+
highDiskThresholdPercent: number;
|
|
160
|
+
highDiskCooldownMs: number;
|
|
161
|
+
wsDisconnectedThresholdMs: number;
|
|
162
|
+
wsDisconnectedCooldownMs: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface CrashReport {
|
|
166
|
+
process: string;
|
|
167
|
+
exitCode: number | null;
|
|
168
|
+
signal: string | null;
|
|
169
|
+
timestamp: string;
|
|
170
|
+
system: {
|
|
171
|
+
memPercent: number;
|
|
172
|
+
diskPercent: number;
|
|
173
|
+
cpuUsage: number;
|
|
174
|
+
uptime: number;
|
|
175
|
+
};
|
|
176
|
+
}
|
package/src/services/commands.ts
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CommandRequest,
|
|
3
|
-
CommandResult,
|
|
4
|
-
CommandHandler,
|
|
5
|
-
WsMessage,
|
|
6
|
-
} from '../lib/types.js';
|
|
7
|
-
import type { WsClient } from './websocket.js';
|
|
8
|
-
import type { Logger } from '../lib/logger.js';
|
|
9
|
-
|
|
10
|
-
export class CommandExecutor {
|
|
11
|
-
private registry = new Map<string, CommandHandler>();
|
|
12
|
-
private wsClient: WsClient;
|
|
13
|
-
private logger: Logger;
|
|
14
|
-
|
|
15
|
-
constructor(wsClient: WsClient, logger: Logger) {
|
|
16
|
-
this.wsClient = wsClient;
|
|
17
|
-
this.logger = logger;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
register(command: string, handler: CommandHandler): void {
|
|
21
|
-
this.registry.set(command, handler);
|
|
22
|
-
this.logger.debug(`Command registered: ${command}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
getRegisteredCommands(): string[] {
|
|
26
|
-
return Array.from(this.registry.keys());
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Handle an incoming command message from the server.
|
|
31
|
-
* Lifecycle: validate → ack → execute → result
|
|
32
|
-
*/
|
|
33
|
-
async handleCommand(msg: WsMessage): Promise<void> {
|
|
34
|
-
const request = msg.payload as unknown as CommandRequest;
|
|
35
|
-
|
|
36
|
-
if (!request || !request.id || !request.command) {
|
|
37
|
-
this.logger.warn('Invalid command request:', msg);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
this.logger.info(`Command received: ${request.command} (${request.id})`);
|
|
42
|
-
|
|
43
|
-
// Check if command is registered
|
|
44
|
-
const handler = this.registry.get(request.command);
|
|
45
|
-
if (!handler) {
|
|
46
|
-
this.sendResult({
|
|
47
|
-
id: request.id,
|
|
48
|
-
command: request.command,
|
|
49
|
-
success: false,
|
|
50
|
-
error: `Unknown command: ${request.command}`,
|
|
51
|
-
durationMs: 0,
|
|
52
|
-
});
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Send ack
|
|
57
|
-
this.wsClient.send({
|
|
58
|
-
type: 'agent:command_ack',
|
|
59
|
-
payload: { id: request.id, command: request.command },
|
|
60
|
-
timestamp: Date.now(),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Execute with timeout
|
|
64
|
-
const start = Date.now();
|
|
65
|
-
const timeout = request.timeout || 30_000;
|
|
66
|
-
let timeoutId: NodeJS.Timeout | undefined;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const data = await Promise.race([
|
|
70
|
-
handler(request.args),
|
|
71
|
-
new Promise<never>((_, reject) => {
|
|
72
|
-
timeoutId = setTimeout(() => reject(new Error('Command timed out')), timeout);
|
|
73
|
-
}),
|
|
74
|
-
]);
|
|
75
|
-
clearTimeout(timeoutId);
|
|
76
|
-
|
|
77
|
-
this.sendResult({
|
|
78
|
-
id: request.id,
|
|
79
|
-
command: request.command,
|
|
80
|
-
success: true,
|
|
81
|
-
data: (data as Record<string, unknown>) || {},
|
|
82
|
-
durationMs: Date.now() - start,
|
|
83
|
-
});
|
|
84
|
-
} catch (err) {
|
|
85
|
-
clearTimeout(timeoutId);
|
|
86
|
-
this.sendResult({
|
|
87
|
-
id: request.id,
|
|
88
|
-
command: request.command,
|
|
89
|
-
success: false,
|
|
90
|
-
error: err instanceof Error ? err.message : String(err),
|
|
91
|
-
durationMs: Date.now() - start,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private sendResult(result: CommandResult): void {
|
|
97
|
-
this.logger.info(
|
|
98
|
-
`Command result: ${result.command} (${result.id}) → ${result.success ? 'OK' : 'FAIL'} in ${result.durationMs}ms`
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
this.wsClient.send({
|
|
102
|
-
type: 'agent:command_result',
|
|
103
|
-
payload: result as unknown as Record<string, unknown>,
|
|
104
|
-
timestamp: Date.now(),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
CommandRequest,
|
|
3
|
+
CommandResult,
|
|
4
|
+
CommandHandler,
|
|
5
|
+
WsMessage,
|
|
6
|
+
} from '../lib/types.js';
|
|
7
|
+
import type { WsClient } from './websocket.js';
|
|
8
|
+
import type { Logger } from '../lib/logger.js';
|
|
9
|
+
|
|
10
|
+
export class CommandExecutor {
|
|
11
|
+
private registry = new Map<string, CommandHandler>();
|
|
12
|
+
private wsClient: WsClient;
|
|
13
|
+
private logger: Logger;
|
|
14
|
+
|
|
15
|
+
constructor(wsClient: WsClient, logger: Logger) {
|
|
16
|
+
this.wsClient = wsClient;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
register(command: string, handler: CommandHandler): void {
|
|
21
|
+
this.registry.set(command, handler);
|
|
22
|
+
this.logger.debug(`Command registered: ${command}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getRegisteredCommands(): string[] {
|
|
26
|
+
return Array.from(this.registry.keys());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle an incoming command message from the server.
|
|
31
|
+
* Lifecycle: validate → ack → execute → result
|
|
32
|
+
*/
|
|
33
|
+
async handleCommand(msg: WsMessage): Promise<void> {
|
|
34
|
+
const request = msg.payload as unknown as CommandRequest;
|
|
35
|
+
|
|
36
|
+
if (!request || !request.id || !request.command) {
|
|
37
|
+
this.logger.warn('Invalid command request:', msg);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.logger.info(`Command received: ${request.command} (${request.id})`);
|
|
42
|
+
|
|
43
|
+
// Check if command is registered
|
|
44
|
+
const handler = this.registry.get(request.command);
|
|
45
|
+
if (!handler) {
|
|
46
|
+
this.sendResult({
|
|
47
|
+
id: request.id,
|
|
48
|
+
command: request.command,
|
|
49
|
+
success: false,
|
|
50
|
+
error: `Unknown command: ${request.command}`,
|
|
51
|
+
durationMs: 0,
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Send ack
|
|
57
|
+
this.wsClient.send({
|
|
58
|
+
type: 'agent:command_ack',
|
|
59
|
+
payload: { id: request.id, command: request.command },
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Execute with timeout
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
const timeout = request.timeout || 30_000;
|
|
66
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const data = await Promise.race([
|
|
70
|
+
handler(request.args),
|
|
71
|
+
new Promise<never>((_, reject) => {
|
|
72
|
+
timeoutId = setTimeout(() => reject(new Error('Command timed out')), timeout);
|
|
73
|
+
}),
|
|
74
|
+
]);
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
|
|
77
|
+
this.sendResult({
|
|
78
|
+
id: request.id,
|
|
79
|
+
command: request.command,
|
|
80
|
+
success: true,
|
|
81
|
+
data: (data as Record<string, unknown>) || {},
|
|
82
|
+
durationMs: Date.now() - start,
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
clearTimeout(timeoutId);
|
|
86
|
+
this.sendResult({
|
|
87
|
+
id: request.id,
|
|
88
|
+
command: request.command,
|
|
89
|
+
success: false,
|
|
90
|
+
error: err instanceof Error ? err.message : String(err),
|
|
91
|
+
durationMs: Date.now() - start,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private sendResult(result: CommandResult): void {
|
|
97
|
+
this.logger.info(
|
|
98
|
+
`Command result: ${result.command} (${result.id}) → ${result.success ? 'OK' : 'FAIL'} in ${result.durationMs}ms`
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
this.wsClient.send({
|
|
102
|
+
type: 'agent:command_result',
|
|
103
|
+
payload: result as unknown as Record<string, unknown>,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|