import ElectronAppWrapper from './ElectronAppWrapper'; import shim, { MessageBoxType } from '@joplin/lib/shim'; import { _, setLocale } from '@joplin/lib/locale'; import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage, Menu, MenuItemConstructorOptions, MenuItem } from 'electron'; import { dirname, toSystemSlashes } from '@joplin/lib/path-utils'; import { fileUriToPath } from '@joplin/utils/url'; import { urlDecode } from '@joplin/lib/string-utils'; import * as Sentry from '@sentry/electron/main'; import { homedir } from 'os'; import { msleep } from '@joplin/utils/time'; import { pathExists, pathExistsSync, writeFileSync, ensureDirSync } from 'fs-extra'; import { extname, normalize, join } from 'path'; import isSafeToOpen from './utils/isSafeToOpen'; import { closeSync, openSync, readSync, statSync } from 'fs'; import { KB } from '@joplin/utils/bytes'; import { defaultWindowId } from '@joplin/lib/reducer'; import { execCommand } from '@joplin/utils'; interface LastSelectedPath { file: string; directory: string; } interface OpenDialogOptions { properties?: string[]; defaultPath?: string; createDirectory?: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied filters?: any[]; } type OnAllowedExtensionsChange = (newExtensions: string[])=> void; interface MessageDialogOptions extends Omit { message?: string; } export class Bridge { private electronWrapper_: ElectronAppWrapper; private lastSelectedPaths_: LastSelectedPath; private autoUploadCrashDumps_ = false; private rootProfileDir_: string; private appName_: string; private appId_: string; private logFilePath_ = ''; private altInstanceId_ = ''; private extraAllowedExtensions_: string[] = []; private onAllowedExtensionsChangeListener_: OnAllowedExtensionsChange = ()=>{}; public constructor(electronWrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean, altInstanceId: string) { this.electronWrapper_ = electronWrapper; this.appId_ = appId; this.appName_ = appName; this.rootProfileDir_ = rootProfileDir; this.autoUploadCrashDumps_ = autoUploadCrashDumps; this.altInstanceId_ = altInstanceId; this.lastSelectedPaths_ = { file: null, directory: null, }; this.sentryInit(); } public setLogFilePath(v: string) { this.logFilePath_ = v; } private getCrashDumpDirectory(): string { try { const platformName = shim.platformName(); switch (platformName) { case 'win32': // Windows: Use %LOCALAPPDATA%\CrashDumps return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'CrashDumps'); case 'darwin': // macOS: Use ~/Library/Logs/DiagnosticReports return join(homedir(), 'Library', 'Logs', 'DiagnosticReports'); case 'linux': // Linux: Use XDG_STATE_HOME (for logs) or fallback to ~/.local/state return join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'joplin'); default: // For unknown platforms, default to the home directory return homedir(); } } catch (error) { // If we can't get the platform name, fallback to the home directory return homedir(); } } private sentryInit() { const getLogLines = () => { try { if (!this.logFilePath_ || !pathExistsSync(this.logFilePath_)) return ''; const { size } = statSync(this.logFilePath_); if (!size) return ''; const bytesToRead = Math.min(size, 100 * KB); const handle = openSync(this.logFilePath_, 'r'); const position = size - bytesToRead; const buffer = Buffer.alloc(bytesToRead); readSync(handle, buffer, 0, bytesToRead, position); closeSync(handle); return buffer.toString('utf-8'); } catch (error) { // Can't do anything in this context return ''; } }; const getLogAttachment = () => { const lines = getLogLines(); if (!lines) return null; return { filename: 'joplin-log.txt', data: lines }; }; const options: Sentry.ElectronMainOptions = { beforeSend: (event, hint) => { try { const logAttachment = getLogAttachment(); if (logAttachment) hint.attachments = [logAttachment]; const date = (new Date()).toISOString().replace(/[:-]/g, '').split('.')[0]; type ErrorEventWithLog = (typeof event) & { log: string[]; }; const errorEventWithLog: ErrorEventWithLog = { ...event, log: logAttachment ? logAttachment.data.trim().split('\n') : [], }; const crashDumpDir = this.getCrashDumpDirectory(); ensureDirSync(crashDumpDir); const crashDumpPath = join(crashDumpDir, `joplin_crash_dump_${date}.json`); writeFileSync(crashDumpPath, JSON.stringify(errorEventWithLog, null, '\t'), 'utf-8'); } catch (error) { // Ignore the error since we can't handle it here } if (!this.autoUploadCrashDumps_) { return null; } else { return event; } }, integrations: [Sentry.electronMinidumpIntegration()], // Using the default ipcMode value causes