// 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+===/.test(raw) && !/===SU:EXIT=0===/.test(raw)) { 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(); }