Lumi/src/services/recovery-mode.js
2026-06-16 09:44:16 +02:00

133 lines
3.4 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const repoRoot = path.join(__dirname, "..", "..");
const recoveryDir = path.join(repoRoot, "data", "recovery");
const markerPath = path.join(recoveryDir, "update-marker.json");
const safeModeFlagPath = path.join(recoveryDir, "safe-mode.flag");
function ensureRecoveryDir() {
fs.mkdirSync(recoveryDir, { recursive: true });
}
function readRecoveryMarker() {
try {
if (!fs.existsSync(markerPath)) return null;
const marker = JSON.parse(fs.readFileSync(markerPath, "utf8"));
return marker && typeof marker === "object" ? marker : null;
} catch {
return {
id: "unreadable",
status: "failed",
error: "Recovery marker could not be parsed.",
marker_path: markerPath
};
}
}
function writeRecoveryMarker(marker) {
ensureRecoveryDir();
const next = {
id: marker.id || crypto.randomUUID(),
status: marker.status || "pending",
updated_at: new Date().toISOString(),
...marker
};
fs.writeFileSync(markerPath, JSON.stringify(next, null, 2), "utf8");
return next;
}
function createRecoveryMarker(details) {
return writeRecoveryMarker({
id: crypto.randomUUID(),
status: "pending",
created_at: new Date().toISOString(),
...details
});
}
function updateRecoveryMarker(values) {
const current = readRecoveryMarker() || {};
return writeRecoveryMarker({ ...current, ...values });
}
function markRecoveryMarkerFailed(error) {
return updateRecoveryMarker({
status: "failed",
error: error?.message || String(error || "Update failed.")
});
}
function markRecoveryMarkerComplete(values = {}) {
return updateRecoveryMarker({
...values,
status: "completed",
completed_at: new Date().toISOString()
});
}
function clearRecoveryMarker() {
try {
fs.rmSync(markerPath, { force: true });
} catch {
// ignore cleanup failures
}
}
function markStartupVerification() {
const marker = readRecoveryMarker();
if (!marker) return null;
if (marker.status === "completed") {
clearRecoveryMarker();
return { ...marker, cleared: true };
}
if (["pending", "applying", "verifying"].includes(marker.status)) {
return writeRecoveryMarker({
...marker,
status: "stale",
stale_at: new Date().toISOString(),
error: marker.error || "Previous update did not complete startup verification."
});
}
return marker;
}
function isSafeModeRequested(argv = process.argv, env = process.env) {
return env.LUMI_SAFE_MODE === "1" ||
env.SAFE_MODE === "1" ||
argv.includes("--safe-mode") ||
fs.existsSync(safeModeFlagPath);
}
function safeModeStatus() {
const marker = readRecoveryMarker();
const requested = isSafeModeRequested();
const incomplete = marker && ["pending", "applying", "verifying", "failed", "stale"].includes(marker.status);
return {
requested,
active: process.env.SAFE_MODE === "1" || process.env.LUMI_SAFE_MODE === "1",
marker,
has_incomplete_marker: Boolean(incomplete),
safe_mode_flag: fs.existsSync(safeModeFlagPath),
marker_path: markerPath,
flag_path: safeModeFlagPath
};
}
module.exports = {
recoveryDir,
markerPath,
safeModeFlagPath,
readRecoveryMarker,
writeRecoveryMarker,
createRecoveryMarker,
updateRecoveryMarker,
markRecoveryMarkerFailed,
markRecoveryMarkerComplete,
clearRecoveryMarker,
markStartupVerification,
isSafeModeRequested,
safeModeStatus
};