From 1fb93873ac0c7949648e51ad5163146b6ee048c7 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Fri, 5 Jun 2026 04:05:58 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20service=20execute=20(full-upgrade/reboo?= =?UTF-8?q?t=20->=20execution=20+=20rapport=20archiv=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- server/services/execute.ts | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 server/services/execute.ts diff --git a/server/services/execute.ts b/server/services/execute.ts new file mode 100644 index 0000000..19f6fce --- /dev/null +++ b/server/services/execute.ts @@ -0,0 +1,88 @@ +// server/services/execute.ts +import { randomUUID } from "node:crypto"; +import { eq } from "drizzle-orm"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { db, schema } from "../db/client.js"; +import { env } from "../env.js"; +import { getMachineRow, getCreds } from "./machines.js"; +import { renderTemplate } from "../templates/render.js"; +import { reduceAptLines } from "../templates/aptReduce.js"; +import { runScriptSudo } from "../ssh/client.js"; +import { parseRebootRequired } from "./aptParse.js"; +import { extractSection } from "./refresh.js"; +import { buildReportMarkdown } from "./report.js"; +import { outputHub } from "../ws/outputHub.js"; +import type { ActionType, ExecutionResult, ExecutionStatus } from "@shared/types.js"; + +const TEMPLATE_FOR: Record = { + apt_full_upgrade: "apt/full-upgrade.sh.tpl", + reboot: "apt/reboot.sh.tpl", +}; + +export async function runAction(machineId: string, action: ActionType): Promise { + const m = getMachineRow(machineId); + if (!m) throw new Error("Machine introuvable"); + + const executionId = `exec_${Date.now()}_${randomUUID().slice(0, 8)}`; + const startedAt = new Date().toISOString(); + outputHub.clear(machineId); + db.update(schema.machines).set({ status: "running" }).where(eq(schema.machines.id, machineId)).run(); + db.insert(schema.executions).values({ + id: executionId, machineId, action, mode: "manual", startedAt, status: "running", + }).run(); + + const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null; + const script = renderTemplate(TEMPLATE_FOR[action], { aptProxy: proxy }); + + let raw = ""; + let status: ExecutionStatus = "ok"; + try { + const res = await runScriptSudo(getCreds(m), script, (c) => { + raw += c; + outputHub.publish(machineId, c); + }); + raw = res.stdout; + if (/===SU:EXIT=(\d+)===/.exec(raw)?.[1] && /===SU:EXIT=0===/.test(raw) === false) { + status = "error"; + } + } catch (err) { + status = "error"; + raw += `\n[ERREUR] ${(err as Error).message}\n`; + } + + const finishedAt = new Date().toISOString(); + const rebootRequired = parseRebootRequired(extractSection(raw, "===SU:REBOOT===", "===SU:EXIT") || raw); + + // Archivage log brut + rapport. + const dir = join(env.reportsDir, machineId); + mkdirSync(dir, { recursive: true }); + const rawLogPath = join(dir, `${executionId}.log`); + const reportPath = join(dir, `${executionId}.md`); + writeFileSync(rawLogPath, raw, "utf8"); + + const result: ExecutionResult = { + executionId, machineId, startedAt, finishedAt, mode: "manual", action, status, + rebootRequiredAfterRun: rebootRequired, + importantLogLines: reduceAptLines(raw), + rawLogRef: rawLogPath, reportRef: reportPath, + }; + writeFileSync(reportPath, buildReportMarkdown(result, m.name), "utf8"); + + db.update(schema.executions).set({ + finishedAt, status, resultJson: JSON.stringify(result), reportPath, rawLogPath, + }).where(eq(schema.executions.id, executionId)).run(); + db.update(schema.machines).set({ status: status === "error" ? "error" : "unknown" }) + .where(eq(schema.machines.id, machineId)).run(); + + outputHub.publish(machineId, `\n===SU:DONE status=${status}===\n`); + return result; +} + +export function listExecutions(machineId: string) { + return db.select().from(schema.executions).where(eq(schema.executions.machineId, machineId)).all(); +} + +export function getExecution(executionId: string) { + return db.select().from(schema.executions).where(eq(schema.executions.id, executionId)).get(); +}