lightman-agent 1.0.12 → 1.0.14
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/package.json +53 -53
- package/scripts/guardian.ps1 +75 -94
- package/scripts/install-linux.sh +134 -134
- package/scripts/install-rpi.sh +117 -117
- package/scripts/install-windows.ps1 +529 -673
- package/scripts/launch-kiosk.vbs +101 -117
- package/scripts/lightman-shell.bat +128 -149
- package/scripts/setup.ps1 +116 -132
- package/scripts/setup.sh +115 -115
- package/scripts/uninstall-linux.sh +50 -50
- package/src/index.ts +734 -652
- package/src/lib/config.ts +69 -90
- package/src/services/autoUpdater.ts +155 -0
- package/src/services/kiosk.ts +395 -414
- package/src/services/presenceSensor.ts +331 -0
package/src/lib/config.ts
CHANGED
|
@@ -1,90 +1,69 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import type { AgentConfig } from './types.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
let raw: Record<string, unknown>;
|
|
71
|
-
try {
|
|
72
|
-
raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
73
|
-
} catch (err) {
|
|
74
|
-
throw new Error(`Invalid JSON in config file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Apply environment overrides
|
|
78
|
-
const merged = {
|
|
79
|
-
...raw,
|
|
80
|
-
...(process.env.LIGHTMAN_SERVER_URL && { serverUrl: process.env.LIGHTMAN_SERVER_URL }),
|
|
81
|
-
...(process.env.LIGHTMAN_DEVICE_SLUG && { deviceSlug: process.env.LIGHTMAN_DEVICE_SLUG }),
|
|
82
|
-
...(process.env.LIGHTMAN_HEALTH_INTERVAL && { healthIntervalMs: parseInt(process.env.LIGHTMAN_HEALTH_INTERVAL, 10) }),
|
|
83
|
-
...(process.env.LIGHTMAN_LOG_LEVEL && { logLevel: process.env.LIGHTMAN_LOG_LEVEL }),
|
|
84
|
-
...(process.env.LIGHTMAN_LOG_FILE && { logFile: process.env.LIGHTMAN_LOG_FILE }),
|
|
85
|
-
...(process.env.LIGHTMAN_IDENTITY_FILE && { identityFile: process.env.LIGHTMAN_IDENTITY_FILE }),
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const result = configSchema.parse(merged);
|
|
89
|
-
return result as AgentConfig;
|
|
90
|
-
}
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { AgentConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
const kioskSchema = z.object({
|
|
7
|
+
browserPath: z.string().default('chromium-browser'),
|
|
8
|
+
defaultUrl: z.string().url().default('http://localhost:3401/display'),
|
|
9
|
+
extraArgs: z.array(z.string()).default([]),
|
|
10
|
+
pollIntervalMs: z.number().int().min(1000).default(10_000),
|
|
11
|
+
maxCrashesInWindow: z.number().int().min(1).default(10),
|
|
12
|
+
crashWindowMs: z.number().int().min(10_000).default(300_000),
|
|
13
|
+
shellMode: z.boolean().default(false),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const screenshotSchema = z.object({
|
|
17
|
+
captureCommand: z.string().default('scrot'),
|
|
18
|
+
quality: z.number().int().min(1).max(100).default(80),
|
|
19
|
+
uploadEndpoint: z.string().default('/api/devices/{deviceId}/screenshot'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const powerScheduleSchema = z.object({
|
|
23
|
+
shutdownCron: z.string().optional(),
|
|
24
|
+
startupCron: z.string().optional(),
|
|
25
|
+
timezone: z.string().default(Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
26
|
+
shutdownWarningSeconds: z.number().int().min(0).max(600).default(60),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const configSchema = z.object({
|
|
30
|
+
serverUrl: z.string().url(),
|
|
31
|
+
deviceSlug: z.string().min(1),
|
|
32
|
+
healthIntervalMs: z.number().int().min(5000).default(60000),
|
|
33
|
+
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
34
|
+
logFile: z.string().default('agent.log'),
|
|
35
|
+
identityFile: z.string().default('.lightman-identity.json'),
|
|
36
|
+
localServices: z.boolean().default(true),
|
|
37
|
+
kiosk: kioskSchema.optional(),
|
|
38
|
+
screenshot: screenshotSchema.optional(),
|
|
39
|
+
powerSchedule: powerScheduleSchema.optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export function loadConfig(configPath?: string): AgentConfig {
|
|
43
|
+
const filePath = configPath || resolve(process.cwd(), 'agent.config.json');
|
|
44
|
+
|
|
45
|
+
if (!existsSync(filePath)) {
|
|
46
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let raw: Record<string, unknown>;
|
|
50
|
+
try {
|
|
51
|
+
raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new Error(`Invalid JSON in config file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Apply environment overrides
|
|
57
|
+
const merged = {
|
|
58
|
+
...raw,
|
|
59
|
+
...(process.env.LIGHTMAN_SERVER_URL && { serverUrl: process.env.LIGHTMAN_SERVER_URL }),
|
|
60
|
+
...(process.env.LIGHTMAN_DEVICE_SLUG && { deviceSlug: process.env.LIGHTMAN_DEVICE_SLUG }),
|
|
61
|
+
...(process.env.LIGHTMAN_HEALTH_INTERVAL && { healthIntervalMs: parseInt(process.env.LIGHTMAN_HEALTH_INTERVAL, 10) }),
|
|
62
|
+
...(process.env.LIGHTMAN_LOG_LEVEL && { logLevel: process.env.LIGHTMAN_LOG_LEVEL }),
|
|
63
|
+
...(process.env.LIGHTMAN_LOG_FILE && { logFile: process.env.LIGHTMAN_LOG_FILE }),
|
|
64
|
+
...(process.env.LIGHTMAN_IDENTITY_FILE && { identityFile: process.env.LIGHTMAN_IDENTITY_FILE }),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = configSchema.parse(merged);
|
|
68
|
+
return result as AgentConfig;
|
|
69
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { platform } from 'os';
|
|
2
|
+
import type { Logger } from '../lib/logger.js';
|
|
3
|
+
import type { WsClient } from './websocket.js';
|
|
4
|
+
import type { Updater } from './updater.js';
|
|
5
|
+
import type { Identity } from '../lib/types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AutoUpdater — periodically polls the server to check if a newer agent version
|
|
9
|
+
* is available. If found, downloads, verifies, installs, and restarts.
|
|
10
|
+
*
|
|
11
|
+
* Check interval: 5 minutes (default).
|
|
12
|
+
* Uses device API key auth so it works without JWT.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
|
|
17
|
+
export class AutoUpdater {
|
|
18
|
+
private readonly logger: Logger;
|
|
19
|
+
private readonly updater: Updater;
|
|
20
|
+
private readonly wsClient: WsClient;
|
|
21
|
+
private readonly serverUrl: string;
|
|
22
|
+
private readonly identity: Identity;
|
|
23
|
+
private readonly currentVersion: string;
|
|
24
|
+
private readonly checkIntervalMs: number;
|
|
25
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
26
|
+
private checking = false;
|
|
27
|
+
|
|
28
|
+
constructor(opts: {
|
|
29
|
+
logger: Logger;
|
|
30
|
+
updater: Updater;
|
|
31
|
+
wsClient: WsClient;
|
|
32
|
+
serverUrl: string;
|
|
33
|
+
identity: Identity;
|
|
34
|
+
currentVersion: string;
|
|
35
|
+
checkIntervalMs?: number;
|
|
36
|
+
}) {
|
|
37
|
+
this.logger = opts.logger;
|
|
38
|
+
this.updater = opts.updater;
|
|
39
|
+
this.wsClient = opts.wsClient;
|
|
40
|
+
this.serverUrl = opts.serverUrl;
|
|
41
|
+
this.identity = opts.identity;
|
|
42
|
+
this.currentVersion = opts.currentVersion;
|
|
43
|
+
this.checkIntervalMs = opts.checkIntervalMs || DEFAULT_CHECK_INTERVAL_MS;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
start(): void {
|
|
47
|
+
this.logger.info(`[AutoUpdate] Started — checking every ${Math.round(this.checkIntervalMs / 1000)}s (current: v${this.currentVersion})`);
|
|
48
|
+
|
|
49
|
+
// Initial check after 30s (let everything else boot first)
|
|
50
|
+
setTimeout(() => this.check(), 30_000);
|
|
51
|
+
|
|
52
|
+
this.timer = setInterval(() => this.check(), this.checkIntervalMs);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
stop(): void {
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearInterval(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async check(): Promise<void> {
|
|
63
|
+
if (this.checking || this.updater.isBusy()) return;
|
|
64
|
+
this.checking = true;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const plat = platform() === 'win32' ? 'windows' : 'linux';
|
|
68
|
+
const url = `${this.serverUrl}/api/agent/check-update?current_version=${encodeURIComponent(this.currentVersion)}&platform=${plat}`;
|
|
69
|
+
|
|
70
|
+
const res = await fetch(url, {
|
|
71
|
+
headers: { 'Authorization': `Bearer ${this.identity.apiKey}` },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
this.logger.debug(`[AutoUpdate] Check failed: HTTP ${res.status}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const json = await res.json() as { success: boolean; data: {
|
|
80
|
+
update_available: boolean;
|
|
81
|
+
version?: string;
|
|
82
|
+
checksum?: string;
|
|
83
|
+
download_url?: string;
|
|
84
|
+
}};
|
|
85
|
+
|
|
86
|
+
if (!json.success || !json.data.update_available) {
|
|
87
|
+
this.logger.debug('[AutoUpdate] No update available');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { version, checksum, download_url } = json.data;
|
|
92
|
+
if (!version || !checksum || !download_url) {
|
|
93
|
+
this.logger.warn('[AutoUpdate] Server returned incomplete update info');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.logger.info(`[AutoUpdate] New version available: v${version} (current: v${this.currentVersion})`);
|
|
98
|
+
|
|
99
|
+
// Notify server
|
|
100
|
+
this.wsClient.send({
|
|
101
|
+
type: 'agent:update_status',
|
|
102
|
+
payload: { phase: 'downloading', version },
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Build full download URL
|
|
107
|
+
const fullUrl = download_url.startsWith('http')
|
|
108
|
+
? download_url
|
|
109
|
+
: `${this.serverUrl}${download_url}`;
|
|
110
|
+
|
|
111
|
+
// Download
|
|
112
|
+
const filePath = await this.updater.download(fullUrl);
|
|
113
|
+
|
|
114
|
+
// Verify
|
|
115
|
+
this.wsClient.send({
|
|
116
|
+
type: 'agent:update_status',
|
|
117
|
+
payload: { phase: 'verifying', version },
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
});
|
|
120
|
+
const valid = await this.updater.verify(filePath, checksum);
|
|
121
|
+
if (!valid) {
|
|
122
|
+
this.wsClient.send({
|
|
123
|
+
type: 'agent:update_status',
|
|
124
|
+
payload: { phase: 'error', version, error: 'Checksum mismatch' },
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
});
|
|
127
|
+
this.logger.error('[AutoUpdate] Checksum mismatch — aborting');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Install
|
|
132
|
+
this.wsClient.send({
|
|
133
|
+
type: 'agent:update_status',
|
|
134
|
+
payload: { phase: 'installing', version },
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
});
|
|
137
|
+
await this.updater.install(filePath, version);
|
|
138
|
+
this.updater.cleanDownloads();
|
|
139
|
+
|
|
140
|
+
// Restart
|
|
141
|
+
this.wsClient.send({
|
|
142
|
+
type: 'agent:update_status',
|
|
143
|
+
payload: { phase: 'restarting', version },
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
});
|
|
146
|
+
this.logger.info(`[AutoUpdate] v${version} installed. Restarting in 2s...`);
|
|
147
|
+
setTimeout(() => process.exit(0), 2000);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
this.logger.error(`[AutoUpdate] Error: ${msg}`);
|
|
151
|
+
} finally {
|
|
152
|
+
this.checking = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|