133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
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<Verdict, string> = { 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<string | null> {
|
|
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<boolean> {
|
|
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<number> {
|
|
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';
|
|
}
|