import "server-only"; import { execFile } from "node:child_process"; import { rm, stat } from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import { serverEnv } from "@/lib/server/env"; import { resolveDataRootDirectory, resolveStoreStacksRoot, } from "@/lib/server/storage/data-root"; const execFileAsync = promisify(execFile); const FACTORY_RESET_LOG_PATH = "/var/log/homeio-factory-reset.log"; // ── Systemd unit helpers ─────────────────────────────────────────────────────── const SYSTEMD_RUN_BASE_ARGS = [ "--quiet", "--no-block", "--collect", "--property=Type=exec", "--property=KillMode=control-group", "--property=TimeoutStopSec=30s", "--property=SendSIGKILL=yes", ] as const; /** * Schedules a short inline shell command via a transient systemd unit. * Used for simple one-liners like reboot and shutdown. */ async function scheduleSystemCommand(command: string, unitName: string) { await execFileAsync("systemd-run", [ ...SYSTEMD_RUN_BASE_ARGS, `--unit=${unitName}`, "bash", "-lc", command, ]); } /** * Schedules a shell script file via a transient systemd unit. * Environment variables are injected cleanly via --setenv instead of being * interpolated into a command string. */ async function scheduleSystemScript( scriptPath: string, unitName: string, env: Record, ) { const envArgs = Object.entries(env).flatMap(([key, value]) => [ "--setenv", `${key}=${value}`, ]); await execFileAsync("systemd-run", [ ...SYSTEMD_RUN_BASE_ARGS, `--unit=${unitName}`, ...envArgs, "bash", scriptPath, ]); } // ── Public API ──────────────────────────────────────────────────────────────── export async function isSystemdAvailable(): Promise { try { await execFileAsync("systemd-run", ["--version"]); return true; } catch { return false; } } export function scheduleSystemReboot() { return scheduleSystemCommand("sleep 2; systemctl reboot", `homeio-reboot-${Date.now()}`); } export function scheduleSystemShutdown() { return scheduleSystemCommand("sleep 2; systemctl poweroff", `homeio-shutdown-${Date.now()}`); } export async function scheduleFactoryReset() { const scriptPath = path.join(process.cwd(), "scripts", "factory-reset.sh"); // Fail fast with a clear error before accepting the request — avoids the user // being redirected to the factory-reset screen when nothing will happen. try { await stat(scriptPath); } catch { throw new Error(`Factory reset script not found at ${scriptPath}.`); } const dataRoot = resolveDataRootDirectory(); const stacksRoot = resolveStoreStacksRoot(); const workdir = process.cwd(); // Resolve the npm binary at schedule time so the script never relies on PATH // inside the restricted systemd environment. const npmBin = await execFileAsync("which", ["npm"]) .then((r) => r.stdout.trim()) .catch(() => "npm"); return scheduleSystemScript( scriptPath, `homeio-factory-reset-${Date.now()}`, { DATABASE_URL: serverEnv.DATABASE_URL, HOMEIO_RESET_DATA_ROOT: dataRoot, HOMEIO_RESET_STACKS_ROOT: stacksRoot, HOMEIO_RESET_WORKDIR: workdir, HOMEIO_RESET_NPM_BIN: npmBin, }, ); } export async function deleteFactoryResetArtifacts() { await rm(FACTORY_RESET_LOG_PATH, { force: true }).catch(() => undefined); }