feat: restore session

This commit is contained in:
2025-12-18 20:29:45 +02:00
parent 6b92da046b
commit 00aeda045b
7 changed files with 132 additions and 9 deletions

View File

@@ -46,7 +46,7 @@ sudo tee "$GREETD_CONFIG" >/dev/null <<EOF
vt = 1 vt = 1
[default_session] [default_session]
command = "bash -c 'IS_CAGE=1 SDL_VIDEODRIVER=wayland cage -s -- $OUT_BIN --debug --debug-log-file=/deltaboot-debug.txt'" command = "bash -c 'IS_CAGE=1 SDL_AUDIODRIVER=dummy SDL_VIDEODRIVER=wayland cage -s -- $OUT_BIN --debug --debug-log-file=/deltaboot-debug.txt'"
user = "root" user = "root"
EOF EOF

View File

@@ -46,7 +46,7 @@ sudo tee "$GREETD_CONFIG" >/dev/null <<EOF
vt = 1 vt = 1
[default_session] [default_session]
command = "bash -c 'IS_CAGE=1 SDL_VIDEODRIVER=wayland cage -s -- $OUT_BIN'" command = "bash -c 'IS_CAGE=1 SDL_AUDIODRIVER=dummy SDL_VIDEODRIVER=wayland cage -s -- $OUT_BIN'"
user = "root" user = "root"
EOF EOF

View File

