From 5165eb3bd4858ab1c24d983a21d733e2f35369e7 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 19 Feb 2019 17:32:44 -0800 Subject: [PATCH] On database error: show popup, allow user to delete and relaunch --- _locales/en/messages.json | 14 +++++++++++ app/global_errors.js | 19 +++++++++------ app/sql.js | 51 ++++++++++++++++++++++++++++++++++++--- js/background.js | 2 ++ main.js | 13 +++++++++- 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 37be8253cc..fbcb0d1ed4 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -4,6 +4,20 @@ "description": "Shown in the about box for the link to https://signal.org/legal" }, + "copyErrorAndQuit": { + "message": "Copy error and quit", + "description": + "Shown in the top-level error popup, allowing user to copy the error text and close the app" + }, + "databaseError": { + "message": "Database Error", + "description": "Shown in a popup if the database cannot start up properly" + }, + "deleteAndRestart": { + "message": "Delete all data and restart", + "description": + "Shown in a popup if the database cannot start up properly; allows user to dalete database and restart" + }, "mainMenuFile": { "message": "&File", "description": diff --git a/app/global_errors.js b/app/global_errors.js index de5d0e6c02..85ddee387a 100644 --- a/app/global_errors.js +++ b/app/global_errors.js @@ -5,9 +5,9 @@ const Errors = require('../js/modules/types/errors'); const { app, dialog, clipboard } = electron; const { redactAll } = require('../js/modules/privacy'); -// We're using hard-coded strings in this file because it needs to be ready -// to report errors before we do anything in the app. Also, we expect users to directly -// paste this text into search engines to find the bugs on GitHub. +// We use hard-coded strings until we're able to update these strings from the locale. +let quitText = 'Quit'; +let copyErrorAndQuitText = 'Copy error and quit'; function handleError(prefix, error) { console.error(`${prefix}:`, Errors.toLogFormat(error)); @@ -15,24 +15,29 @@ function handleError(prefix, error) { if (app.isReady()) { // title field is not shown on macOS, so we don't use it const buttonIndex = dialog.showMessageBox({ - buttons: ['OK', 'Copy error'], + buttons: [quitText, copyErrorAndQuitText], defaultId: 0, - detail: error.stack, + detail: redactAll(error.stack), message: prefix, noLink: true, type: 'error', }); if (buttonIndex === 1) { - clipboard.writeText(`${prefix}\n${redactAll(error.stack)}`); + clipboard.writeText(`${prefix}\n\n${redactAll(error.stack)}`); } } else { dialog.showErrorBox(prefix, error.stack); } - app.quit(); + app.exit(1); } +exports.updateLocale = messages => { + quitText = messages.quit.message; + copyErrorAndQuitText = messages.copyErrorAndQuit.message; +}; + exports.addHandler = () => { process.on('uncaughtException', error => { handleError('Unhandled Error', error); diff --git a/app/sql.js b/app/sql.js index ac26078fff..4a46dae4ee 100644 --- a/app/sql.js +++ b/app/sql.js @@ -2,9 +2,13 @@ const path = require('path'); const mkdirp = require('mkdirp'); const rimraf = require('rimraf'); const sql = require('@journeyapps/sqlcipher'); +const { app, dialog, clipboard } = require('electron'); +const { redactAll } = require('../js/modules/privacy'); +const { remove: removeUserConfig } = require('./user_config'); + const pify = require('pify'); const uuidv4 = require('uuid/v4'); -const { map, isString, fromPairs, forEach, last } = require('lodash'); +const { map, isObject, isString, fromPairs, forEach, last } = require('lodash'); // To get long stack traces // https://github.com/mapbox/node-sqlite3/wiki/API#sqlite3verbose @@ -670,7 +674,7 @@ let db; let filePath; let indexedDBPath; -async function initialize({ configDir, key }) { +async function initialize({ configDir, key, messages }) { if (db) { throw new Error('Cannot initialize more than once!'); } @@ -679,7 +683,10 @@ async function initialize({ configDir, key }) { throw new Error('initialize: configDir is required!'); } if (!isString(key)) { - throw new Error('initialize: key` is required!'); + throw new Error('initialize: key is required!'); + } + if (!isObject(messages)) { + throw new Error('initialize: message is required!'); } indexedDBPath = path.join(configDir, 'IndexedDB'); @@ -705,6 +712,40 @@ async function initialize({ configDir, key }) { await updateSchema(promisified); db = promisified; + + // test database + try { + await getMessageCount(); + } catch (error) { + console.log('Database startup error:', error.stack); + const buttonIndex = dialog.showMessageBox({ + buttons: [ + messages.copyErrorAndQuit.message, + messages.deleteAndRestart.message, + ], + defaultId: 0, + detail: redactAll(error.stack), + message: messages.databaseError.message, + noLink: true, + type: 'error', + }); + + if (buttonIndex === 0) { + clipboard.writeText( + `Database startup error:\n\n${redactAll(error.stack)}` + ); + } else { + await close(); + await removeDB(); + removeUserConfig(); + app.relaunch(); + } + + app.exit(1); + return false; + } + + return true; } async function close() { @@ -952,7 +993,9 @@ async function getConversationCount() { const row = await db.get('SELECT count(*) from conversations;'); if (!row) { - throw new Error('getMessageCount: Unable to get count of conversations'); + throw new Error( + 'getConversationCount: Unable to get count of conversations' + ); } return row['count(*)']; diff --git a/js/background.js b/js/background.js index a13d1a6c64..b42afeff97 100644 --- a/js/background.js +++ b/js/background.js @@ -1328,6 +1328,8 @@ messageReceiver = null; } + onEmpty(); + window.log.warn( 'Client is no longer authorized; deleting local configuration' ); diff --git a/main.js b/main.js index a591de6709..0e90caa423 100644 --- a/main.js +++ b/main.js @@ -651,6 +651,8 @@ app.on('ready', async () => { locale = loadLocale({ appLocale, logger }); } + GlobalErrors.updateLocale(locale.messages); + let key = userConfig.get('key'); if (!key) { console.log( @@ -660,7 +662,15 @@ app.on('ready', async () => { key = crypto.randomBytes(32).toString('hex'); userConfig.set('key', key); } - await sql.initialize({ configDir: userDataPath, key }); + const success = await sql.initialize({ + configDir: userDataPath, + key, + messages: locale.messages, + }); + if (!success) { + console.log('sql.initialize was unsuccessful; returning early'); + return; + } await sqlChannels.initialize(); try { @@ -773,6 +783,7 @@ app.on('before-quit', () => { readyForShutdown: mainWindow ? mainWindow.readyForShutdown : null, shouldQuit: windowState.shouldQuit(), }); + windowState.markShouldQuit(); });