diff --git a/apps/client-standalone/src/lightweight/platform_provider.ts b/apps/client-standalone/src/lightweight/platform_provider.ts new file mode 100644 index 0000000000..9a1340eb4e --- /dev/null +++ b/apps/client-standalone/src/lightweight/platform_provider.ts @@ -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 + }); + } +} diff --git a/apps/client-standalone/src/local-bridge.ts b/apps/client-standalone/src/local-bridge.ts index 270954b925..216d6ee87b 100644 --- a/apps/client-standalone/src/local-bridge.ts +++ b/apps/client-standalone/src/local-bridge.ts @@ -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); diff --git a/apps/client-standalone/src/local-server-worker.ts b/apps/client-standalone/src/local-server-worker.ts index 6b6e7409ea..86e945be10 100644 --- a/apps/client-standalone/src/local-server-worker.ts +++ b/apps/client-standalone/src/local-server-worker.ts @@ -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 { clsModule, cryptoModule, requestModule, + platformModule, translationModule, routesModule ] = await Promise.all([ @@ -89,6 +91,7 @@ async function loadModules(): Promise { 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 { 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 { crypto: new BrowserCryptoProvider(), messaging: messagingProvider!, request: new FetchRequestProvider(), + platform: new StandalonePlatformProvider(), translations: translationProvider, schema: schemaModule.default, dbConfig: { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index db6024154b..149420541e 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -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, diff --git a/apps/desktop/src/platform_provider.ts b/apps/desktop/src/platform_provider.ts new file mode 100644 index 0000000000..e9dab7c5cf --- /dev/null +++ b/apps/desktop/src/platform_provider.ts @@ -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); + } +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 10d9a9822e..61ce2b00af 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -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, diff --git a/apps/server/src/platform_provider.ts b/apps/server/src/platform_provider.ts new file mode 100644 index 0000000000..ed9d5919e5 --- /dev/null +++ b/apps/server/src/platform_provider.ts @@ -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); + } +} diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index 9998413bd9..70531a376f 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -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 { export default { compareVersions, constantTimeCompare, - crash, envToBoolean, escapeHtml, escapeRegExp, diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index 10c5c4f4cd..1138fecc24 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -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); diff --git a/packages/trilium-core/src/services/migration.ts b/packages/trilium-core/src/services/migration.ts index cc2b21ed8e..34b9bc94a6 100644 --- a/packages/trilium-core/src/services/migration.ts +++ b/packages/trilium-core/src/services/migration.ts @@ -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()) { diff --git a/packages/trilium-core/src/services/platform.ts b/packages/trilium-core/src/services/platform.ts new file mode 100644 index 0000000000..8c5d9e0929 --- /dev/null +++ b/packages/trilium-core/src/services/platform.ts @@ -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; +} diff --git a/packages/trilium-core/src/services/utils/index.ts b/packages/trilium-core/src/services/utils/index.ts index be338a5712..74f47927a4 100644 --- a/packages/trilium-core/src/services/utils/index.ts +++ b/packages/trilium-core/src/services/utils/index.ts @@ -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); -}