This commit is contained in:
jeanotx32
2026-08-11 00:26:56 +02:00
commit 6f11b72cbb
43 changed files with 7175 additions and 0 deletions

215
packages/agent/src/index.ts Normal file
View File

@@ -0,0 +1,215 @@
#!/usr/bin/env node
import os from 'node:os';
import { WebSocket } from 'ws';
import type {
AgentStatus,
AgentToServer,
LogLevel,
ServerToAgent,
} from '@stream-control/shared';
import { PROTOCOL_VERSION, detectPlatform, emptyStatus, safeJsonParse } from '@stream-control/shared';
import { loadConfig, persistIdentity, type AgentConfig } from './config.ts';
import { ObsController } from './obs.ts';
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
const AGENT_VERSION = '0.1.0';
const RECONNECT_MIN_MS = 1000;
const RECONNECT_MAX_MS = 30_000;
let config: AgentConfig;
try {
config = loadConfig();
} catch (err) {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
}
const obs = new ObsController(config.obs);
let socket: WebSocket | null = null;
let statusTimer: NodeJS.Timeout | null = null;
let reconnectDelay = RECONNECT_MIN_MS;
let statusIntervalMs = 2000;
let shuttingDown = false;
// --- Transport vers le serveur de contrôle ----------------------------------
function send(message: AgentToServer): void {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}
function report(level: LogLevel, message: string): void {
const prefix = level === 'error' ? '✖' : level === 'warn' ? '!' : '·';
console.log(`${prefix} ${message}`);
send({ type: 'log', level, message, ts: Date.now() });
}
obs.on('log', (level: LogLevel, message: string) => report(level, message));
function connect(): void {
if (shuttingDown) return;
const url = new URL(config.serverUrl);
console.log(`Connexion au serveur ${url.origin}${url.pathname}`);
socket = new WebSocket(url, {
headers: { authorization: `Bearer ${config.token}` },
rejectUnauthorized: !config.insecureTls,
handshakeTimeout: 10_000,
});
socket.on('open', () => {
reconnectDelay = RECONNECT_MIN_MS;
send({
type: 'hello',
protocol: PROTOCOL_VERSION,
agentId: config.agentId,
name: config.name,
hostname: os.hostname(),
platform: detectPlatform(process.platform),
agentVersion: AGENT_VERSION,
});
});
socket.on('message', (raw) => {
const message = safeJsonParse<ServerToAgent>(raw.toString());
if (message) void handleServerMessage(message);
});
socket.on('close', (code, reason) => {
stopStatusLoop();
socket = null;
if (shuttingDown) return;
const why = reason.toString() || `code ${code}`;
console.warn(`Session serveur fermée (${why}), nouvelle tentative dans ${reconnectDelay / 1000}s`);
scheduleReconnect();
});
socket.on('error', (err: Error) => {
console.error(`Erreur de connexion : ${err.message}`);
});
}
function scheduleReconnect(): void {
const delay = reconnectDelay;
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
setTimeout(connect, delay).unref?.();
}
// --- Traitement des messages serveur ----------------------------------------
async function handleServerMessage(message: ServerToAgent): Promise<void> {
switch (message.type) {
case 'welcome': {
statusIntervalMs = message.statusIntervalMs || statusIntervalMs;
if (message.token && message.token !== config.token) {
config.token = message.token;
config.agentId = message.agentId;
persistIdentity(message.agentId, message.token);
} else if (!config.agentId) {
config.agentId = message.agentId;
}
obs.applySettings(message.obs);
report('info', `Agent « ${config.name} » rattaché au serveur (id ${message.agentId})`);
if (message.autoConnectObs && !obs.isConnected) {
obs.connect().catch((err: Error) => report('warn', err.message));
}
startStatusLoop();
break;
}
case 'config': {
obs.applySettings(message.obs);
if (message.autoConnectObs && !obs.isConnected) {
obs.connect().catch((err: Error) => report('warn', err.message));
}
break;
}
case 'command': {
try {
const data = await obs.execute(message.action, message.params ?? {});
send({ type: 'result', requestId: message.requestId, ok: true, data });
void pushStatus(); // état rafraîchi immédiatement après l'action
} catch (err) {
const text = err instanceof Error ? err.message : String(err);
send({ type: 'result', requestId: message.requestId, ok: false, error: text });
report('error', `Échec de « ${message.action} » : ${text}`);
}
break;
}
case 'ping': {
send({ type: 'pong', ts: Date.now() });
break;
}
}
}
// --- Boucle de statut --------------------------------------------------------
async function buildStatus(): Promise<AgentStatus> {
const snapshot = await obs.snapshot();
const memory = memoryUsage();
const disk = await diskUsage(snapshot.recordDirectory);
return {
...emptyStatus(),
...snapshot,
lastRecordingPath: obs.recordingPath,
systemCpu: cpuUsagePercent(),
systemMemoryUsed: memory.used,
systemMemoryTotal: memory.total,
diskFreeBytes: disk?.freeBytes,
diskTotalBytes: disk?.totalBytes,
updatedAt: Date.now(),
};
}
async function pushStatus(): Promise<void> {
if (socket?.readyState !== WebSocket.OPEN) return;
try {
send({ type: 'status', status: await buildStatus() });
} catch (err) {
console.error('Collecte de statut en échec :', err);
}
}
function startStatusLoop(): void {
stopStatusLoop();
void pushStatus();
statusTimer = setInterval(() => void pushStatus(), statusIntervalMs);
statusTimer.unref?.();
}
function stopStatusLoop(): void {
if (statusTimer) clearInterval(statusTimer);
statusTimer = null;
}
// --- Cycle de vie ------------------------------------------------------------
async function shutdown(signal: string): Promise<void> {
if (shuttingDown) return;
shuttingDown = true;
console.log(`\n${signal} reçu, arrêt de l'agent…`);
stopStatusLoop();
// L'enregistrement OBS en cours n'est volontairement pas interrompu.
await obs.disconnect().catch(() => undefined);
socket?.close(1000, 'Arrêt de l\'agent');
setTimeout(() => process.exit(0), 500).unref();
}
process.on('SIGINT', () => void shutdown('SIGINT'));
process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('unhandledRejection', (reason) => {
console.error('Rejet non géré :', reason);
});
console.log(`stream-control agent v${AGENT_VERSION}${config.name} (${process.platform})`);
connect();