586 lines
18 KiB
TypeScript
586 lines
18 KiB
TypeScript
import { createWriteStream, mkdirSync } from "node:fs";
|
|
import { dirname } from "node:path";
|
|
import { type CanvasRenderingContext2D } from "@napi-rs/canvas";
|
|
|
|
import { createBootSequenceUI } from "../bootsequence/questions";
|
|
import { GREETD_SOCKET, handoffToGreetd, KNOWN_SESSIONS_FRIENDLY } from "../desktop";
|
|
import type { BootsequenceAnswers } from "../types";
|
|
import { loadImageAsset } from "../renderer/assets";
|
|
import { parseCli } from "../renderer/cli";
|
|
import { createDebugHud } from "../renderer/debug-hud";
|
|
import { createFpsCounter } from "../renderer/fps";
|
|
import { createLayoutCalculator } from "../renderer/layout";
|
|
import { createRenderer } from "../renderer/index";
|
|
import { SDLWindow } from "../renderer/window";
|
|
import { createRendererRegistry } from "../renderers";
|
|
import type { RendererInstance, RendererProps } from "../renderers/types";
|
|
import { clearLock, readLock } from "./lock";
|
|
|
|
const BUNVERS = `Bun ${Bun.version} ${process.platform} ${process.arch}`;
|
|
const DEFAULT_DEBUG_LOG_FILE = "/tmp/device_contact.debug.log";
|
|
const LOG_LIFETIME_MS = 8_000;
|
|
const LOG_FADE_MS = 3_000;
|
|
const LOG_MAX_LINES = 64;
|
|
const debugLogEntries: DebugLogEntry[] = [];
|
|
|
|
export async function runDeviceContactUI(argv: string[] = process.argv) {
|
|
const cli = parseCli(argv);
|
|
const isDev = process.env.NODE_ENV === "development";
|
|
const debugOptions = {
|
|
showGlobal: isDev || cli.debugGlobalHud || process.env.DEBUG_UI === "true",
|
|
showRenderer: isDev || cli.debugRendererHud || process.env.DEBUG_RENDERER === "true" || cli.debugGlobalHud,
|
|
showCustom: isDev || cli.debugGlobalHud || process.env.DEBUG_UI === "true"
|
|
};
|
|
const debugLoggingEnabled = debugHudOptionsEnabled(debugOptions);
|
|
const debugLogFile =
|
|
cli.debugLogFile ??
|
|
process.env.DEBUG_LOG_FILE ??
|
|
(debugLoggingEnabled ? DEFAULT_DEBUG_LOG_FILE : undefined);
|
|
const restoreDebug =
|
|
debugLoggingEnabled || debugLogFile
|
|
? hookDebugLogs({ filePath: debugLogFile })
|
|
: () => { };
|
|
|
|
console.debug("[debug] ESTABLISHING CONNECTION");
|
|
console.debug("[debug]", BUNVERS);
|
|
|
|
const crashLock = readLock();
|
|
|
|
const crashDesktop = crashLock?.answers?.desktop;
|
|
const isCrashRecovery = !!(cli.crashRecoverySession ?? process.env.CRASH_RECOVERY_SESSION ?? crashDesktop);
|
|
const forcedErrorScreen = Boolean(cli.errorScreenRequested);
|
|
const requestedRenderer = cli.rendererId ?? "device_contact";
|
|
const defaultRendererId = forcedErrorScreen
|
|
? "errormessage"
|
|
: isCrashRecovery
|
|
? "recoverymenu"
|
|
: requestedRenderer;
|
|
const shouldRunBootSequence = defaultRendererId !== "recoverymenu" && defaultRendererId !== "errormessage";
|
|
const isTTY = process.env.SDL_VIDEODRIVER === "kmsdrm";
|
|
const isCage = process.env.IS_CAGE === "true";
|
|
|
|
const windowOptions = {
|
|
// DO NOT CHANGE TITLE
|
|
title: "DEVICE CONTACT (DELTARUNE Chapter 1)",
|
|
width: 1920,
|
|
height: 1080,
|
|
fullscreen: true
|
|
};
|
|
|
|
const window = new SDLWindow(windowOptions);
|
|
window.Window.setFullscreen(true);
|
|
|
|
// will segfault bun if ran in tty
|
|
if (!isTTY && !isCage) {
|
|
window.on("keyUp", (e) => {
|
|
if (e.key === "f11") {
|
|
window.Window.setFullscreen(!window.Window.fullscreen);
|
|
}
|
|
});
|
|
}
|
|
|
|
const icon = await loadImageAsset("icon.png");
|
|
window.setIconFromImage(icon);
|
|
window.Window.setResizable(true);
|
|
window.Window.setAccelerated(true);
|
|
|
|
if (isTTY) {
|
|
console.debug("[debug] KMSDRM detected, What the fuck?? Deltarune in the TTY?");
|
|
}
|
|
if (isCage) {
|
|
console.debug("[debug] Cage detected, are you trying to make a login manager or something?");
|
|
setInterval(() => {
|
|
try {
|
|
if (!window.Window.fullscreen) window.Window.setFullscreen(true);
|
|
} catch { }
|
|
}, 100)
|
|
}
|
|
|
|
// Base dim for UI/layout (matches the original background logical size).
|
|
const baseWidth = 160;
|
|
const baseHeight = 120;
|
|
const viewWidth = 1280;
|
|
const viewHeight = 960;
|
|
const uiScale = 0.6;
|
|
const crashRecoverySession =
|
|
cli.crashRecoverySession ??
|
|
process.env.CRASH_RECOVERY_SESSION ??
|
|
(crashDesktop
|
|
? KNOWN_SESSIONS_FRIENDLY[crashDesktop] ?? crashDesktop
|
|
: undefined);
|
|
|
|
const renderer = createRenderer({ window });
|
|
renderer.ctx.imageSmoothingEnabled = false;
|
|
|
|
const rendererPropsById: Record<string, RendererProps> = {
|
|
recoverymenu: {
|
|
question: crashRecoverySession
|
|
? `${crashRecoverySession} crashed. Do you want to restart it?`
|
|
: "?????",
|
|
yesLabel: "Yes",
|
|
noLabel: "No",
|
|
session: crashRecoverySession,
|
|
restoreSession: crashLock ?? undefined
|
|
},
|
|
errormessage: {
|
|
title: cli.errorScreenTitle ?? process.env.ERROR_TITLE ?? "ERROR",
|
|
message: cli.errorScreenMessage ?? process.env.ERROR_MESSAGE ?? "An unexpected error occurred.",
|
|
hint: cli.errorScreenHint ?? process.env.ERROR_HINT ?? "Switch between VT's with CTRL+ALT+F[0-9]."
|
|
}
|
|
};
|
|
|
|
const rendererRegistry = createRendererRegistry({ rendererProps: rendererPropsById });
|
|
let activeRenderer: RendererInstance | null = null;
|
|
let rendererExit: { id: string; result: unknown } | null = null;
|
|
let fatalErrorProps: RendererProps | null = null;
|
|
const requireActiveRenderer = () => {
|
|
if (!activeRenderer) {
|
|
throw new Error("Active renderer not initialized");
|
|
}
|
|
return activeRenderer;
|
|
};
|
|
const logSelectedRenderer = () => {
|
|
const current = requireActiveRenderer();
|
|
console.debug("[debug] renderer selected", current.id);
|
|
};
|
|
if (crashRecoverySession) {
|
|
console.debug("[debug] crash recovery mode", crashRecoverySession);
|
|
}
|
|
if (debugLogFile) {
|
|
console.debug("[debug] writing debug log to", debugLogFile);
|
|
}
|
|
|
|
const debugHud = createDebugHud(debugOptions);
|
|
const globalFps = createFpsCounter();
|
|
const rendererFps = createFpsCounter();
|
|
|
|
const getLayout = createLayoutCalculator({
|
|
baseWidth,
|
|
baseHeight,
|
|
viewWidth,
|
|
viewHeight
|
|
});
|
|
|
|
let bootUI = shouldRunBootSequence
|
|
? await createBootSequenceUI(baseWidth, baseHeight)
|
|
: null;
|
|
let bootAnswers: BootsequenceAnswers | null = null;
|
|
let contactComplete = false;
|
|
|
|
const rendererIds = Array.from(new Set(rendererRegistry.list().map((r) => r.id)));
|
|
let lastFrameMs = Date.now();
|
|
const requestRendererExit = () => {
|
|
const current = requireActiveRenderer();
|
|
if (rendererExit) return;
|
|
rendererExit = {
|
|
id: current.id,
|
|
result: current.getResult ? current.getResult() : undefined
|
|
};
|
|
renderer.requestStop();
|
|
};
|
|
const switchRenderer = (id: string) => {
|
|
if (activeRenderer && id === activeRenderer.id) return activeRenderer;
|
|
rendererExit = null;
|
|
const next = rendererRegistry.switchTo(id, rendererPropsById[id]);
|
|
renderer.ctx.clearRect(0, 0, renderer.canvas.width, renderer.canvas.height);
|
|
activeRenderer = next;
|
|
lastFrameMs = Date.now();
|
|
return activeRenderer;
|
|
};
|
|
activeRenderer = switchRenderer(defaultRendererId);
|
|
logSelectedRenderer();
|
|
|
|
if (cli.helpRequested) {
|
|
console.log(`Usage: bun run src/ui/app.ts [options]
|
|
Options:
|
|
--renderer <id> Select renderer by id (default: ${defaultRendererId})
|
|
--debug Enable all debug HUD panels
|
|
--debug-global Enable global debug HUD
|
|
--debug-renderer Enable renderer debug HUD
|
|
--error-screen [msg] Start on error screen (optional message)
|
|
--error-title <t> Set error screen title
|
|
--error-hint <h> Set error screen hint
|
|
--debug-log-file <path> Write debug logs to file (default: ${DEFAULT_DEBUG_LOG_FILE})
|
|
--crash-recovery [id] Start in crash recovery mode (optional session id)
|
|
--help, -h Show this help message`);
|
|
process.exit(0);
|
|
}
|
|
|
|
window.on("keyDown", (e) => {
|
|
const currentRenderer = requireActiveRenderer();
|
|
currentRenderer.handleKey?.(e.key ?? null);
|
|
bootUI?.handleKey({
|
|
key: e.key ?? null,
|
|
scancode: e.scancode ?? 0,
|
|
ctrl: e.ctrl ?? 0,
|
|
shift: e.shift ?? 0,
|
|
alt: e.alt ?? 0,
|
|
super: e.super ?? 0
|
|
});
|
|
});
|
|
|
|
const drawFrame = async (
|
|
_ctx: CanvasRenderingContext2D,
|
|
size: { width: number; height: number }
|
|
): Promise<void> => {
|
|
const currentRenderer = requireActiveRenderer();
|
|
const { ctx } = renderer;
|
|
ctx.imageSmoothingEnabled = false;
|
|
const layout = getLayout(size);
|
|
const {
|
|
width,
|
|
height,
|
|
contentScale,
|
|
x,
|
|
y
|
|
} = layout;
|
|
const now = Date.now();
|
|
const deltaMs = now - lastFrameMs;
|
|
lastFrameMs = now;
|
|
globalFps.tick(now);
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
await currentRenderer.render({ ctx, deltaMs, layout });
|
|
rendererFps.tick(now);
|
|
if (currentRenderer.shouldExit?.()) {
|
|
if (currentRenderer.id === "recoverymenu") {
|
|
activeRenderer = switchRenderer("device_contact");
|
|
logSelectedRenderer();
|
|
if (!bootUI) {
|
|
bootUI = await createBootSequenceUI(baseWidth, baseHeight);
|
|
}
|
|
} else {
|
|
requestRendererExit();
|
|
}
|
|
}
|
|
|
|
// Text/UI layer: above BG/overlay, below FPS.
|
|
ctx.save();
|
|
const uiOffsetX = (contentScale - contentScale * uiScale) * baseWidth * 0.5;
|
|
const uiOffsetY = (contentScale - contentScale * uiScale) * baseHeight * 0.5;
|
|
ctx.translate(x + uiOffsetX, y + uiOffsetY);
|
|
ctx.scale(contentScale * uiScale, contentScale * uiScale);
|
|
if (bootUI) {
|
|
bootUI.update(deltaMs);
|
|
bootUI.render(ctx);
|
|
if (bootUI.isFinished()) {
|
|
if (!bootAnswers) {
|
|
bootAnswers = bootUI.getAnswers();
|
|
}
|
|
contactComplete = true;
|
|
renderer.requestStop();
|
|
window.Window.destroy();
|
|
}
|
|
}
|
|
ctx.restore();
|
|
|
|
debugHud.draw(ctx, layout, {
|
|
global: [
|
|
`${globalFps.value.toFixed(2)} FPS`,
|
|
`${window.Window.display.name ? (process.env.MONITOR_SN ? window.Window.display.name.replaceAll((process.env.MONITOR_SN || "") + " ", "") : window.Window.display.name) : "unknown"} ${window.Window.display.frequency}hz [${process.env.SDL_VIDEODRIVER ?? "sdl2"}]`,
|
|
`activeRenderer: ${currentRenderer.id}`,
|
|
`crashRecoverySession: ${crashRecoverySession ?? "none"}`,
|
|
`${BUNVERS}`
|
|
],
|
|
renderer: {
|
|
id: currentRenderer.id,
|
|
label: currentRenderer.label,
|
|
stats: {
|
|
...(currentRenderer.getDebugStats ? currentRenderer.getDebugStats() : {}),
|
|
...(currentRenderer.getDebugHudStats ? currentRenderer.getDebugHudStats() : {})
|
|
},
|
|
fps: rendererFps.value
|
|
},
|
|
custom: {
|
|
greetdSocket: GREETD_SOCKET,
|
|
tty: isTTY,
|
|
cage: isCage
|
|
}
|
|
});
|
|
if (debugLoggingEnabled) {
|
|
drawDebugLogs(ctx, layout, now);
|
|
}
|
|
};
|
|
|
|
console.debug("[debug] reached main");
|
|
try {
|
|
await renderer.run(drawFrame);
|
|
if (rendererExit) {
|
|
console.debug("[debug] renderer exit requested", rendererExit);
|
|
}
|
|
} finally {
|
|
requireActiveRenderer().unload();
|
|
}
|
|
|
|
if (contactComplete) {
|
|
const desktopHintRaw =
|
|
(bootAnswers as BootsequenceAnswers | null | undefined)?.desktop ??
|
|
crashRecoverySession ??
|
|
process.env.DESKTOP_SESSION_FRIENDLY_NAME ??
|
|
process.env.XDG_CURRENT_DESKTOP;
|
|
const desktopHint = typeof desktopHintRaw === "string" ? desktopHintRaw : undefined;
|
|
try {
|
|
await handoffToGreetd(desktopHint);
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fatalErrorProps) {
|
|
return
|
|
}
|
|
|
|
restoreDebug();
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
await runDeviceContactUI();
|
|
}
|
|
|
|
type DebugLogEntry = {
|
|
message: string;
|
|
ts: number;
|
|
};
|
|
|
|
function debugHudOptionsEnabled(options: { showGlobal: boolean; showRenderer: boolean; showCustom?: boolean }) {
|
|
return options.showGlobal || options.showRenderer || Boolean(options.showCustom);
|
|
}
|
|
|
|
function hookDebugLogs(options?: { filePath?: string }): () => void {
|
|
const originalDebug = console.debug;
|
|
let logStream = createDebugLogStream(options?.filePath, originalDebug);
|
|
if (logStream) {
|
|
logStream.on("error", (error) => {
|
|
originalDebug("[debug] debug log stream error", error);
|
|
logStream?.destroy();
|
|
logStream = null;
|
|
});
|
|
}
|
|
|
|
console.debug = (...args: unknown[]) => {
|
|
const message = formatDebugMessage(args);
|
|
const now = Date.now();
|
|
debugLogEntries.push({ message, ts: now });
|
|
pruneOldLogs(now);
|
|
if (debugLogEntries.length > 100) {
|
|
debugLogEntries.splice(0, debugLogEntries.length - 100);
|
|
}
|
|
if (logStream) {
|
|
try {
|
|
logStream.write(`[${new Date(now).toISOString()}] ${message}\n`);
|
|
} catch (error) {
|
|
originalDebug("[debug] failed to write debug log to file", error);
|
|
logStream = null;
|
|
}
|
|
}
|
|
originalDebug(...args);
|
|
};
|
|
return () => {
|
|
console.debug = originalDebug;
|
|
debugLogEntries.length = 0;
|
|
if (logStream) {
|
|
logStream.end();
|
|
logStream = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
function formatDebugMessage(args: unknown[]): string {
|
|
return args
|
|
.map((arg) => {
|
|
if (typeof arg === "string") return arg;
|
|
if (arg instanceof Error) {
|
|
return `${arg.name}: ${arg.message}`;
|
|
}
|
|
try {
|
|
return JSON.stringify(arg, (_k, v) => {
|
|
if (typeof v === "bigint") return v.toString();
|
|
return v;
|
|
});
|
|
} catch {
|
|
return String(arg);
|
|
}
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
function drawDebugLogs(ctx: CanvasRenderingContext2D, layout: { width: number; height: number }, now: number) {
|
|
const padding = 8;
|
|
const lineHeight = 18;
|
|
pruneOldLogs(now);
|
|
const visible = debugLogEntries.slice(-LOG_MAX_LINES);
|
|
if (visible.length === 0) return;
|
|
|
|
ctx.save();
|
|
ctx.font = "14px \"JetBrains Mono\", monospace";
|
|
ctx.textBaseline = "bottom";
|
|
|
|
let cursorY = layout.height - padding;
|
|
const cursorX = padding;
|
|
|
|
for (const entry of [...visible].reverse()) {
|
|
const age = now - entry.ts;
|
|
const fadeStart = LOG_LIFETIME_MS - LOG_FADE_MS;
|
|
const alpha = age <= fadeStart ? 1 : Math.max(0, (LOG_LIFETIME_MS - age) / LOG_FADE_MS);
|
|
const text = entry.message;
|
|
|
|
ctx.globalAlpha = alpha;
|
|
ctx.fillStyle = "#66ccff";
|
|
ctx.fillText(text, cursorX, cursorY);
|
|
cursorY -= lineHeight;
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function pruneOldLogs(now: number) {
|
|
for (let i = debugLogEntries.length - 1; i >= 0; i--) {
|
|
if (now - debugLogEntries[i]!.ts >= LOG_LIFETIME_MS) {
|
|
debugLogEntries.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function runErrorScreen(
|
|
props: RendererProps,
|
|
options?: { debugOptions?: { showGlobal: boolean; showRenderer: boolean; showCustom?: boolean } }
|
|
) {
|
|
const isTTY = process.env.SDL_VIDEODRIVER === "kmsdrm";
|
|
const isCage = process.env.IS_CAGE === "true";
|
|
const windowOptions = {
|
|
// DO NOT CHANGE TITLE
|
|
title: "DEVICE CONTACT (DELTARUNE Chapter 1)",
|
|
width: 1920,
|
|
height: 1080,
|
|
fullscreen: true
|
|
};
|
|
const window = new SDLWindow(windowOptions);
|
|
window.Window.setFullscreen(true);
|
|
if (!isTTY && !isCage) {
|
|
window.on("keyUp", (e) => {
|
|
if (e.key === "f11") {
|
|
window.Window.setFullscreen(!window.Window.fullscreen);
|
|
}
|
|
});
|
|
}
|
|
const icon = await loadImageAsset("icon.png");
|
|
window.setIconFromImage(icon);
|
|
window.Window.setResizable(true);
|
|
window.Window.setAccelerated(true);
|
|
|
|
const renderer = createRenderer({ window });
|
|
const rendererRegistry = createRendererRegistry({ rendererProps: { errormessage: props } });
|
|
let activeRenderer: RendererInstance | null = rendererRegistry.switchTo("errormessage");
|
|
let rendererExit: { id: string; result: unknown } | null = null;
|
|
const requestRendererExit = () => {
|
|
const current = activeRenderer;
|
|
if (!current || rendererExit) return;
|
|
rendererExit = {
|
|
id: current.id,
|
|
result: current.getResult ? current.getResult() : undefined
|
|
};
|
|
renderer.requestStop();
|
|
};
|
|
window.on("keyDown", (e) => {
|
|
activeRenderer?.handleKey?.(e.key ?? null);
|
|
});
|
|
|
|
const baseWidth = 160;
|
|
const baseHeight = 120;
|
|
const viewWidth = 1280;
|
|
const viewHeight = 960;
|
|
const uiScale = 0.6;
|
|
const getLayout = createLayoutCalculator({
|
|
baseWidth,
|
|
baseHeight,
|
|
viewWidth,
|
|
viewHeight
|
|
});
|
|
const debugHud = createDebugHud(
|
|
options?.debugOptions ?? {
|
|
showGlobal: true,
|
|
showRenderer: true,
|
|
showCustom: true
|
|
}
|
|
);
|
|
const globalFps = createFpsCounter();
|
|
const rendererFps = createFpsCounter();
|
|
let lastFrameMs = Date.now();
|
|
|
|
const drawFrame = async (_ctx: CanvasRenderingContext2D, size: { width: number; height: number }) => {
|
|
const currentRenderer = activeRenderer!;
|
|
const { ctx } = renderer;
|
|
ctx.imageSmoothingEnabled = false;
|
|
const layout = getLayout(size);
|
|
const { width, height, contentScale, x, y } = layout;
|
|
const now = Date.now();
|
|
const deltaMs = now - lastFrameMs;
|
|
lastFrameMs = now;
|
|
globalFps.tick(now);
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
await currentRenderer.render({ ctx, deltaMs, layout });
|
|
rendererFps.tick(now);
|
|
if (currentRenderer.shouldExit?.()) {
|
|
requestRendererExit();
|
|
}
|
|
|
|
ctx.save();
|
|
const uiOffsetX = (contentScale - contentScale * uiScale) * baseWidth * 0.5;
|
|
const uiOffsetY = (contentScale - contentScale * uiScale) * baseHeight * 0.5;
|
|
ctx.translate(x + uiOffsetX, y + uiOffsetY);
|
|
ctx.scale(contentScale * uiScale, contentScale * uiScale);
|
|
ctx.restore();
|
|
|
|
debugHud.draw(ctx, layout, {
|
|
global: [
|
|
`${globalFps.value.toFixed(2)} FPS`,
|
|
`${window.Window.display.name ? (process.env.MONITOR_SN ? window.Window.display.name.replaceAll((process.env.MONITOR_SN || "") + " ", "") : window.Window.display.name) : "unknown"} ${window.Window.display.frequency}hz [${process.env.SDL_VIDEODRIVER ?? "sdl2"}]`,
|
|
`activeRenderer: ${currentRenderer.id}`,
|
|
`${BUNVERS}`
|
|
],
|
|
renderer: {
|
|
id: currentRenderer.id,
|
|
label: currentRenderer.label,
|
|
stats: {
|
|
...(currentRenderer.getDebugStats ? currentRenderer.getDebugStats() : {}),
|
|
...(currentRenderer.getDebugHudStats ? currentRenderer.getDebugHudStats() : {})
|
|
},
|
|
fps: rendererFps.value
|
|
},
|
|
custom: {
|
|
greetdSocket: GREETD_SOCKET,
|
|
tty: isTTY,
|
|
cage: isCage
|
|
}
|
|
});
|
|
if (debugHudOptionsEnabled(options?.debugOptions ?? { showGlobal: true, showRenderer: true, showCustom: true })) {
|
|
drawDebugLogs(ctx, layout, now);
|
|
}
|
|
};
|
|
|
|
await renderer.run(drawFrame);
|
|
if (rendererExit) {
|
|
console.debug("[debug] error renderer exit requested", rendererExit);
|
|
}
|
|
activeRenderer?.unload();
|
|
}
|
|
|
|
function createDebugLogStream(filePath: string | undefined, debug: typeof console.debug) {
|
|
if (!filePath) return null;
|
|
try {
|
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
return createWriteStream(filePath, { flags: "a" });
|
|
} catch (error) {
|
|
debug("[debug] failed to open debug log file", { filePath, error });
|
|
return null;
|
|
}
|
|
}
|