@@ -62,6 +62,7 @@ type KeyInput = {
}; };
import type { BootsequenceAnswers } from "../types"; import type { BootsequenceAnswers } from "../types";
import { writeLock } from "../ui/lock";
export type BootSequenceUI = { export type BootSequenceUI = {
update: (deltaMs: number) => void; update: (deltaMs: number) => void;
@@ -243,7 +244,8 @@ export async function createBootSequenceUI(
if (!loggedCompletion) { if (!loggedCompletion) {
loggedCompletion = true; loggedCompletion = true;
console.info("[debug] [bootsequence/questions] finished questions", answers); console.info("[debug] [bootsequence/questions] finished questions", answers);
writeFileSync(join(homedir(), ".deltaboot.json"), JSON.stringify(answers)) writeFileSync(join(homedir(), ".deltaboot.json"), JSON.stringify(answers));
writeLock(answers);
} }
return; return;
} }

View File

@@ -6,6 +6,12 @@ const KNOWN_SESSIONS: Record<string, string> = {
hyprland: "Hyprland", hyprland: "Hyprland",
plasma: "startplasma-wayland" plasma: "startplasma-wayland"
}; };
export const KNOWN_SESSIONS_FRIENDLY: Record<string, string> = {
"hyprland": "Hyprland",
"plasma": "KDE"
};
const GREETD_TIMEOUT_MS = Number(process.env.GREETD_TIMEOUT_MS ?? 5_000); const GREETD_TIMEOUT_MS = Number(process.env.GREETD_TIMEOUT_MS ?? 5_000);
export async function handoffToGreetd(desktopHint?: string): Promise<void> { export async function handoffToGreetd(desktopHint?: string): Promise<void> {

View File

@@ -12,6 +12,8 @@ import {
measureTextWidth measureTextWidth
} from "../../bootsequence/font"; } from "../../bootsequence/font";
import type { RendererInstance, RendererProps } from "../types"; import type { RendererInstance, RendererProps } from "../types";
import { clearLock, readLock, type DeviceContactLock } from "../../ui/lock";
import { handoffToGreetd } from "../../desktop";
/* ---------------- */ /* ---------------- */
@@ -58,6 +60,7 @@ type RecoveryMenuProps = {
question?: string; question?: string;
yesLabel?: string; yesLabel?: string;
noLabel?: string; noLabel?: string;
restoreSession?: DeviceContactLock
}; };
/* ---------------- */ /* ---------------- */
@@ -107,7 +110,8 @@ export function createRecoveryMenuRenderer(
? props.question ? props.question
: "????????/?", : "????????/?",
yesLabel: typeof props.yesLabel === "string" ? props.yesLabel : "Yes", yesLabel: typeof props.yesLabel === "string" ? props.yesLabel : "Yes",
noLabel: typeof props.noLabel === "string" ? props.noLabel : "No" noLabel: typeof props.noLabel === "string" ? props.noLabel : "No",
restoreSession: props.restoreSession as DeviceContactLock || readLock()
}; };
const resources = createLazyResource<MenuResources>(async () => { const resources = createLazyResource<MenuResources>(async () => {
@@ -122,6 +126,7 @@ export function createRecoveryMenuRenderer(
let blinkMs = 0; let blinkMs = 0;
let selection: "yes" | "no" = "yes"; let selection: "yes" | "no" = "yes";
let confirmed = false; let confirmed = false;
let isIniting = false;
const render = async ({ const render = async ({
ctx, ctx,
@@ -245,9 +250,37 @@ export function createRecoveryMenuRenderer(
ctx.restore(); ctx.restore();
}; };
const tryRestoreSession = async () => {
const session = config.restoreSession;
const desktop = session?.answers?.desktop;
if (!desktop) {
console.error("[renderers/recoverymenu] missing restore session");
return;
}
try {
await handoffToGreetd(desktop);
clearLock();
} catch (error) {
console.error("[ui/app] greetd handoff failed\n", error);
console.error("[ui/app] Press CTRL+ALT+F[0-9] to switch to a different VT");
if (process.env.NODE_ENV !== "development") {
process.exit(1);
}
}
};
const onSelect = (dir: "yes" | "no") => { const onSelect = (dir: "yes" | "no") => {
if (isIniting) return;
const changed = dir !== selection; const changed = dir !== selection;
selection = dir; selection = dir;
if (confirmed === true) {
if (dir === "yes") {
isIniting = true;
void tryRestoreSession();
return true;
}
clearLock();
}
void resources.load().then((r) => { void resources.load().then((r) => {
(changed ? r.sndMove : r.sndSelect).play(); (changed ? r.sndMove : r.sndSelect).play();
}); });
@@ -307,7 +340,7 @@ export function createRecoveryMenuRenderer(
return true; return true;
}, },
shouldExit() { shouldExit() {
return confirmed; return confirmed && !isIniting;
}, },
getResult() { getResult() {
return selection; return selection;
@@ -325,6 +358,7 @@ export function createRecoveryMenuRenderer(
return { return {
loaded: resources.isLoaded(), loaded: resources.isLoaded(),
selection, selection,
confirmed,
blinkMs: Number(blinkMs.toFixed(1)) blinkMs: Number(blinkMs.toFixed(1))
}; };
}, },

View File

@@ -3,7 +3,7 @@ import { dirname } from "node:path";
import { type CanvasRenderingContext2D } from "@napi-rs/canvas"; import { type CanvasRenderingContext2D } from "@napi-rs/canvas";
import { createBootSequenceUI } from "../bootsequence/questions"; import { createBootSequenceUI } from "../bootsequence/questions";
import { GREETD_SOCKET, handoffToGreetd } from "../desktop"; import { GREETD_SOCKET, handoffToGreetd, KNOWN_SESSIONS_FRIENDLY } from "../desktop";
import type { BootsequenceAnswers } from "../types"; import type { BootsequenceAnswers } from "../types";
import { loadImageAsset } from "../renderer/assets"; import { loadImageAsset } from "../renderer/assets";
import { parseCli } from "../renderer/cli"; import { parseCli } from "../renderer/cli";
@@ -14,6 +14,7 @@ import { createRenderer } from "../renderer/index";
import { SDLWindow } from "../renderer/window"; import { SDLWindow } from "../renderer/window";
import { createRendererRegistry } from "../renderers"; import { createRendererRegistry } from "../renderers";
import type { RendererInstance, RendererProps } from "../renderers/types"; import type { RendererInstance, RendererProps } from "../renderers/types";
import { clearLock, readLock } from "./lock";
const BUNVERS = `Bun ${Bun.version} ${process.platform} ${process.arch}`; const BUNVERS = `Bun ${Bun.version} ${process.platform} ${process.arch}`;
const DEFAULT_DEBUG_LOG_FILE = "/tmp/device_contact.debug.log"; const DEFAULT_DEBUG_LOG_FILE = "/tmp/device_contact.debug.log";
@@ -43,7 +44,10 @@ export async function runDeviceContactUI(argv: string[] = process.argv) {
console.debug("[debug] ESTABLISHING CONNECTION"); console.debug("[debug] ESTABLISHING CONNECTION");
console.debug("[debug]", BUNVERS); console.debug("[debug]", BUNVERS);
const isCrashRecovery = !!(cli.crashRecoverySession ?? process.env.CRASH_RECOVERY_SESSION); const crashLock = readLock();
const crashDesktop = crashLock?.answers?.desktop;
const isCrashRecovery = !!(cli.crashRecoverySession ?? process.env.CRASH_RECOVERY_SESSION ?? crashDesktop);
const forcedErrorScreen = Boolean(cli.errorScreenRequested); const forcedErrorScreen = Boolean(cli.errorScreenRequested);
const requestedRenderer = cli.rendererId ?? "device_contact"; const requestedRenderer = cli.rendererId ?? "device_contact";
const defaultRendererId = forcedErrorScreen const defaultRendererId = forcedErrorScreen
@@ -98,7 +102,12 @@ export async function runDeviceContactUI(argv: string[] = process.argv) {
const viewWidth = 1280; const viewWidth = 1280;
const viewHeight = 960; const viewHeight = 960;
const uiScale = 0.6; const uiScale = 0.6;
const crashRecoverySession = cli.crashRecoverySession ?? process.env.CRASH_RECOVERY_SESSION; const crashRecoverySession =
cli.crashRecoverySession ??
process.env.CRASH_RECOVERY_SESSION ??
(crashDesktop
? KNOWN_SESSIONS_FRIENDLY[crashDesktop] ?? crashDesktop
: undefined);
const renderer = createRenderer({ window }); const renderer = createRenderer({ window });
renderer.ctx.imageSmoothingEnabled = false; renderer.ctx.imageSmoothingEnabled = false;
@@ -110,7 +119,8 @@ export async function runDeviceContactUI(argv: string[] = process.argv) {
: "?????", : "?????",
yesLabel: "Yes", yesLabel: "Yes",
noLabel: "No", noLabel: "No",
session: crashRecoverySession session: crashRecoverySession,
restoreSession: crashLock ?? undefined
}, },
errormessage: { errormessage: {
title: cli.errorScreenTitle ?? process.env.ERROR_TITLE ?? "ERROR", title: cli.errorScreenTitle ?? process.env.ERROR_TITLE ?? "ERROR",
@@ -312,6 +322,7 @@ Options:
const desktopHint = typeof desktopHintRaw === "string" ? desktopHintRaw : undefined; const desktopHint = typeof desktopHintRaw === "string" ? desktopHintRaw : undefined;
try { try {
await handoffToGreetd(desktopHint); await handoffToGreetd(desktopHint);
// clearLock();
} catch (error) { } catch (error) {
console.error("[ui/app] greetd handoff failed\n", error); console.error("[ui/app] greetd handoff failed\n", error);
console.error("[ui/app] Press CTRL+ALT+F[0-9] to switch to a different VT"); console.error("[ui/app] Press CTRL+ALT+F[0-9] to switch to a different VT");

70
src/ui/lock.ts Normal file
View File

@@ -0,0 +1,70 @@
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
import { dirname, join } from "path";
import type { BootsequenceAnswers } from "../types";
const dir = "/run/deltabootd";
const LOCK_PATH = join(dir, "DEVICE_CONTACT.lock");
try {
mkdirSync(dir, { recursive: true, mode: 0o755 });
} catch { }
export type DeviceContactLock = {
lock: true,
answers: BootsequenceAnswers
};
function ensureDir() {
const dir = dirname(LOCK_PATH);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true, mode: 0o755 });
}
}
function isBootsequenceAnswers(value: any): value is BootsequenceAnswers {
return Boolean(
value &&
typeof value === "object" &&
typeof value.char === "string" &&
typeof value.desktop === "string" &&
typeof value.color === "string" &&
typeof value.gift === "string"
);
}
export function readLock(): DeviceContactLock | null {
try {
if (!existsSync(LOCK_PATH)) return null;
const raw = readFileSync(LOCK_PATH, "utf8");
const parsed = JSON.parse(raw) as Partial<DeviceContactLock>;
if (!parsed || parsed.lock !== true) return null;
if (!isBootsequenceAnswers((parsed as any).answers)) return null;
return {
lock: true,
answers: parsed.answers!
};
} catch {
return null;
}
}
export function writeLock(answers: BootsequenceAnswers): void {
ensureDir();
const lock: DeviceContactLock = {
lock: true,
answers
};
writeFileSync(LOCK_PATH, JSON.stringify(lock), {
mode: 0o644,
});
}
export function clearLock(): void {
try {
if (existsSync(LOCK_PATH)) {
unlinkSync(LOCK_PATH);
}
} catch { }
}