feat(core): unified crash system using platform provider

This commit is contained in:
Elian Doran
2026-03-26 18:17:24 +02:00
parent 48219f54fc
commit afe597c811
12 changed files with 75 additions and 23 deletions

View File

@@ -0,0 +1,11 @@
import type { PlatformProvider } from "@triliumnext/core";
export default class StandalonePlatformProvider implements PlatformProvider {
crash(message: string): void {
console.error("[Standalone] FATAL:", message);
self.postMessage({
type: "FATAL_ERROR",
message
});
}
}

View File

@@ -2,6 +2,10 @@ import LocalServerWorker from "./local-server-worker?worker";
let localWorker: Worker | null = null;
const pending = new Map();
function showFatalErrorDialog(message: string) {
alert(message);
}
export function startLocalServerWorker() {
if (localWorker) return localWorker;
localWorker = new LocalServerWorker();
@@ -19,6 +23,13 @@ export function startLocalServerWorker() {
localWorker.onmessage = (event) => {
const msg = event.data;
// Handle fatal platform crashes (shown as a dialog to the user)
if (msg?.type === "FATAL_ERROR") {
console.error("[LocalBridge] Fatal error:", msg.message);
showFatalErrorDialog(msg.message);
return;
}
// Handle worker error reports
if (msg?.type === "WORKER_ERROR") {
console.error("[LocalBridge] Worker reported error:", msg.error);

View File

@@ -56,6 +56,7 @@ let WorkerMessagingProvider: typeof import('./lightweight/messaging_provider').d
let BrowserExecutionContext: typeof import('./lightweight/cls_provider').default;
let BrowserCryptoProvider: typeof import('./lightweight/crypto_provider').default;
let FetchRequestProvider: typeof import('./lightweight/request_provider').default;
let StandalonePlatformProvider: typeof import('./lightweight/platform_provider').default;
let translationProvider: typeof import('./lightweight/translation_provider').default;
let createConfiguredRouter: typeof import('./lightweight/browser_routes').createConfiguredRouter;
@@ -81,6 +82,7 @@ async function loadModules(): Promise<void> {
clsModule,
cryptoModule,
requestModule,
platformModule,
translationModule,
routesModule
] = await Promise.all([
@@ -89,6 +91,7 @@ async function loadModules(): Promise<void> {
import('./lightweight/cls_provider.js'),
import('./lightweight/crypto_provider.js'),
import('./lightweight/request_provider.js'),
import('./lightweight/platform_provider.js'),
import('./lightweight/translation_provider.js'),
import('./lightweight/browser_routes.js')
]);
@@ -98,6 +101,7 @@ async function loadModules(): Promise<void> {
BrowserExecutionContext = clsModule.default;
BrowserCryptoProvider = cryptoModule.default;
FetchRequestProvider = requestModule.default;
StandalonePlatformProvider = platformModule.default;
translationProvider = translationModule.default;
createConfiguredRouter = routesModule.createConfiguredRouter;
@@ -149,6 +153,7 @@ async function initialize(): Promise<void> {
crypto: new BrowserCryptoProvider(),
messaging: messagingProvider!,
request: new FetchRequestProvider(),
platform: new StandalonePlatformProvider(),
translations: translationProvider,
schema: schemaModule.default,
dbConfig: {

View File

@@ -18,6 +18,7 @@ import path, { join } from "path";
import { deferred, LOCALES } from "../../../packages/commons/src";
import { PRODUCT_NAME } from "./app-info";
import DesktopPlatformProvider from "./platform_provider";
async function main() {
const userDataPath = getUserData();
@@ -136,6 +137,7 @@ async function main() {
executionContext: new ClsHookedExecutionContext(),
messaging: new WebSocketMessagingProvider(),
schema: fs.readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
platform: new DesktopPlatformProvider(),
translations: (await import("@triliumnext/server/src/services/i18n.js")).initializeTranslations,
extraAppInfo: {
nodeVersion: process.version,

View File

@@ -0,0 +1,9 @@
import { PlatformProvider, t } from "@triliumnext/core";
import electron from "electron";
export default class DesktopPlatformProvider implements PlatformProvider {
crash(message: string): void {
electron.dialog.showErrorBox(t("modals.error_title"), message);
electron.app.exit(1);
}
}

View File

@@ -10,6 +10,7 @@ import path from "path";
import ClsHookedExecutionContext from "./cls_provider.js";
import NodejsCryptoProvider from "./crypto_provider.js";
import ServerPlatformProvider from "./platform_provider.js";
import dataDirs from "./services/data_dir.js";
import port from "./services/port.js";
import NodeRequestProvider from "./services/request.js";
@@ -54,6 +55,7 @@ async function startApplication() {
executionContext: new ClsHookedExecutionContext(),
messaging: new WebSocketMessagingProvider(),
schema: fs.readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
platform: new ServerPlatformProvider(),
translations: (await import("./services/i18n.js")).initializeTranslations,
extraAppInfo: {
nodeVersion: process.version,

View File

@@ -0,0 +1,8 @@
import { getLog, PlatformProvider } from "@triliumnext/core";
export default class ServerPlatformProvider implements PlatformProvider {
crash(message: string): void {
getLog().error(message);
process.exit(1);
}
}

View File

@@ -99,17 +99,6 @@ export function stripTags(text: string) {
return text.replace(/<(?:.|\n)*?>/gm, "");
}
export async function crash(message: string) {
if (isElectron) {
const electron = await import("electron");
electron.dialog.showErrorBox(t("modals.error_title"), message);
electron.app.exit(1);
} else {
log.error(message);
process.exit(1);
}
}
/** @deprecated */
export function getContentDisposition(filename: string) {
return coreUtils.getContentDisposition(filename);
@@ -405,7 +394,6 @@ export function waitForStreamToFinish(stream: any): Promise<void> {
export default {
compareVersions,
constantTimeCompare,
crash,
envToBoolean,
escapeHtml,
escapeRegExp,

View File

@@ -8,6 +8,7 @@ import { initRequest, RequestProvider } from "./services/request";
import { initTranslations, TranslationProvider } from "./services/i18n";
import { initSchema } from "./services/sql_init";
import appInfo from "./services/app_info";
import PlatformProvider, { initPlatform } from "./services/platform";
export { getLog } from "./services/log";
export type * from "./services/sql/types";
@@ -90,13 +91,16 @@ export { default as consistency_checks } from "./services/consistency_checks";
export { default as content_hash } from "./services/content_hash";
export { default as sync_mutex } from "./services/sync_mutex";
export { default as setup } from "./services/setup";
export { getPlatform, type PlatformProvider } from "./services/platform";
export { t } from "i18next";
export type { RequestProvider, ExecOpts, CookieJar } from "./services/request";
export async function initializeCore({ dbConfig, executionContext, crypto, translations, messaging, request, schema, extraAppInfo }: {
export async function initializeCore({ dbConfig, executionContext, crypto, translations, messaging, request, schema, extraAppInfo, platform }: {
dbConfig: SqlServiceParams,
executionContext: ExecutionContext,
crypto: CryptoProvider,
translations: TranslationProvider,
platform: PlatformProvider,
schema: string,
messaging?: MessagingProvider,
request?: RequestProvider,
@@ -105,6 +109,7 @@ export async function initializeCore({ dbConfig, executionContext, crypto, trans
dataDirectory: string;
};
}) {
initPlatform(platform);
initLog();
await initTranslations(translations);
initCrypto(crypto);

View File

@@ -1,7 +1,7 @@
import backupService from "./backup.js";
import { getSql } from "./sql/index.js";
import { getLog } from "./log.js";
import { crash } from "./utils/index.js";
import { getPlatform } from "./platform.js";
import appInfo from "./app_info.js";
import * as cls from "./context.js";
import { t } from "i18next";
@@ -20,8 +20,7 @@ async function migrate() {
const currentDbVersion = getDbVersion();
if (currentDbVersion < 214) {
await crash(t("migration.old_version"));
return;
getPlatform().crash(t("migration.old_version"));
}
// backup before attempting migration
@@ -59,8 +58,7 @@ async function migrate() {
log.info(`Migration to version ${mig.dbVersion} has been successful.`);
} catch (e: any) {
console.error(e);
crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack }));
break; // crash() is sometimes async
getPlatform().crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack }));
}
}
});
@@ -126,7 +124,7 @@ async function migrateIfNecessary() {
const currentDbVersion = getDbVersion();
if (currentDbVersion > appInfo.dbVersion && process.env.TRILIUM_IGNORE_DB_VERSION !== "true") {
await crash(t("migration.wrong_db_version", { version: currentDbVersion, targetVersion: appInfo.dbVersion }));
getPlatform().crash(t("migration.wrong_db_version", { version: currentDbVersion, targetVersion: appInfo.dbVersion }));
}
if (!isDbUpToDate()) {

View File

@@ -0,0 +1,17 @@
/**
* Interface for platform-specific services. This is used to abstract away platform-specific implementations, such as crash reporting, from the core logic of the application.
*/
export interface PlatformProvider {
crash(message: string): void;
}
let platformProvider: PlatformProvider | null = null;
export function initPlatform(provider: PlatformProvider) {
platformProvider = provider;
}
export function getPlatform(): PlatformProvider {
if (!platformProvider) throw new Error("Platform provider not initialized");
return platformProvider;
}

View File

@@ -202,7 +202,3 @@ export function isEmptyOrWhitespace(str: string | null | undefined) {
export function escapeRegExp(str: string) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
export async function crash(message: string): never {
throw new Error(message);
}