import { execFile } from 'node:child_process'; import net from 'node:net'; import { promisify } from 'node:util'; import { CONFIG_PATH, type AgentConfig } from './config.ts'; const run = promisify(execFile); type Verdict = 'ok' | 'warn' | 'fail'; const MARKS: Record = { ok: '✓', warn: '!', fail: '✗' }; function line(verdict: Verdict, label: string, detail: string): Verdict { console.log(` ${MARKS[verdict]} ${label.padEnd(16)} ${detail}`); return verdict; } /** Test TCP simple : le port répond-il ? */ function probeTcp(host: string, port: number, timeoutMs = 4000): Promise { return new Promise((resolve) => { const socket = new net.Socket(); const done = (result: string | null) => { socket.destroy(); resolve(result); }; socket.setTimeout(timeoutMs); socket.once('connect', () => done(null)); socket.once('timeout', () => done('délai dépassé')); socket.once('error', (err: NodeJS.ErrnoException) => done(err.code ?? err.message)); socket.connect(port, host); }); } async function commandExists(command: string, args: string[]): Promise { try { await run(command, args, { timeout: 8000, windowsHide: true }); return true; } catch (err) { return (err as NodeJS.ErrnoException).code !== 'ENOENT'; } } /** * Vérifie une installation d'agent sans rien démarrer : configuration, joignabilité * du serveur et d'OBS, prérequis du rappel plein écran. * * Sert aussi de test de fumée en CI (`node agent.cjs --check`) : un import cassé ou * une régression de chemin sous Windows fait échouer la commande. */ export async function runDiagnostics(config: AgentConfig): Promise { console.log(`\nVérification de l'agent — ${process.platform} / Node ${process.versions.node}\n`); const results: Verdict[] = []; results.push(line('ok', 'configuration', CONFIG_PATH)); results.push( line( config.token ? 'ok' : 'fail', 'jeton', config.token ? `présent (${config.token.slice(0, 6)}…)` : 'ABSENT', ), ); results.push(line('ok', 'nom', config.name)); // --- Serveur de contrôle -------------------------------------------------- let serverUrl: URL | null = null; try { serverUrl = new URL(config.serverUrl); } catch { results.push(line('fail', 'serveur', `URL invalide : ${config.serverUrl}`)); } if (serverUrl) { const port = Number(serverUrl.port) || (serverUrl.protocol === 'wss:' ? 443 : 80); const error = await probeTcp(serverUrl.hostname, port); results.push( error ? line('fail', 'serveur', `${serverUrl.host} injoignable (${error})`) : line('ok', 'serveur', `${serverUrl.host} joignable`), ); if (serverUrl.protocol === 'ws:' && !isLoopback(serverUrl.hostname)) { line('warn', 'transport', 'ws:// non chiffré — à réserver à un réseau privé'); } } // --- OBS ------------------------------------------------------------------ const obsError = await probeTcp(config.obs.host, config.obs.port); results.push( obsError ? line('warn', 'obs-websocket', `${config.obs.host}:${config.obs.port} fermé (${obsError})`) : line('ok', 'obs-websocket', `${config.obs.host}:${config.obs.port} ouvert`), ); // --- Prérequis du rappel plein écran ------------------------------------- if (process.platform === 'linux') { const hasXdotool = await commandExists('xdotool', ['--version']); results.push( hasXdotool ? line('ok', 'xdotool', 'installé') : line('warn', 'xdotool', 'absent — apt install xdotool'), ); const display = process.env.DISPLAY; results.push( display ? line('ok', 'DISPLAY', display) : line('warn', 'DISPLAY', 'non défini — le rappel plein écran échouera'), ); if (process.env.WAYLAND_DISPLAY) { line('warn', 'session', 'Wayland détecté — xdotool exige X11'); } } else if (process.platform === 'win32') { const hasPowershell = await commandExists('powershell.exe', ['-NoProfile', '-Command', 'exit']); results.push( hasPowershell ? line('ok', 'powershell', 'disponible') : line('warn', 'powershell', 'introuvable — le rappel plein écran échouera'), ); } const failed = results.filter((verdict) => verdict === 'fail').length; const warned = results.filter((verdict) => verdict === 'warn').length; console.log( failed ? `\n${failed} problème(s) bloquant(s), ${warned} avertissement(s).\n` : `\nAucun problème bloquant${warned ? `, ${warned} avertissement(s)` : ''}.\n`, ); return failed > 0 ? 1 : 0; } function isLoopback(hostname: string): boolean { return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'; }