CI : Deployment
This commit is contained in:
@@ -9,8 +9,10 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=cjs --outfile=dist-bundle/agent.cjs --external:bufferutil --external:utf-8-validate --legal-comments=none",
|
||||
"dev": "npm run build -w @stream-control/shared && node --experimental-strip-types --disable-warning=ExperimentalWarning --watch src/index.ts",
|
||||
"start": "node dist/index.js"
|
||||
"start": "node dist/index.js",
|
||||
"check": "node dist/index.js --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stream-control/shared": "*",
|
||||
@@ -20,6 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/ws": "^8.5.13",
|
||||
"esbuild": "^0.28.2",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
132
packages/agent/src/doctor.ts
Normal file
132
packages/agent/src/doctor.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
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';
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
safeJsonParse,
|
||||
} from '@stream-control/shared';
|
||||
import { loadConfig, persistIdentity, type AgentConfig } from './config.ts';
|
||||
import { runDiagnostics } from './doctor.ts';
|
||||
import { ObsController } from './obs.ts';
|
||||
import { StreamWatcher } from './watcher.ts';
|
||||
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
|
||||
@@ -256,4 +257,10 @@ process.on('unhandledRejection', (reason) => {
|
||||
});
|
||||
|
||||
console.log(`stream-control agent v${AGENT_VERSION} — ${config.name} (${process.platform})`);
|
||||
connect();
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
// Diagnostic seul : rien n'est démarré, aucune connexion n'est maintenue.
|
||||
runDiagnostics(config).then((code) => process.exit(code));
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user