You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	This commit is contained in:
		| @@ -415,6 +415,8 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js | ||||
| packages/app-desktop/utils/checkForUpdatesUtils.js | ||||
| packages/app-desktop/utils/checkForUpdatesUtilsTestData.js | ||||
| packages/app-desktop/utils/markupLanguageUtils.js | ||||
| packages/app-desktop/utils/restartInSafeModeFromMain.test.js | ||||
| packages/app-desktop/utils/restartInSafeModeFromMain.js | ||||
| packages/app-mobile/PluginAssetsLoader.js | ||||
| packages/app-mobile/components/ActionButton.js | ||||
| packages/app-mobile/components/BackButtonDialogBox.js | ||||
| @@ -902,6 +904,7 @@ packages/lib/themes/type.js | ||||
| packages/lib/time.js | ||||
| packages/lib/utils/credentialFiles.js | ||||
| packages/lib/utils/joplinCloud.js | ||||
| packages/lib/utils/processStartFlags.js | ||||
| packages/lib/utils/userFetcher.js | ||||
| packages/lib/utils/webDAVUtils.test.js | ||||
| packages/lib/utils/webDAVUtils.js | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -397,6 +397,8 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js | ||||
| packages/app-desktop/utils/checkForUpdatesUtils.js | ||||
| packages/app-desktop/utils/checkForUpdatesUtilsTestData.js | ||||
| packages/app-desktop/utils/markupLanguageUtils.js | ||||
| packages/app-desktop/utils/restartInSafeModeFromMain.test.js | ||||
| packages/app-desktop/utils/restartInSafeModeFromMain.js | ||||
| packages/app-mobile/PluginAssetsLoader.js | ||||
| packages/app-mobile/components/ActionButton.js | ||||
| packages/app-mobile/components/BackButtonDialogBox.js | ||||
| @@ -884,6 +886,7 @@ packages/lib/themes/type.js | ||||
| packages/lib/time.js | ||||
| packages/lib/utils/credentialFiles.js | ||||
| packages/lib/utils/joplinCloud.js | ||||
| packages/lib/utils/processStartFlags.js | ||||
| packages/lib/utils/userFetcher.js | ||||
| packages/lib/utils/webDAVUtils.test.js | ||||
| packages/lib/utils/webDAVUtils.js | ||||
|   | ||||
| @@ -9,7 +9,10 @@ const url = require('url'); | ||||
| const path = require('path'); | ||||
| const { dirname } = require('@joplin/lib/path-utils'); | ||||
| const fs = require('fs-extra'); | ||||
| const { ipcMain } = require('electron'); | ||||
|  | ||||
| import { dialog, ipcMain } from 'electron'; | ||||
| import { _ } from '@joplin/lib/locale'; | ||||
| import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain'; | ||||
|  | ||||
| interface RendererProcessQuitReply { | ||||
| 	canClose: boolean; | ||||
| @@ -34,7 +37,7 @@ export default class ElectronAppWrapper { | ||||
| 	private pluginWindows_: PluginWindows = {}; | ||||
| 	private initialCallbackUrl_: string = null; | ||||
|  | ||||
| 	public constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) { | ||||
| 	public constructor(electronApp: any, env: string, profilePath: string|null, isDebugMode: boolean, initialCallbackUrl: string) { | ||||
| 		this.electronApp_ = electronApp; | ||||
| 		this.env_ = env; | ||||
| 		this.isDebugMode_ = isDebugMode; | ||||
| @@ -66,6 +69,42 @@ export default class ElectronAppWrapper { | ||||
| 		return this.initialCallbackUrl_; | ||||
| 	} | ||||
|  | ||||
| 	// Call when the app fails in a significant way. | ||||
| 	// | ||||
| 	// Assumes that the renderer process may be in an invalid state and so cannot | ||||
| 	// be accessed. | ||||
| 	public async handleAppFailure(errorMessage: string, canIgnore: boolean, isTesting?: boolean) { | ||||
| 		const buttons = []; | ||||
| 		buttons.push(_('Quit')); | ||||
| 		const exitIndex = 0; | ||||
|  | ||||
| 		if (canIgnore) { | ||||
| 			buttons.push(_('Ignore')); | ||||
| 		} | ||||
| 		const restartIndex = buttons.length; | ||||
| 		buttons.push(_('Restart in safe mode')); | ||||
|  | ||||
| 		const { response } = await dialog.showMessageBox({ | ||||
| 			message: _('An error occurred: %s', errorMessage), | ||||
| 			buttons, | ||||
| 		}); | ||||
|  | ||||
| 		if (response === restartIndex) { | ||||
| 			await restartInSafeModeFromMain(); | ||||
|  | ||||
| 			// A hung renderer seems to prevent the process from exiting completely. | ||||
| 			// In this case, crashing the renderer allows the window to close. | ||||
| 			// | ||||
| 			// Also only run this if not testing (crashing the renderer breaks automated | ||||
| 			// tests). | ||||
| 			if (this.win_ && !this.win_.webContents.isCrashed() && !isTesting) { | ||||
| 				this.win_.webContents.forcefullyCrashRenderer(); | ||||
| 			} | ||||
| 		} else if (response === exitIndex) { | ||||
| 			process.exit(1); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public createWindow() { | ||||
| 		// Set to true to view errors if the application does not start | ||||
| 		const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_; | ||||
| @@ -121,6 +160,20 @@ export default class ElectronAppWrapper { | ||||
| 			this.win_.setPosition(primaryDisplayWidth / 2 - windowWidth, primaryDisplayHeight / 2 - windowHeight); | ||||
| 		} | ||||
|  | ||||
| 		this.win_.webContents.on('unresponsive', async () => { | ||||
| 			await this.handleAppFailure(_('Window unresponsive.'), true); | ||||
| 		}); | ||||
|  | ||||
| 		this.win_.webContents.on('render-process-gone', async _event => { | ||||
| 			await this.handleAppFailure('Renderer process gone.', false); | ||||
| 		}); | ||||
|  | ||||
| 		this.win_.webContents.on('did-fail-load', async event => { | ||||
| 			if ((event as any).isMainFrame) { | ||||
| 				await this.handleAppFailure('Renderer process failed to load', false); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		void this.win_.loadURL(url.format({ | ||||
| 			pathname: path.join(__dirname, 'index.html'), | ||||
| 			protocol: 'file:', | ||||
|   | ||||
| @@ -2,6 +2,9 @@ import { test, expect } from './util/test'; | ||||
| import MainScreen from './models/MainScreen'; | ||||
| import activateMainMenuItem from './util/activateMainMenuItem'; | ||||
| import SettingsScreen from './models/SettingsScreen'; | ||||
| import { _electron as electron } from '@playwright/test'; | ||||
| import { writeFile } from 'fs-extra'; | ||||
| import { join } from 'path'; | ||||
|  | ||||
|  | ||||
| test.describe('main', () => { | ||||
| @@ -121,4 +124,23 @@ test.describe('main', () => { | ||||
|  | ||||
| 		expect(await nextExternalUrlPromise).toBe(linkHref); | ||||
| 	}); | ||||
|  | ||||
| 	test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => { | ||||
| 		await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8'); | ||||
|  | ||||
| 		// We need to write to the force-safe-mode file before opening the Electron app. | ||||
| 		// Open the app ourselves: | ||||
| 		const startupArgs = [ | ||||
| 			'main.js', '--env', 'dev', '--profile', profileDirectory, | ||||
| 		]; | ||||
| 		const electronApp = await electron.launch({ args: startupArgs }); | ||||
| 		const mainWindow = await electronApp.firstWindow(); | ||||
|  | ||||
| 		const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart'); | ||||
| 		await safeModeDisableLink.waitFor(); | ||||
| 		await expect(safeModeDisableLink).toBeInViewport(); | ||||
|  | ||||
| 		await electronApp.close(); | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import uuid from '@joplin/lib/uuid'; | ||||
|  | ||||
|  | ||||
| type JoplinFixtures = { | ||||
| 	profileDirectory: string; | ||||
| 	electronApp: ElectronApplication; | ||||
| 	mainWindow: Page; | ||||
| }; | ||||
| @@ -20,19 +21,26 @@ export const test = base.extend<JoplinFixtures>({ | ||||
| 	// See https://github.com/microsoft/playwright/issues/8798 | ||||
| 	// | ||||
| 	// eslint-disable-next-line no-empty-pattern | ||||
| 	electronApp: async ({ }, use) => { | ||||
| 	profileDirectory: async ({ }, use) => { | ||||
| 		const profilePath = resolve(join(dirname(__dirname), 'test-profile')); | ||||
| 		const profileSubdir = join(profilePath, uuid.createNano()); | ||||
| 		await mkdirp(profileSubdir); | ||||
|  | ||||
| 		const startupArgs = ['main.js', '--env', 'dev', '--profile', profileSubdir]; | ||||
| 		await use(profileSubdir); | ||||
|  | ||||
| 		await remove(profileSubdir); | ||||
| 	}, | ||||
|  | ||||
| 	electronApp: async ({ profileDirectory }, use) => { | ||||
| 		const startupArgs = [ | ||||
| 			'main.js', '--env', 'dev', '--profile', profileDirectory, | ||||
| 		]; | ||||
| 		const electronApp = await electron.launch({ args: startupArgs }); | ||||
|  | ||||
| 		await use(electronApp); | ||||
|  | ||||
| 		await electronApp.firstWindow(); | ||||
| 		await electronApp.close(); | ||||
| 		await remove(profileSubdir); | ||||
| 	}, | ||||
|  | ||||
| 	mainWindow: async ({ electronApp }, use) => { | ||||
|   | ||||
| @@ -31,117 +31,122 @@ const React = require('react'); | ||||
| const nodeSqlite = require('sqlite3'); | ||||
| const initLib = require('@joplin/lib/initLib').default; | ||||
|  | ||||
|  | ||||
| if (bridge().env() === 'dev') { | ||||
| 	const newConsole = function(oldConsole) { | ||||
| 		const output = {}; | ||||
| 		const fnNames = ['assert', 'clear', 'context', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'memory', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog', 'timeStamp', 'trace', 'warn']; | ||||
| 		for (const fnName of fnNames) { | ||||
| 			if (fnName === 'warn') { | ||||
| 				output.warn = function(...text) { | ||||
| 					const s = [...text].join(''); | ||||
| 					// React spams the console with walls of warnings even outside of strict mode, and even after having renamed | ||||
| 					// unsafe methods to UNSAFE_xxxx, so we need to hack the console to remove them... | ||||
| 					if (s.indexOf('Warning: componentWillReceiveProps has been renamed, and is not recommended for use') === 0) return; | ||||
| 					if (s.indexOf('Warning: componentWillUpdate has been renamed, and is not recommended for use.') === 0) return; | ||||
| 					oldConsole.warn(...text); | ||||
| 				}; | ||||
| 			} else { | ||||
| 				output[fnName] = function(...text) { | ||||
| 					return oldConsole[fnName](...text); | ||||
| 				}; | ||||
| const main = async () => { | ||||
| 	if (bridge().env() === 'dev') { | ||||
| 		const newConsole = function(oldConsole) { | ||||
| 			const output = {}; | ||||
| 			const fnNames = ['assert', 'clear', 'context', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'memory', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog', 'timeStamp', 'trace', 'warn']; | ||||
| 			for (const fnName of fnNames) { | ||||
| 				if (fnName === 'warn') { | ||||
| 					output.warn = function(...text) { | ||||
| 						const s = [...text].join(''); | ||||
| 						// React spams the console with walls of warnings even outside of strict mode, and even after having renamed | ||||
| 						// unsafe methods to UNSAFE_xxxx, so we need to hack the console to remove them... | ||||
| 						if (s.indexOf('Warning: componentWillReceiveProps has been renamed, and is not recommended for use') === 0) return; | ||||
| 						if (s.indexOf('Warning: componentWillUpdate has been renamed, and is not recommended for use.') === 0) return; | ||||
| 						oldConsole.warn(...text); | ||||
| 					}; | ||||
| 				} else { | ||||
| 					output[fnName] = function(...text) { | ||||
| 						return oldConsole[fnName](...text); | ||||
| 					}; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return output; | ||||
| 	}(window.console); | ||||
| 			return output; | ||||
| 		}(window.console); | ||||
|  | ||||
| 	window.console = newConsole; | ||||
| } | ||||
| 		window.console = newConsole; | ||||
| 	} | ||||
|  | ||||
| // eslint-disable-next-line no-console | ||||
| console.info(`Environment: ${bridge().env()}`); | ||||
| 	// eslint-disable-next-line no-console | ||||
| 	console.info(`Environment: ${bridge().env()}`); | ||||
|  | ||||
| const fsDriver = new FsDriverNode(); | ||||
| Logger.fsDriver_ = fsDriver; | ||||
| Resource.fsDriver_ = fsDriver; | ||||
| EncryptionService.fsDriver_ = fsDriver; | ||||
| FileApiDriverLocal.fsDriver_ = fsDriver; | ||||
| 	const fsDriver = new FsDriverNode(); | ||||
| 	Logger.fsDriver_ = fsDriver; | ||||
| 	Resource.fsDriver_ = fsDriver; | ||||
| 	EncryptionService.fsDriver_ = fsDriver; | ||||
| 	FileApiDriverLocal.fsDriver_ = fsDriver; | ||||
|  | ||||
| // That's not good, but it's to avoid circular dependency issues | ||||
| // in the BaseItem class. | ||||
| BaseItem.loadClass('Note', Note); | ||||
| BaseItem.loadClass('Folder', Folder); | ||||
| BaseItem.loadClass('Resource', Resource); | ||||
| BaseItem.loadClass('Tag', Tag); | ||||
| BaseItem.loadClass('NoteTag', NoteTag); | ||||
| BaseItem.loadClass('MasterKey', MasterKey); | ||||
| BaseItem.loadClass('Revision', Revision); | ||||
| 	// That's not good, but it's to avoid circular dependency issues | ||||
| 	// in the BaseItem class. | ||||
| 	BaseItem.loadClass('Note', Note); | ||||
| 	BaseItem.loadClass('Folder', Folder); | ||||
| 	BaseItem.loadClass('Resource', Resource); | ||||
| 	BaseItem.loadClass('Tag', Tag); | ||||
| 	BaseItem.loadClass('NoteTag', NoteTag); | ||||
| 	BaseItem.loadClass('MasterKey', MasterKey); | ||||
| 	BaseItem.loadClass('Revision', Revision); | ||||
|  | ||||
| Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`); | ||||
| Setting.setConstant('appType', 'desktop'); | ||||
| 	Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`); | ||||
| 	Setting.setConstant('appType', 'desktop'); | ||||
|  | ||||
| // eslint-disable-next-line no-console | ||||
| console.info(`appId: ${Setting.value('appId')}`); | ||||
| // eslint-disable-next-line no-console | ||||
| console.info(`appType: ${Setting.value('appType')}`); | ||||
| 	// eslint-disable-next-line no-console | ||||
| 	console.info(`appId: ${Setting.value('appId')}`); | ||||
| 	// eslint-disable-next-line no-console | ||||
| 	console.info(`appType: ${Setting.value('appType')}`); | ||||
|  | ||||
| let keytar; | ||||
| try { | ||||
| 	keytar = shim.platformSupportsKeyChain() ? require('keytar') : null; | ||||
| } catch (error) { | ||||
| 	console.error('Cannot load keytar - keychain support will be disabled', error); | ||||
| 	keytar = null; | ||||
| } | ||||
| 	let keytar; | ||||
| 	try { | ||||
| 		keytar = shim.platformSupportsKeyChain() ? require('keytar') : null; | ||||
| 	} catch (error) { | ||||
| 		console.error('Cannot load keytar - keychain support will be disabled', error); | ||||
| 		keytar = null; | ||||
| 	} | ||||
|  | ||||
| function appVersion() { | ||||
| 	const p = require('./packageInfo.js'); | ||||
| 	return p.version; | ||||
| } | ||||
| 	function appVersion() { | ||||
| 		const p = require('./packageInfo.js'); | ||||
| 		return p.version; | ||||
| 	} | ||||
|  | ||||
| shimInit({ | ||||
| 	keytar, | ||||
| 	React, | ||||
| 	appVersion, | ||||
| 	electronBridge: bridge(), | ||||
| 	nodeSqlite, | ||||
| }); | ||||
| 	shimInit({ | ||||
| 		keytar, | ||||
| 		React, | ||||
| 		appVersion, | ||||
| 		electronBridge: bridge(), | ||||
| 		nodeSqlite, | ||||
| 	}); | ||||
|  | ||||
| // Disable drag and drop of links inside application (which would | ||||
| // open it as if the whole app was a browser) | ||||
| document.addEventListener('dragover', event => event.preventDefault()); | ||||
| document.addEventListener('drop', event => event.preventDefault()); | ||||
| 	// Disable drag and drop of links inside application (which would | ||||
| 	// open it as if the whole app was a browser) | ||||
| 	document.addEventListener('dragover', event => event.preventDefault()); | ||||
| 	document.addEventListener('drop', event => event.preventDefault()); | ||||
|  | ||||
| // Disable middle-click (which would open a new browser window, but we don't want this) | ||||
| document.addEventListener('auxclick', event => event.preventDefault()); | ||||
| 	// Disable middle-click (which would open a new browser window, but we don't want this) | ||||
| 	document.addEventListener('auxclick', event => event.preventDefault()); | ||||
|  | ||||
| // Each link (rendered as a button or list item) has its own custom click event | ||||
| // so disable the default. In particular this will disable Ctrl+Clicking a link | ||||
| // which would open a new browser window. | ||||
| document.addEventListener('click', (event) => { | ||||
| 	// We don't apply this to labels and inputs because it would break | ||||
| 	// checkboxes. Such a global event handler is probably not a good idea | ||||
| 	// anyway but keeping it for now, as it doesn't seem to break anything else. | ||||
| 	// https://github.com/facebook/react/issues/13477#issuecomment-489274045 | ||||
| 	if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return; | ||||
| 	// Each link (rendered as a button or list item) has its own custom click event | ||||
| 	// so disable the default. In particular this will disable Ctrl+Clicking a link | ||||
| 	// which would open a new browser window. | ||||
| 	document.addEventListener('click', (event) => { | ||||
| 		// We don't apply this to labels and inputs because it would break | ||||
| 		// checkboxes. Such a global event handler is probably not a good idea | ||||
| 		// anyway but keeping it for now, as it doesn't seem to break anything else. | ||||
| 		// https://github.com/facebook/react/issues/13477#issuecomment-489274045 | ||||
| 		if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return; | ||||
|  | ||||
| 	event.preventDefault(); | ||||
| }); | ||||
| 		event.preventDefault(); | ||||
| 	}); | ||||
|  | ||||
| const logger = new Logger(); | ||||
| Logger.initializeGlobalLogger(logger); | ||||
| initLib(logger); | ||||
| 	const logger = new Logger(); | ||||
| 	Logger.initializeGlobalLogger(logger); | ||||
| 	initLib(logger); | ||||
|  | ||||
| app().start(bridge().processArgv()).then((result) => { | ||||
| 	if (!result || !result.action) { | ||||
| 	const startResult = await app().start(bridge().processArgv()); | ||||
|  | ||||
| 	if (!startResult || !startResult.action) { | ||||
| 		require('./gui/Root'); | ||||
| 	} else if (result.action === 'upgradeSyncTarget') { | ||||
| 	} else if (startResult.action === 'upgradeSyncTarget') { | ||||
| 		require('./gui/Root_UpgradeSyncTarget'); | ||||
| 	} | ||||
| }).catch((error) => { | ||||
| 	const env = bridge().env(); | ||||
| }; | ||||
|  | ||||
| main().catch((error) => { | ||||
| 	const env = bridge().env(); | ||||
| 	console.error(error); | ||||
|  | ||||
| 	let errorMessage; | ||||
| 	if (error.code === 'flagError') { | ||||
| 		bridge().showErrorMessageBox(error.message); | ||||
| 		errorMessage = error.message; | ||||
| 	} else { | ||||
| 		// If something goes wrong at this stage we don't have a console or a log file | ||||
| 		// so display the error in a message box. | ||||
| @@ -150,13 +155,12 @@ app().start(bridge().processArgv()).then((result) => { | ||||
| 		if (error.lineNumber) msg.push(error.lineNumber); | ||||
| 		if (error.stack) msg.push(error.stack); | ||||
|  | ||||
| 		if (env === 'dev') { | ||||
| 			console.error(error); | ||||
| 		} else { | ||||
| 			bridge().showErrorMessageBox(msg.join('\n\n')); | ||||
| 		} | ||||
| 		errorMessage = msg.join('\n\n'); | ||||
| 	} | ||||
|  | ||||
| 	// In dev, we leave the app open as debug statements in the console can be useful | ||||
| 	if (env !== 'dev') bridge().electronApp().exit(1); | ||||
| 	// In dev, we give the option to leave the app open as debug statements in the | ||||
| 	// console can be useful | ||||
| 	const canIgnore = env === 'dev'; | ||||
| 	bridge().electronApp().handleAppFailure(errorMessage, canIgnore); | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										45
									
								
								packages/app-desktop/utils/restartInSafeModeFromMain.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								packages/app-desktop/utils/restartInSafeModeFromMain.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
|  | ||||
| let currentProfileDirectory: string; | ||||
|  | ||||
| jest.doMock('../bridge', () => ({ | ||||
| 	// Mock the bridge functions used by restartInSafeModeFromMain | ||||
| 	// to remove the dependency on Electron. | ||||
| 	default: () => ({ | ||||
| 		restart: jest.fn(), | ||||
| 		processArgv: () => [ | ||||
| 			// The argument parser expects the first two arguments to | ||||
| 			// be the path to NodeJS and the second to be the main filename. | ||||
| 			process.argv[0], __filename, | ||||
|  | ||||
| 			// Only the following arguments are used. | ||||
| 			'--profile', currentProfileDirectory, | ||||
| 		], | ||||
| 		env: () => 'dev', | ||||
| 	}), | ||||
| })); | ||||
|  | ||||
| import { mkdtemp, readFile, remove } from 'fs-extra'; | ||||
| import restartInSafeModeFromMain from './restartInSafeModeFromMain'; | ||||
| import { tmpdir } from 'os'; | ||||
| import { join } from 'path'; | ||||
| import { safeModeFlagFilename } from '@joplin/lib/BaseApplication'; | ||||
|  | ||||
|  | ||||
| describe('restartInSafeModeFromMain', () => { | ||||
| 	beforeEach(async () => { | ||||
| 		currentProfileDirectory = await mkdtemp(join(tmpdir(), 'safemode-restart-test')); | ||||
| 	}); | ||||
|  | ||||
| 	afterEach(async () => { | ||||
| 		await remove(currentProfileDirectory); | ||||
| 	}); | ||||
|  | ||||
| 	test('should create a safe mode flag file', async () => { | ||||
| 		await restartInSafeModeFromMain(); | ||||
| 		const safeModeFlagFilepath = join( | ||||
| 			currentProfileDirectory, safeModeFlagFilename, | ||||
| 		); | ||||
| 		expect(await readFile(safeModeFlagFilepath, 'utf8')).toBe('true'); | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										33
									
								
								packages/app-desktop/utils/restartInSafeModeFromMain.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/app-desktop/utils/restartInSafeModeFromMain.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| 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 initProfile from '@joplin/lib/services/profileConfig/initProfile'; | ||||
| import { writeFile } from 'fs-extra'; | ||||
| import { join } from 'path'; | ||||
|  | ||||
|  | ||||
| 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`; | ||||
| 	Setting.setConstant('appId', `net.cozic.${appName}`); | ||||
| 	Setting.setConstant('appType', 'desktop'); | ||||
| 	Setting.setConstant('appName', appName); | ||||
|  | ||||
| 	// Load just enough for us to write a file in the profile directory | ||||
| 	const { shimInit } = require('@joplin/lib/shim-init-node.js'); | ||||
| 	shimInit({}); | ||||
|  | ||||
| 	const startFlags = await processStartFlags(bridge().processArgv()); | ||||
| 	const rootProfileDir = BaseApplication.determineProfileDir(startFlags.matched); | ||||
| 	const { profileDir } = await initProfile(rootProfileDir); | ||||
|  | ||||
| 	// We can't access the database, so write to a file instead. | ||||
| 	const safeModeFlagFile = join(profileDir, safeModeFlagFilename); | ||||
| 	await writeFile(safeModeFlagFile, 'true', 'utf8'); | ||||
|  | ||||
| 	bridge().restart(); | ||||
| }; | ||||
|  | ||||
| export default restartInSafeModeFromMain; | ||||
| @@ -6,7 +6,7 @@ import BaseService from './services/BaseService'; | ||||
| import reducer, { getNotesParent, serializeNotesParent, setStore, State } from './reducer'; | ||||
| import KeychainServiceDriver from './services/keychain/KeychainServiceDriver.node'; | ||||
| import KeychainServiceDriverDummy from './services/keychain/KeychainServiceDriver.dummy'; | ||||
| import { _, setLocale } from './locale'; | ||||
| import { setLocale } from './locale'; | ||||
| import KvStore from './services/KvStore'; | ||||
| import SyncTargetJoplinServer from './SyncTargetJoplinServer'; | ||||
| import SyncTargetOneDrive from './SyncTargetOneDrive'; | ||||
| @@ -26,8 +26,7 @@ import time from './time'; | ||||
| import BaseSyncTarget from './BaseSyncTarget'; | ||||
| const reduxSharedMiddleware = require('./components/shared/reduxSharedMiddleware'); | ||||
| const os = require('os'); | ||||
| const fs = require('fs-extra'); | ||||
| import JoplinError from './JoplinError'; | ||||
| import fs = require('fs-extra'); | ||||
| const EventEmitter = require('events'); | ||||
| const syswidecas = require('./vendor/syswide-cas'); | ||||
| import SyncTargetRegistry from './SyncTargetRegistry'; | ||||
| @@ -60,6 +59,8 @@ import { ProfileConfig } from './services/profileConfig/types'; | ||||
| import initProfile from './services/profileConfig/initProfile'; | ||||
| import { parseShareCache } from './services/share/reducer'; | ||||
| import RotatingLogs from './RotatingLogs'; | ||||
| import { join } from 'path'; | ||||
| import processStartFlags, { MatchedStartFlags } from './utils/processStartFlags'; | ||||
|  | ||||
| const appLogger: LoggerWrapper = Logger.create('App'); | ||||
|  | ||||
| @@ -70,6 +71,7 @@ interface StartOptions { | ||||
| 	keychainEnabled?: boolean; | ||||
| 	setupGlobalLogger?: boolean; | ||||
| } | ||||
| export const safeModeFlagFilename = 'force-safe-mode-on-next-start'; | ||||
|  | ||||
| export default class BaseApplication { | ||||
|  | ||||
| @@ -163,154 +165,15 @@ export default class BaseApplication { | ||||
| 	// Handles the initial flags passed to main script and | ||||
| 	// returns the remaining args. | ||||
| 	private async handleStartFlags_(argv: string[], setDefaults = true) { | ||||
| 		const matched: any = {}; | ||||
| 		argv = argv.slice(0); | ||||
| 		argv.splice(0, 2); // First arguments are the node executable, and the node JS file | ||||
| 		const flags = await processStartFlags(argv, setDefaults); | ||||
|  | ||||
| 		while (argv.length) { | ||||
| 			const arg = argv[0]; | ||||
| 			const nextArg = argv.length >= 2 ? argv[1] : null; | ||||
|  | ||||
| 			if (arg === '--profile') { | ||||
| 				if (!nextArg) throw new JoplinError(_('Usage: %s', '--profile <dir-path>'), 'flagError'); | ||||
| 				matched.profileDir = nextArg; | ||||
| 				argv.splice(0, 2); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--no-welcome') { | ||||
| 				matched.welcomeDisabled = true; | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--env') { | ||||
| 				if (!nextArg) throw new JoplinError(_('Usage: %s', '--env <dev|prod>'), 'flagError'); | ||||
| 				matched.env = nextArg; | ||||
| 				argv.splice(0, 2); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--is-demo') { | ||||
| 				Setting.setConstant('isDemo', true); | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--safe-mode') { | ||||
| 				matched.isSafeMode = true; | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--open-dev-tools') { | ||||
| 				Setting.setConstant('flagOpenDevTools', true); | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--debug') { | ||||
| 				// Currently only handled by ElectronAppWrapper (isDebugMode property) | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--update-geolocation-disabled') { | ||||
| 				Note.updateGeolocationEnabled_ = false; | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--stack-trace-enabled') { | ||||
| 				this.showStackTraces_ = true; | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--log-level') { | ||||
| 				if (!nextArg) throw new JoplinError(_('Usage: %s', '--log-level <none|error|warn|info|debug>'), 'flagError'); | ||||
| 				matched.logLevel = Logger.levelStringToId(nextArg); | ||||
| 				argv.splice(0, 2); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg.indexOf('-psn') === 0) { | ||||
| 				// Some weird flag passed by macOS - can be ignored. | ||||
| 				// https://github.com/laurent22/joplin/issues/480 | ||||
| 				// https://stackoverflow.com/questions/10242115 | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--enable-logging') { | ||||
| 				// Electron-specific flag used for debugging - ignore it | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--dev-plugins') { | ||||
| 				Setting.setConstant('startupDevPlugins', nextArg.split(',').map(p => p.trim())); | ||||
| 				argv.splice(0, 2); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg.indexOf('--remote-debugging-port=') === 0) { | ||||
| 				// Electron-specific flag used for debugging - ignore it. Electron expects this flag in '--x=y' form, a single string. | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--no-sandbox') { | ||||
| 				// Electron-specific flag for running the app without chrome-sandbox | ||||
| 				// Allows users to use it as a workaround for the electron+AppImage issue | ||||
| 				// https://github.com/laurent22/joplin/issues/2246 | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg.indexOf('--user-data-dir=') === 0) { | ||||
| 				// Electron-specific flag. Allows users to run the app with chromedriver. | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg.indexOf('--enable-features=') === 0) { | ||||
| 				// Electron-specific flag - ignore it | ||||
| 				// Allows users to run the app on native wayland | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg.indexOf('--ozone-platform=') === 0) { | ||||
| 				// Electron-specific flag - ignore it | ||||
| 				// Allows users to run the app on native wayland | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg === '--disable-smooth-scrolling') { | ||||
| 				// Electron-specific flag - ignore it | ||||
| 				// Allows users to disable smooth scrolling | ||||
| 				argv.splice(0, 1); | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if (arg.length && arg[0] === '-') { | ||||
| 				throw new JoplinError(_('Unknown flag: %s', arg), 'flagError'); | ||||
| 			} else { | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (setDefaults) { | ||||
| 			if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO; | ||||
| 			if (!matched.env) matched.env = 'prod'; | ||||
| 			if (!matched.devPlugins) matched.devPlugins = []; | ||||
| 		if (flags.matched.showStackTraces) { | ||||
| 			this.showStackTraces_ = true; | ||||
| 		} | ||||
|  | ||||
| 		return { | ||||
| 			matched: matched, | ||||
| 			argv: argv, | ||||
| 			matched: flags.matched, | ||||
| 			argv: flags.argv, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -725,7 +588,7 @@ export default class BaseApplication { | ||||
| 		return flags.matched; | ||||
| 	} | ||||
|  | ||||
| 	public determineProfileDir(initArgs: any) { | ||||
| 	public static determineProfileDir(initArgs: MatchedStartFlags) { | ||||
| 		let output = ''; | ||||
|  | ||||
| 		if (initArgs.profileDir) { | ||||
| @@ -773,7 +636,7 @@ export default class BaseApplication { | ||||
| 		// https://immerjs.github.io/immer/docs/freezing | ||||
| 		setAutoFreeze(initArgs.env === 'dev'); | ||||
|  | ||||
| 		const rootProfileDir = this.determineProfileDir(initArgs); | ||||
| 		const rootProfileDir = BaseApplication.determineProfileDir(initArgs); | ||||
| 		const { profileDir, profileConfig, isSubProfile } = await initProfile(rootProfileDir); | ||||
| 		this.profileConfig_ = profileConfig; | ||||
|  | ||||
| @@ -863,6 +726,13 @@ export default class BaseApplication { | ||||
| 			Setting.setValue('isSafeMode', true); | ||||
| 		} | ||||
|  | ||||
| 		const safeModeFlagFile = join(profileDir, safeModeFlagFilename); | ||||
| 		if (await fs.pathExists(safeModeFlagFile) && fs.readFileSync(safeModeFlagFile, 'utf8') === 'true') { | ||||
| 			appLogger.info(`Safe mode enabled because of file: ${safeModeFlagFile}`); | ||||
| 			Setting.setValue('isSafeMode', true); | ||||
| 			fs.removeSync(safeModeFlagFile); | ||||
| 		} | ||||
|  | ||||
| 		if (Setting.value('firstStart')) { | ||||
| 			const locale = shim.detectAndSetLocale(Setting); | ||||
| 			reg.logger().info(`First start: detected locale as ${locale}`); | ||||
|   | ||||
							
								
								
									
										174
									
								
								packages/lib/utils/processStartFlags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								packages/lib/utils/processStartFlags.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
|  | ||||
|  | ||||
| import Logger, { LogLevel } from '@joplin/utils/Logger'; | ||||
| import JoplinError from '../JoplinError'; | ||||
| import { _ } from '../locale'; | ||||
| import Setting from '../models/Setting'; | ||||
| import Note from '../models/Note'; | ||||
|  | ||||
| export interface MatchedStartFlags { | ||||
| 	profileDir?: string; | ||||
| 	welcomeDisabled?: boolean; | ||||
| 	env?: string; | ||||
| 	isSafeMode?: boolean; | ||||
| 	showStackTraces?: boolean; | ||||
| 	logLevel?: LogLevel; | ||||
| 	devPlugins?: string[]; | ||||
| } | ||||
|  | ||||
| // Handles the initial flags passed to main script and | ||||
| // returns the remaining args. | ||||
| const processStartFlags = async (argv: string[], setDefaults = true) => { | ||||
| 	const matched: MatchedStartFlags = {}; | ||||
| 	argv = argv.slice(0); | ||||
| 	argv.splice(0, 2); // First arguments are the node executable, and the node JS file | ||||
|  | ||||
| 	while (argv.length) { | ||||
| 		const arg = argv[0]; | ||||
| 		const nextArg = argv.length >= 2 ? argv[1] : null; | ||||
|  | ||||
| 		if (arg === '--profile') { | ||||
| 			if (!nextArg) throw new JoplinError(_('Usage: %s', '--profile <dir-path>'), 'flagError'); | ||||
| 			matched.profileDir = nextArg; | ||||
| 			argv.splice(0, 2); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--no-welcome') { | ||||
| 			matched.welcomeDisabled = true; | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--env') { | ||||
| 			if (!nextArg) throw new JoplinError(_('Usage: %s', '--env <dev|prod>'), 'flagError'); | ||||
| 			matched.env = nextArg; | ||||
| 			argv.splice(0, 2); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--is-demo') { | ||||
| 			Setting.setConstant('isDemo', true); | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--safe-mode') { | ||||
| 			matched.isSafeMode = true; | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--open-dev-tools') { | ||||
| 			Setting.setConstant('flagOpenDevTools', true); | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--debug') { | ||||
| 			// Currently only handled by ElectronAppWrapper (isDebugMode property) | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--update-geolocation-disabled') { | ||||
| 			Note.updateGeolocationEnabled_ = false; | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--stack-trace-enabled') { | ||||
| 			matched.showStackTraces = true; | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--log-level') { | ||||
| 			if (!nextArg) throw new JoplinError(_('Usage: %s', '--log-level <none|error|warn|info|debug>'), 'flagError'); | ||||
| 			matched.logLevel = Logger.levelStringToId(nextArg); | ||||
| 			argv.splice(0, 2); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg.indexOf('-psn') === 0) { | ||||
| 			// Some weird flag passed by macOS - can be ignored. | ||||
| 			// https://github.com/laurent22/joplin/issues/480 | ||||
| 			// https://stackoverflow.com/questions/10242115 | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--enable-logging') { | ||||
| 			// Electron-specific flag used for debugging - ignore it | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--dev-plugins') { | ||||
| 			matched.devPlugins = nextArg.split(',').map(p => p.trim()); | ||||
| 			Setting.setConstant('startupDevPlugins', matched.devPlugins); | ||||
| 			argv.splice(0, 2); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg.indexOf('--remote-debugging-port=') === 0) { | ||||
| 			// Electron-specific flag used for debugging - ignore it. Electron expects this flag in '--x=y' form, a single string. | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--no-sandbox') { | ||||
| 			// Electron-specific flag for running the app without chrome-sandbox | ||||
| 			// Allows users to use it as a workaround for the electron+AppImage issue | ||||
| 			// https://github.com/laurent22/joplin/issues/2246 | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg.indexOf('--user-data-dir=') === 0) { | ||||
| 			// Electron-specific flag. Allows users to run the app with chromedriver. | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg.indexOf('--enable-features=') === 0) { | ||||
| 			// Electron-specific flag - ignore it | ||||
| 			// Allows users to run the app on native wayland | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg.indexOf('--ozone-platform=') === 0) { | ||||
| 			// Electron-specific flag - ignore it | ||||
| 			// Allows users to run the app on native wayland | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg === '--disable-smooth-scrolling') { | ||||
| 			// Electron-specific flag - ignore it | ||||
| 			// Allows users to disable smooth scrolling | ||||
| 			argv.splice(0, 1); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		if (arg.length && arg[0] === '-') { | ||||
| 			throw new JoplinError(_('Unknown flag: %s', arg), 'flagError'); | ||||
| 		} else { | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (setDefaults) { | ||||
| 		if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO; | ||||
| 		if (!matched.env) matched.env = 'prod'; | ||||
| 		if (!matched.devPlugins) matched.devPlugins = []; | ||||
| 	} | ||||
|  | ||||
| 	return { | ||||
| 		matched: matched, | ||||
| 		argv: argv, | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
| export default processStartFlags; | ||||
		Reference in New Issue
	
	Block a user