diff --git a/libtextsecure/test/_test.js b/libtextsecure/test/_test.js index b58f2f6cb..6d80df89b 100644 --- a/libtextsecure/test/_test.js +++ b/libtextsecure/test/_test.js @@ -57,7 +57,7 @@ before(async () => { try { window.SignalWindow.log.info('Initializing SQL in renderer'); const isTesting = true; - await window.sqlInitializer.initialize(isTesting); + await window.Signal.Data.startInRenderer(isTesting); window.SignalWindow.log.info('SQL initialized in renderer'); } catch (err) { window.SignalWindow.log.error( diff --git a/preload.js b/preload.js index 3a676f308..e4bd7e89c 100644 --- a/preload.js +++ b/preload.js @@ -29,8 +29,6 @@ try { const { remote } = electron; const { app } = remote; - window.sqlInitializer = require('./ts/sql/initialize'); - const config = require('url').parse(window.location.toString(), true).query; setEnvironment(parseEnvironment(config.environment)); diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js index 25c720280..21463c07f 100644 --- a/sticker-creator/preload.js +++ b/sticker-creator/preload.js @@ -37,8 +37,6 @@ const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024; setEnvironment(parseEnvironment(config.environment)); -window.sqlInitializer = require('../ts/sql/initialize'); - window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/'; window.getEnvironment = getEnvironment; window.getVersion = () => config.version; @@ -173,7 +171,6 @@ window.encryptAndUpload = async ( cover, onProgress = noop ) => { - await window.sqlInitializer.goBackToMainProcess(); const usernameItem = await window.Signal.Data.getItemById('uuid_id'); const oldUsernameItem = await window.Signal.Data.getItemById('number_id'); const passwordItem = await window.Signal.Data.getItemById('password'); diff --git a/test/_test.js b/test/_test.js index 20c8bc55e..c7b7ebf07 100644 --- a/test/_test.js +++ b/test/_test.js @@ -68,7 +68,7 @@ before(async () => { try { window.SignalWindow.log.info('Initializing SQL in renderer'); const isTesting = true; - await window.sqlInitializer.initialize(isTesting); + await window.Signal.Data.startInRenderer(isTesting); window.SignalWindow.log.info('SQL initialized in renderer'); } catch (err) { window.SignalWindow.log.error( diff --git a/ts/background.ts b/ts/background.ts index 9e2b5a2f6..ddf13ee3f 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -154,13 +154,6 @@ export async function startApp(): Promise { storage: window.storage, }); window.attachmentDownloadQueue = []; - try { - log.info('Initializing SQL in renderer'); - await window.sqlInitializer.initialize(); - log.info('SQL initialized in renderer'); - } catch (err) { - log.error('SQL failed to initialize', err && err.stack ? err.stack : err); - } await window.Signal.Util.initializeMessageCounter(); @@ -803,6 +796,12 @@ export async function startApp(): Promise { window.Signal.Data.ensureFilePermissions(); } + try { + await window.Signal.Data.startInRendererProcess(); + } catch (err) { + log.error('SQL failed to initialize', err && err.stack ? err.stack : err); + } + Views.Initialization.setMessage(window.i18n('loading')); idleDetector = new IdleDetector(); @@ -2335,7 +2334,7 @@ export async function startApp(): Promise { ); // Go back to main process before processing delayed actions - await window.sqlInitializer.goBackToMainProcess(); + await window.Signal.Data.goBackToMainProcess(); profileKeyResponseQueue.start(); lightSessionResetQueue.start(); @@ -3441,7 +3440,7 @@ export async function startApp(): Promise { // If we couldn't connect during startup - we should still switch SQL to // the main process to avoid stalling UI. - window.sqlInitializer.goBackToMainProcess(); + window.Signal.Data.goBackToMainProcess(); } return; } diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index a2cb5e72f..adb5c8745 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -7,7 +7,9 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ -import { ipcRenderer } from 'electron'; +import { ipcRenderer as ipc } from 'electron'; +import fs from 'fs-extra'; +import pify from 'pify'; import { cloneDeep, @@ -27,7 +29,7 @@ import { import * as Bytes from '../Bytes'; import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { createBatcher } from '../util/batcher'; -import { assert } from '../util/assert'; +import { assert, strictAssert } from '../util/assert'; import { cleanDataForIpc } from './cleanDataForIpc'; import { ReactionType } from '../types/Reactions'; import { ConversationColorType, CustomColorType } from '../types/Colors'; @@ -84,14 +86,16 @@ import { isCorruptionError } from './errors'; import { MessageModel } from '../models/messages'; import { ConversationModel } from '../models/conversations'; -// We listen to a lot of events on ipcRenderer, often on the same channel. This prevents +// We listen to a lot of events on ipc, often on the same channel. This prevents // any warnings that might be sent to the console in that case. -if (ipcRenderer && ipcRenderer.setMaxListeners) { - ipcRenderer.setMaxListeners(0); +if (ipc && ipc.setMaxListeners) { + ipc.setMaxListeners(0); } else { - log.warn('sql/Client: ipcRenderer is not available!'); + log.warn('sql/Client: ipc is not available!'); } +const getRealPath = pify(fs.realpath); + const MIN_TRACE_DURATION = 10; const SQL_CHANNEL_KEY = 'sql-channel'; @@ -109,13 +113,20 @@ type ClientJobUpdateType = { args?: Array; }; +enum RendererState { + InMain = 'InMain', + Opening = 'Opening', + InRenderer = 'InRenderer', + Closing = 'Closing', +} + const _jobs: { [id: string]: ClientJobType } = Object.create(null); const _DEBUG = false; let _jobCounter = 0; let _shuttingDown = false; let _shutdownCallback: Function | null = null; let _shutdownPromise: Promise | null = null; -let shouldUseRendererProcess = true; +let state = RendererState.InMain; const startupQueries = new Map(); // Because we can't force this module to conform to an interface, we narrow our exports @@ -294,6 +305,7 @@ const dataInterface: ClientInterface = { // Client-side only, and test-only + startInRendererProcess, goBackToMainProcess, _removeConversations, _jobs, @@ -301,22 +313,49 @@ const dataInterface: ClientInterface = { export default dataInterface; -async function goBackToMainProcess(): Promise { - if (!shouldUseRendererProcess) { - log.info('data.goBackToMainProcess: already switched to main process'); - return; +async function startInRendererProcess(isTesting = false): Promise { + strictAssert( + state === RendererState.InMain, + `startInRendererProcess: expected ${state} to be ${RendererState.InMain}` + ); + + log.info('data.startInRendererProcess: switching to renderer process'); + state = RendererState.Opening; + + if (!isTesting) { + ipc.send('database-ready'); + + await new Promise(resolve => { + ipc.once('database-ready', () => { + resolve(); + }); + }); } + const configDir = await getRealPath(ipc.sendSync('get-user-data-path')); + const key = ipc.sendSync('user-config-key'); + + await Server.initializeRenderer({ configDir, key }); + + log.info('data.startInRendererProcess: switched to renderer process'); + + state = RendererState.InRenderer; +} + +async function goBackToMainProcess(): Promise { + strictAssert( + state === RendererState.InRenderer, + `goBackToMainProcess: expected ${state} to be ${RendererState.InRenderer}` + ); + // We don't need to wait for pending queries since they are synchronous. log.info('data.goBackToMainProcess: switching to main process'); - - // Close the database in the renderer process. const closePromise = close(); // It should be the last query we run in renderer process - shouldUseRendererProcess = false; - + state = RendererState.Closing; await closePromise; + state = RendererState.InMain; // Print query statistics for whole startup const entries = Array.from(startupQueries.entries()); @@ -329,6 +368,8 @@ async function goBackToMainProcess(): Promise { .forEach(([query, duration]) => { log.info(`startup query: ${query} ${duration}ms`); }); + + log.info('data.goBackToMainProcess: switched to main process'); } const channelsAsUnknown = fromPairs( @@ -474,38 +515,35 @@ function _getJob(id: number) { return _jobs[id]; } -if (ipcRenderer && ipcRenderer.on) { - ipcRenderer.on( - `${SQL_CHANNEL_KEY}-done`, - (_, jobId, errorForDisplay, result) => { - const job = _getJob(jobId); - if (!job) { - throw new Error( - `Received SQL channel reply to job ${jobId}, but did not have it in our registry!` - ); - } - - const { resolve, reject, fnName } = job; - - if (!resolve || !reject) { - throw new Error( - `SQL channel job ${jobId} (${fnName}): didn't have a resolve or reject` - ); - } - - if (errorForDisplay) { - return reject( - new Error( - `Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}` - ) - ); - } - - return resolve(result); +if (ipc && ipc.on) { + ipc.on(`${SQL_CHANNEL_KEY}-done`, (_, jobId, errorForDisplay, result) => { + const job = _getJob(jobId); + if (!job) { + throw new Error( + `Received SQL channel reply to job ${jobId}, but did not have it in our registry!` + ); } - ); + + const { resolve, reject, fnName } = job; + + if (!resolve || !reject) { + throw new Error( + `SQL channel job ${jobId} (${fnName}): didn't have a resolve or reject` + ); + } + + if (errorForDisplay) { + return reject( + new Error( + `Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}` + ) + ); + } + + return resolve(result); + }); } else { - log.warn('sql/Client: ipcRenderer.on is not available!'); + log.warn('sql/Client: ipc.on is not available!'); } function makeChannel(fnName: string) { @@ -514,7 +552,7 @@ function makeChannel(fnName: string) { // the db that exists in the renderer process to be able to boot up quickly // once the app is running we switch back to the main process to avoid the // UI from locking up whenever we do costly db operations. - if (shouldUseRendererProcess) { + if (state === RendererState.InRenderer) { const serverFnName = fnName as keyof ServerInterface; const start = Date.now(); @@ -529,7 +567,7 @@ function makeChannel(fnName: string) { 'Detected sql corruption in renderer process. ' + `Restarting the application immediately. Error: ${error.message}` ); - ipcRenderer?.send('database-error', error.stack); + ipc?.send('database-error', error.stack); } log.error( `Renderer SQL channel job (${fnName}) error ${error.message}` @@ -557,7 +595,7 @@ function makeChannel(fnName: string) { () => new Promise((resolve, reject) => { try { - ipcRenderer.send(SQL_CHANNEL_KEY, jobId, fnName, ...args); + ipc.send(SQL_CHANNEL_KEY, jobId, fnName, ...args); _updateJob(jobId, { resolve, @@ -1556,8 +1594,8 @@ async function callChannel(name: string) { return createTaskWithTimeout( () => new Promise((resolve, reject) => { - ipcRenderer.send(name); - ipcRenderer.once(`${name}-done`, (_, error) => { + ipc.send(name); + ipc.once(`${name}-done`, (_, error) => { if (error) { reject(error); diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index dc9b0c9c9..0ea94adf3 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -685,6 +685,7 @@ export type ClientInterface = DataInterface & { // whether we should use IPC to use the database in the main process or // use the db already running in the renderer. goBackToMainProcess: () => Promise; + startInRendererProcess: (isTesting?: boolean) => Promise; }; export type ClientJobType = { diff --git a/ts/sql/initialize.ts b/ts/sql/initialize.ts deleted file mode 100644 index e14275a02..000000000 --- a/ts/sql/initialize.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { ipcRenderer as ipc } from 'electron'; -import fs from 'fs-extra'; -import pify from 'pify'; -import sql from './Server'; - -const getRealPath = pify(fs.realpath); - -// Called from renderer. -export async function initialize(isTesting = false): Promise { - if (!isTesting) { - ipc.send('database-ready'); - - await new Promise(resolve => { - ipc.once('database-ready', () => { - resolve(); - }); - }); - } - - const configDir = await getRealPath(ipc.sendSync('get-user-data-path')); - const key = ipc.sendSync('user-config-key'); - - await sql.initializeRenderer({ configDir, key }); -} - -export async function goBackToMainProcess(): Promise { - return window.Signal.Data.goBackToMainProcess(); -} diff --git a/ts/util/createIPCEvents.ts b/ts/util/createIPCEvents.ts index 875078583..643a9018a 100644 --- a/ts/util/createIPCEvents.ts +++ b/ts/util/createIPCEvents.ts @@ -380,13 +380,13 @@ export function createIPCEvents( showKeyboardShortcuts: () => window.showKeyboardShortcuts(), deleteAllData: async () => { - await window.sqlInitializer.goBackToMainProcess(); + await window.Signal.Data.goBackToMainProcess(); renderClearingDataView(); }, closeDB: async () => { - await window.sqlInitializer.goBackToMainProcess(); + await window.Signal.Data.goBackToMainProcess(); }, showStickerPack: (packId, key) => { diff --git a/ts/window.d.ts b/ts/window.d.ts index effb5f49d..254d96e63 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -267,11 +267,6 @@ declare global { titleBarDoubleClick: () => void; unregisterForActive: (handler: () => void) => void; updateTrayIcon: (count: number) => void; - sqlInitializer: { - initialize: () => Promise; - goBackToMainProcess: () => Promise; - }; - Backbone: typeof Backbone; CI?: CI; Accessibility: {