IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

power-service.ts

Open raw
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<string, string>,
) {
  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<boolean> {
  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);
}
bytes
3,675
lines
123
Type
ts
Latest commit
16 October 2026