diff --git a/.eslintignore b/.eslintignore index 60a314c6cf..0d372308e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -679,6 +679,7 @@ packages/lib/components/shared/side-menu-shared.js packages/lib/database-driver-better-sqlite.js packages/lib/database.js packages/lib/debug/DebugService.js +packages/lib/determineProfileDir.js packages/lib/dom.js packages/lib/errorUtils.js packages/lib/errors.js diff --git a/.gitignore b/.gitignore index 6f4b4500bc..0e2d5603cc 100644 --- a/.gitignore +++ b/.gitignore @@ -659,6 +659,7 @@ packages/lib/components/shared/side-menu-shared.js packages/lib/database-driver-better-sqlite.js packages/lib/database.js packages/lib/debug/DebugService.js +packages/lib/determineProfileDir.js packages/lib/dom.js packages/lib/errorUtils.js packages/lib/errors.js diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 722601daa8..8fb1fbc347 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -11,7 +11,7 @@ import AlarmServiceDriverNode from '@joplin/lib/services/AlarmServiceDriverNode' import Logger, { TargetType } from '@joplin/utils/Logger'; import Setting from '@joplin/lib/models/Setting'; import actionApi from '@joplin/lib/services/rest/actionApi.desktop'; -import BaseApplication from '@joplin/lib/BaseApplication'; +import BaseApplication, { StartOptions } from '@joplin/lib/BaseApplication'; import DebugService from '@joplin/lib/debug/DebugService'; import { _, setLocale } from '@joplin/lib/locale'; import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService'; @@ -129,10 +129,6 @@ class Application extends BaseApplication { this.setupOcrService(); } - if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'autoUploadCrashDumps') { - bridge().setAutoUploadCrashDumps(action.value); - } - if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'style.editor.fontFamily' || action.type === 'SETTING_UPDATE_ALL') { this.updateEditorFont(); } @@ -383,14 +379,12 @@ class Application extends BaseApplication { eventManager.on(EventName.ResourceChange, handleResourceChange); } - public async start(argv: string[]): Promise { + public async start(argv: string[], startOptions: StartOptions = null): Promise { // If running inside a package, the command line, instead of being "node.exe " is "joplin.exe " so // insert an extra argument so that they can be processed in a consistent way everywhere. if (!bridge().electronIsDev()) argv.splice(1, 0, '.'); - argv = await super.start(argv); - - bridge().setAutoUploadCrashDumps(Setting.value('autoUploadCrashDumps')); + argv = await super.start(argv, startOptions); await this.applySettingsSideEffects(); diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index fcca7a2da6..da33b29380 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -25,16 +25,26 @@ export class Bridge { private electronWrapper_: ElectronAppWrapper; private lastSelectedPaths_: LastSelectedPath; private autoUploadCrashDumps_ = false; + private rootProfileDir_: string; + private appName_: string; + private appId_: string; - public constructor(electronWrapper: ElectronAppWrapper) { + public constructor(electronWrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean) { this.electronWrapper_ = electronWrapper; + this.appId_ = appId; + this.appName_ = appName; + this.rootProfileDir_ = rootProfileDir; + this.autoUploadCrashDumps_ = autoUploadCrashDumps; this.lastSelectedPaths_ = { file: null, directory: null, }; - Sentry.init({ - dsn: 'https://cceec550871b1e8a10fee4c7a28d5cf2@o4506576757522432.ingest.sentry.io/4506594281783296', + this.sentryInit(); + } + + private sentryInit() { + const options: Sentry.ElectronMainOptions = { beforeSend: event => { try { const date = (new Date()).toISOString().replace(/[:-]/g, '').split('.')[0]; @@ -49,7 +59,26 @@ export class Bridge { return event; } }, - }); + }; + + if (this.autoUploadCrashDumps_) options.dsn = 'https://cceec550871b1e8a10fee4c7a28d5cf2@o4506576757522432.ingest.sentry.io/4506594281783296'; + + // eslint-disable-next-line no-console + console.info('Sentry: Initialized with autoUploadCrashDumps:', this.autoUploadCrashDumps_); + + Sentry.init(options); + } + + public appId() { + return this.appId_; + } + + public appName() { + return this.appName_; + } + + public rootProfileDir() { + return this.rootProfileDir_; } public electronApp() { @@ -67,10 +96,6 @@ export class Bridge { await msleep(10); } - public setAutoUploadCrashDumps(v: boolean) { - this.autoUploadCrashDumps_ = v; - } - // The build directory contains additional external files that are going to // be packaged by Electron Builder. This is for files that need to be // accessed outside of the Electron app (for example the application icon). @@ -328,9 +353,9 @@ export class Bridge { let bridge_: Bridge = null; -export function initBridge(wrapper: ElectronAppWrapper) { +export function initBridge(wrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean) { if (bridge_) throw new Error('Bridge already initialized'); - bridge_ = new Bridge(wrapper); + bridge_ = new Bridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps); return bridge_; } diff --git a/packages/app-desktop/main-html.js b/packages/app-desktop/main-html.js index 3e5ef81a6e..2da53e78d9 100644 --- a/packages/app-desktop/main-html.js +++ b/packages/app-desktop/main-html.js @@ -80,7 +80,7 @@ const main = async () => { BaseItem.loadClass('MasterKey', MasterKey); BaseItem.loadClass('Revision', Revision); - Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`); + Setting.setConstant('appId', bridge().appId()); Setting.setConstant('appType', 'desktop'); // eslint-disable-next-line no-console diff --git a/packages/app-desktop/main.js b/packages/app-desktop/main.js index 4a83329d3d..1b0ab7d85b 100644 --- a/packages/app-desktop/main.js +++ b/packages/app-desktop/main.js @@ -3,12 +3,14 @@ const electronApp = require('electron').app; require('@electron/remote/main').initialize(); const ElectronAppWrapper = require('./ElectronAppWrapper').default; +const { pathExistsSync, readFileSync } = require('fs-extra'); const { initBridge } = require('./bridge'); const Logger = require('@joplin/utils/Logger').default; const FsDriverNode = require('@joplin/lib/fs-driver-node').default; const envFromArgs = require('@joplin/lib/envFromArgs'); const packageInfo = require('./packageInfo.js'); const { isCallbackUrl } = require('@joplin/lib/callbackUrlUtils'); +const determineProfileDir = require('@joplin/lib/determineProfileDir').default; // Electron takes the application name from package.json `name` and // displays this in the tray icon toolip and message box titles, however in @@ -24,7 +26,7 @@ process.on('unhandledRejection', (reason, p) => { // Likewise, we want to know if a profile is specified early, in particular // to save the window state data. -function profileFromArgs(args) { +function getProfileFromArgs(args) { if (!args) return null; const profileIndex = args.indexOf('--profile'); if (profileIndex <= 0 || profileIndex >= args.length - 1) return null; @@ -35,16 +37,35 @@ function profileFromArgs(args) { Logger.fsDriver_ = new FsDriverNode(); const env = envFromArgs(process.argv); -const profilePath = profileFromArgs(process.argv); +const profileFromArgs = getProfileFromArgs(process.argv); const isDebugMode = !!process.argv && process.argv.indexOf('--debug') >= 0; +// We initialize all these variables here because they are needed from the main process. They are +// then passed to the renderer process via the bridge. +const appId = `net.cozic.joplin${env === 'dev' ? 'dev' : ''}-desktop`; +let appName = env === 'dev' ? 'joplindev' : 'joplin'; +if (appId.indexOf('-desktop') >= 0) appName += '-desktop'; +const rootProfileDir = determineProfileDir(profileFromArgs, appName); +const settingsPath = `${rootProfileDir}/settings.json`; +let autoUploadCrashDumps = false; + +if (pathExistsSync(settingsPath)) { + const settingsContent = readFileSync(settingsPath, 'utf8'); + try { + const settings = JSON.parse(settingsContent); + autoUploadCrashDumps = !!settings && !!settings.autoUploadCrashDumps; + } catch (error) { + console.error(`Could not load settings: ${settingsPath}:`, error); + } +} + electronApp.setAsDefaultProtocolClient('joplin'); const initialCallbackUrl = process.argv.find((arg) => isCallbackUrl(arg)); -const wrapper = new ElectronAppWrapper(electronApp, env, profilePath, isDebugMode, initialCallbackUrl); +const wrapper = new ElectronAppWrapper(electronApp, env, rootProfileDir, isDebugMode, initialCallbackUrl); -initBridge(wrapper); +initBridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps); wrapper.start().catch((error) => { console.error('Electron App fatal error:'); diff --git a/packages/app-desktop/utils/restartInSafeModeFromMain.ts b/packages/app-desktop/utils/restartInSafeModeFromMain.ts index a34f73ff50..32f994e789 100644 --- a/packages/app-desktop/utils/restartInSafeModeFromMain.ts +++ b/packages/app-desktop/utils/restartInSafeModeFromMain.ts @@ -1,16 +1,17 @@ import Setting from '@joplin/lib/models/Setting'; import bridge from '../bridge'; import processStartFlags from '@joplin/lib/utils/processStartFlags'; -import BaseApplication, { safeModeFlagFilename } from '@joplin/lib/BaseApplication'; +import { safeModeFlagFilename } from '@joplin/lib/BaseApplication'; import initProfile from '@joplin/lib/services/profileConfig/initProfile'; import { writeFile } from 'fs-extra'; import { join } from 'path'; +import determineProfileDir from '@joplin/lib/determineProfileDir'; const restartInSafeModeFromMain = async () => { // Only set constants here -- the main process doesn't have easy access (without loading // a large amount of other code) to the database. - const appName = `joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`; + const appName = bridge().appName(); Setting.setConstant('appId', `net.cozic.${appName}`); Setting.setConstant('appType', 'desktop'); Setting.setConstant('appName', appName); @@ -20,7 +21,7 @@ const restartInSafeModeFromMain = async () => { shimInit({}); const startFlags = await processStartFlags(bridge().processArgv()); - const rootProfileDir = BaseApplication.determineProfileDir(startFlags.matched); + const rootProfileDir = determineProfileDir(startFlags.matched.profileDir, appName); const { profileDir } = await initProfile(rootProfileDir); // We can't access the database, so write to a file instead. diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 82914db220..8df9cdd60c 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -25,7 +25,6 @@ import { reg } from './registry'; import time from './time'; import BaseSyncTarget from './BaseSyncTarget'; import reduxSharedMiddleware from './components/shared/reduxSharedMiddleware'; -const os = require('os'); import dns = require('dns'); import fs = require('fs-extra'); const EventEmitter = require('events'); @@ -48,7 +47,6 @@ import MigrationService from './services/MigrationService'; import ShareService from './services/share/ShareService'; import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation'; import SyncTargetJoplinCloud from './SyncTargetJoplinCloud'; -const { toSystemSlashes } = require('./path-utils'); const { setAutoFreeze } = require('immer'); import { getEncryptionEnabled } from './services/synchronizer/syncInfoUtils'; import { loadMasterKeysFromSettings, migrateMasterPassword } from './services/e2ee/utils'; @@ -62,16 +60,20 @@ import { parseShareCache } from './services/share/reducer'; import RotatingLogs from './RotatingLogs'; import { NoteEntity } from './services/database/types'; import { join } from 'path'; -import processStartFlags, { MatchedStartFlags } from './utils/processStartFlags'; +import processStartFlags from './utils/processStartFlags'; +import determineProfileDir from './determineProfileDir'; const appLogger: LoggerWrapper = Logger.create('App'); // const ntpClient = require('./vendor/ntp-client'); // ntpClient.dgram = require('dgram'); -interface StartOptions { +export interface StartOptions { keychainEnabled?: boolean; setupGlobalLogger?: boolean; + rootProfileDir?: string; + appName?: string; + appId?: string; } export const safeModeFlagFilename = 'force-safe-mode-on-next-start'; @@ -600,20 +602,6 @@ export default class BaseApplication { return flags.matched; } - public static determineProfileDir(initArgs: MatchedStartFlags) { - let output = ''; - - if (initArgs.profileDir) { - output = initArgs.profileDir; - } else if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) { - output = `${process.env.PORTABLE_EXECUTABLE_DIR}/JoplinProfile`; - } else { - output = `${os.homedir()}/.config/${Setting.value('appName')}`; - } - - return toSystemSlashes(output, 'linux'); - } - protected startRotatingLogMaintenance(profileDir: string) { this.rotatingLogs = new RotatingLogs(profileDir); const processLogs = async () => { @@ -641,14 +629,17 @@ export default class BaseApplication { let initArgs = startFlags.matched; if (argv.length) this.showPromptString_ = false; - let appName = initArgs.env === 'dev' ? 'joplindev' : 'joplin'; - if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop'; + let appName = options.appName; + if (!appName) { + appName = initArgs.env === 'dev' ? 'joplindev' : 'joplin'; + if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop'; + } Setting.setConstant('appName', appName); // https://immerjs.github.io/immer/docs/freezing setAutoFreeze(initArgs.env === 'dev'); - const rootProfileDir = BaseApplication.determineProfileDir(initArgs); + const rootProfileDir = options.rootProfileDir ? options.rootProfileDir : determineProfileDir(initArgs.profileDir, appName); const { profileDir, profileConfig, isSubProfile } = await initProfile(rootProfileDir); this.profileConfig_ = profileConfig; diff --git a/packages/lib/determineProfileDir.ts b/packages/lib/determineProfileDir.ts new file mode 100644 index 0000000000..b37e576fb7 --- /dev/null +++ b/packages/lib/determineProfileDir.ts @@ -0,0 +1,16 @@ +import { homedir } from 'os'; +import { toSystemSlashes } from './path-utils'; + +export default (profileFromArgs: string, appName: string) => { + let output = ''; + + if (profileFromArgs) { + output = profileFromArgs; + } else if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) { + output = `${process.env.PORTABLE_EXECUTABLE_DIR}/JoplinProfile`; + } else { + output = `${homedir()}/.config/${appName}`; + } + + return toSystemSlashes(output, 'linux'); +}; diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 2b7368495e..7cb3fbcd8a 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -1391,7 +1391,9 @@ class Setting extends BaseModel { public: true, appTypes: [AppType.Desktop], label: () => 'Automatically upload crash reports', - description: () => 'If you experience a crash, please enable this option to automatically send a crash report.', + description: () => 'If you experience a crash, please enable this option to automatically send crash reports. You will need to restart the application for this change to take effect.', + isGlobal: true, + storage: SettingStorage.File, }, 'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false },