feat: socle BDD (tâche 1.9 Phase 1-2) + moteur APT (tâche 2 SJ-0→3) + WIP capabilities/auth/Rust
Checkpoint multi-chantiers (arbre vert : tsc 0 erreur, 70 tests, build OK). - tâche 1.9 Phase 1 : schéma socle (machine_state/events/reports/raw_artifacts/ hardware/metrics + colonnes étendues) + wiring refresh/execute. Migration 0002. - tâche 1.9 Phase 2 : machine_credentials + machine_host_keys (non destructif, dual-read + backfill). Migration 0003. Fix séquence journal de migration. - tâche 2 : SJ-0 (types étendus rétro-compatibles, réducteur Docker, resolveTemplate), SJ-1 (update-analyze enrichi), SJ-2 (apply + diff dpkg + timeout inactivité SSH), SJ-3 (reboot vérifié boot_id). - WIP parallèle inclus : /api/capabilities, auth/apiTokens/apiClients, system metrics, scaffold app_rust, ajustements frontend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+24
-2
@@ -44,17 +44,20 @@ export async function runPlain(creds: SshCreds, command: string): Promise<RunRes
|
||||
* Exécute un script shell sous sudo. Le script est encodé en base64 pour éviter
|
||||
* tout problème de quoting; le mot de passe sudo est poussé sur stdin (sudo -S -p '').
|
||||
* `onData` reçoit chaque chunk de sortie pour le streaming live.
|
||||
* `inactivityTimeoutMs` (défaut 0 = désactivé) : si aucune sortie n'est reçue pendant
|
||||
* cette durée, la connexion est fermée et une erreur `human_interaction_required` est levée.
|
||||
*/
|
||||
export async function runScriptSudo(
|
||||
creds: SshCreds,
|
||||
script: string,
|
||||
onData: (chunk: string) => void,
|
||||
inactivityTimeoutMs = 0,
|
||||
): Promise<RunResult> {
|
||||
const conn = await connect(creds);
|
||||
try {
|
||||
const b64 = Buffer.from(script, "utf8").toString("base64");
|
||||
const cmd = `sudo -S -p '' sh -c "$(printf '%s' '${b64}' | base64 -d)"`;
|
||||
return await execStream(conn, cmd, (creds.sudoPassword ?? creds.password) + "\n", onData);
|
||||
return await execStream(conn, cmd, (creds.sudoPassword ?? creds.password) + "\n", onData, inactivityTimeoutMs);
|
||||
} finally {
|
||||
conn.end();
|
||||
}
|
||||
@@ -65,26 +68,45 @@ function execStream(
|
||||
command: string,
|
||||
stdinData: string | null,
|
||||
onData: (chunk: string) => void,
|
||||
inactivityTimeoutMs = 0,
|
||||
): Promise<RunResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
conn.exec(command, { pty: false }, (err, stream) => {
|
||||
if (err) return reject(err);
|
||||
let stdout = "";
|
||||
let code = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const arm = () => {
|
||||
if (!inactivityTimeoutMs) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
stream.close();
|
||||
conn.end();
|
||||
reject(new Error(`human_interaction_required: aucune sortie depuis ${Math.round(inactivityTimeoutMs / 1000)}s`));
|
||||
}, inactivityTimeoutMs);
|
||||
};
|
||||
arm();
|
||||
|
||||
if (stdinData) {
|
||||
stream.write(stdinData);
|
||||
}
|
||||
stream
|
||||
.on("close", (c: number) => resolve({ stdout, code: c ?? code }))
|
||||
.on("close", (c: number) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, code: c ?? code });
|
||||
})
|
||||
.on("data", (d: Buffer) => {
|
||||
const s = d.toString("utf8");
|
||||
stdout += s;
|
||||
onData(s);
|
||||
arm();
|
||||
});
|
||||
stream.stderr.on("data", (d: Buffer) => {
|
||||
const s = d.toString("utf8");
|
||||
stdout += s;
|
||||
onData(s);
|
||||
arm();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user