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,152 +1,152 @@
|
|
|
1
|
-
import WebSocket from 'ws';
|
|
2
|
-
import type { WsMessage, Identity } from '../lib/types.js';
|
|
3
|
-
import type { Logger } from '../lib/logger.js';
|
|
4
|
-
|
|
5
|
-
interface WsClientOptions {
|
|
6
|
-
serverUrl: string;
|
|
7
|
-
identity: Identity;
|
|
8
|
-
logger: Logger;
|
|
9
|
-
onMessage: (msg: WsMessage) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class WsClient {
|
|
13
|
-
private ws: WebSocket | null = null;
|
|
14
|
-
private serverUrl: string;
|
|
15
|
-
private identity: Identity;
|
|
16
|
-
private logger: Logger;
|
|
17
|
-
private onMessage: (msg: WsMessage) => void;
|
|
18
|
-
|
|
19
|
-
private reconnectAttempts = 0;
|
|
20
|
-
private maxReconnectDelay = 60_000;
|
|
21
|
-
private baseDelay = 1_000;
|
|
22
|
-
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
23
|
-
private closed = false;
|
|
24
|
-
private messageQueue: WsMessage[] = [];
|
|
25
|
-
|
|
26
|
-
constructor(options: WsClientOptions) {
|
|
27
|
-
this.serverUrl = options.serverUrl;
|
|
28
|
-
this.identity = options.identity;
|
|
29
|
-
this.logger = options.logger;
|
|
30
|
-
this.onMessage = options.onMessage;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
connect(): void {
|
|
34
|
-
if (this.closed) return;
|
|
35
|
-
|
|
36
|
-
const wsUrl = this.serverUrl.replace(/^http/, 'ws') +
|
|
37
|
-
'/ws/agent?apiKey=' + encodeURIComponent(this.identity.apiKey);
|
|
38
|
-
|
|
39
|
-
this.logger.debug('Connecting to', wsUrl.replace(/apiKey=.*/, 'apiKey=***'));
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
this.ws = new WebSocket(wsUrl);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
this.logger.error('Failed to create WebSocket:', err);
|
|
45
|
-
this.scheduleReconnect();
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
this.ws.on('open', () => {
|
|
50
|
-
this.logger.info('WebSocket connected');
|
|
51
|
-
this.reconnectAttempts = 0;
|
|
52
|
-
this.flushQueue();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
this.ws.on('message', (data) => {
|
|
56
|
-
try {
|
|
57
|
-
const msg: WsMessage = JSON.parse(data.toString());
|
|
58
|
-
this.onMessage(msg);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
this.logger.error('Failed to parse WS message:', err);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
this.ws.on('close', (code, reason) => {
|
|
65
|
-
this.logger.warn(`WebSocket closed: ${code} ${reason.toString()}`);
|
|
66
|
-
this.ws = null;
|
|
67
|
-
if (!this.closed) {
|
|
68
|
-
this.scheduleReconnect();
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
this.ws.on('error', (err) => {
|
|
73
|
-
this.logger.error('WebSocket error:', err);
|
|
74
|
-
// 'close' event will fire after this, triggering reconnect
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
this.ws.on('ping', () => {
|
|
78
|
-
// ws library auto-responds with pong
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
send(msg: WsMessage): void {
|
|
83
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
84
|
-
this.ws.send(JSON.stringify(msg));
|
|
85
|
-
} else {
|
|
86
|
-
// Queue message for when connection is restored
|
|
87
|
-
this.messageQueue.push(msg);
|
|
88
|
-
// Keep queue bounded
|
|
89
|
-
if (this.messageQueue.length > 100) {
|
|
90
|
-
this.messageQueue.shift();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
close(): void {
|
|
96
|
-
this.closed = true;
|
|
97
|
-
if (this.reconnectTimer) {
|
|
98
|
-
clearTimeout(this.reconnectTimer);
|
|
99
|
-
this.reconnectTimer = null;
|
|
100
|
-
}
|
|
101
|
-
if (this.ws) {
|
|
102
|
-
this.ws.close(1000, 'Agent shutting down');
|
|
103
|
-
this.ws = null;
|
|
104
|
-
}
|
|
105
|
-
this.messageQueue = [];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
isConnected(): boolean {
|
|
109
|
-
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Calculate delay with exponential backoff + jitter */
|
|
113
|
-
getReconnectDelay(): number {
|
|
114
|
-
const exponential = this.baseDelay * Math.pow(2, this.reconnectAttempts);
|
|
115
|
-
const capped = Math.min(exponential, this.maxReconnectDelay);
|
|
116
|
-
// Add jitter: 0.5x to 1.5x
|
|
117
|
-
const jitter = capped * (0.5 + Math.random());
|
|
118
|
-
return Math.round(jitter);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private scheduleReconnect(): void {
|
|
122
|
-
if (this.closed) return;
|
|
123
|
-
|
|
124
|
-
const delay = this.getReconnectDelay();
|
|
125
|
-
this.reconnectAttempts++;
|
|
126
|
-
|
|
127
|
-
this.logger.info(
|
|
128
|
-
`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
this.reconnectTimer = setTimeout(() => {
|
|
132
|
-
this.reconnectTimer = null;
|
|
133
|
-
this.connect();
|
|
134
|
-
}, delay);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private flushQueue(): void {
|
|
138
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
139
|
-
|
|
140
|
-
const now = Date.now();
|
|
141
|
-
const MAX_AGE = 300_000; // 5 minutes
|
|
142
|
-
|
|
143
|
-
while (this.messageQueue.length > 0) {
|
|
144
|
-
const msg = this.messageQueue.shift()!;
|
|
145
|
-
if (now - msg.timestamp > MAX_AGE) {
|
|
146
|
-
this.logger.warn(`Discarding stale queued message: ${msg.type}`);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
this.ws.send(JSON.stringify(msg));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import type { WsMessage, Identity } from '../lib/types.js';
|
|
3
|
+
import type { Logger } from '../lib/logger.js';
|
|
4
|
+
|
|
5
|
+
interface WsClientOptions {
|
|
6
|
+
serverUrl: string;
|
|
7
|
+
identity: Identity;
|
|
8
|
+
logger: Logger;
|
|
9
|
+
onMessage: (msg: WsMessage) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WsClient {
|
|
13
|
+
private ws: WebSocket | null = null;
|
|
14
|
+
private serverUrl: string;
|
|
15
|
+
private identity: Identity;
|
|
16
|
+
private logger: Logger;
|
|
17
|
+
private onMessage: (msg: WsMessage) => void;
|
|
18
|
+
|
|
19
|
+
private reconnectAttempts = 0;
|
|
20
|
+
private maxReconnectDelay = 60_000;
|
|
21
|
+
private baseDelay = 1_000;
|
|
22
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
23
|
+
private closed = false;
|
|
24
|
+
private messageQueue: WsMessage[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(options: WsClientOptions) {
|
|
27
|
+
this.serverUrl = options.serverUrl;
|
|
28
|
+
this.identity = options.identity;
|
|
29
|
+
this.logger = options.logger;
|
|
30
|
+
this.onMessage = options.onMessage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connect(): void {
|
|
34
|
+
if (this.closed) return;
|
|
35
|
+
|
|
36
|
+
const wsUrl = this.serverUrl.replace(/^http/, 'ws') +
|
|
37
|
+
'/ws/agent?apiKey=' + encodeURIComponent(this.identity.apiKey);
|
|
38
|
+
|
|
39
|
+
this.logger.debug('Connecting to', wsUrl.replace(/apiKey=.*/, 'apiKey=***'));
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
this.ws = new WebSocket(wsUrl);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
this.logger.error('Failed to create WebSocket:', err);
|
|
45
|
+
this.scheduleReconnect();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.ws.on('open', () => {
|
|
50
|
+
this.logger.info('WebSocket connected');
|
|
51
|
+
this.reconnectAttempts = 0;
|
|
52
|
+
this.flushQueue();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
this.ws.on('message', (data) => {
|
|
56
|
+
try {
|
|
57
|
+
const msg: WsMessage = JSON.parse(data.toString());
|
|
58
|
+
this.onMessage(msg);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
this.logger.error('Failed to parse WS message:', err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.ws.on('close', (code, reason) => {
|
|
65
|
+
this.logger.warn(`WebSocket closed: ${code} ${reason.toString()}`);
|
|
66
|
+
this.ws = null;
|
|
67
|
+
if (!this.closed) {
|
|
68
|
+
this.scheduleReconnect();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this.ws.on('error', (err) => {
|
|
73
|
+
this.logger.error('WebSocket error:', err);
|
|
74
|
+
// 'close' event will fire after this, triggering reconnect
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.ws.on('ping', () => {
|
|
78
|
+
// ws library auto-responds with pong
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
send(msg: WsMessage): void {
|
|
83
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
84
|
+
this.ws.send(JSON.stringify(msg));
|
|
85
|
+
} else {
|
|
86
|
+
// Queue message for when connection is restored
|
|
87
|
+
this.messageQueue.push(msg);
|
|
88
|
+
// Keep queue bounded
|
|
89
|
+
if (this.messageQueue.length > 100) {
|
|
90
|
+
this.messageQueue.shift();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
close(): void {
|
|
96
|
+
this.closed = true;
|
|
97
|
+
if (this.reconnectTimer) {
|
|
98
|
+
clearTimeout(this.reconnectTimer);
|
|
99
|
+
this.reconnectTimer = null;
|
|
100
|
+
}
|
|
101
|
+
if (this.ws) {
|
|
102
|
+
this.ws.close(1000, 'Agent shutting down');
|
|
103
|
+
this.ws = null;
|
|
104
|
+
}
|
|
105
|
+
this.messageQueue = [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
isConnected(): boolean {
|
|
109
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Calculate delay with exponential backoff + jitter */
|
|
113
|
+
getReconnectDelay(): number {
|
|
114
|
+
const exponential = this.baseDelay * Math.pow(2, this.reconnectAttempts);
|
|
115
|
+
const capped = Math.min(exponential, this.maxReconnectDelay);
|
|
116
|
+
// Add jitter: 0.5x to 1.5x
|
|
117
|
+
const jitter = capped * (0.5 + Math.random());
|
|
118
|
+
return Math.round(jitter);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private scheduleReconnect(): void {
|
|
122
|
+
if (this.closed) return;
|
|
123
|
+
|
|
124
|
+
const delay = this.getReconnectDelay();
|
|
125
|
+
this.reconnectAttempts++;
|
|
126
|
+
|
|
127
|
+
this.logger.info(
|
|
128
|
+
`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
this.reconnectTimer = setTimeout(() => {
|
|
132
|
+
this.reconnectTimer = null;
|
|
133
|
+
this.connect();
|
|
134
|
+
}, delay);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private flushQueue(): void {
|
|
138
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
139
|
+
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const MAX_AGE = 300_000; // 5 minutes
|
|
142
|
+
|
|
143
|
+
while (this.messageQueue.length > 0) {
|
|
144
|
+
const msg = this.messageQueue.shift()!;
|
|
145
|
+
if (now - msg.timestamp > MAX_AGE) {
|
|
146
|
+
this.logger.warn(`Discarding stale queued message: ${msg.type}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
this.ws.send(JSON.stringify(msg));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"sourceMap": true,
|
|
16
|
-
"noUnusedLocals": false,
|
|
17
|
-
"noUnusedParameters": false,
|
|
18
|
-
"noFallthroughCasesInSwitch": true,
|
|
19
|
-
"isolatedModules": true,
|
|
20
|
-
"paths": {
|
|
21
|
-
"@/*": ["./src/*"]
|
|
22
|
-
},
|
|
23
|
-
"baseUrl": ".",
|
|
24
|
-
"types": ["node"]
|
|
25
|
-
},
|
|
26
|
-
"include": ["src/**/*"],
|
|
27
|
-
"exclude": ["node_modules", "dist", "src/__tests__"]
|
|
28
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"noUnusedLocals": false,
|
|
17
|
+
"noUnusedParameters": false,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
},
|
|
23
|
+
"baseUrl": ".",
|
|
24
|
+
"types": ["node"]
|
|
25
|
+
},
|
|
26
|
+
"include": ["src/**/*"],
|
|
27
|
+
"exclude": ["node_modules", "dist", "src/__tests__"]
|
|
28
|
+
}
|