feat: restore session
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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))
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
70
src/ui/lock.ts
Normal 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 { }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user