diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6312275108d..2f69947db99 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -804,6 +804,10 @@ "messageformat": "Failed to send message with endorsements", "description": "An error popup when we attempted and failed to send a message using endorsements, only for internal users." }, + "icu:Toast--SQLError": { + "messageformat": "Database error: please share your logs.", + "description": "An error popup when a database method fails, for internal & beta users" + }, "icu:Toast--InvalidStorageServiceHeaders": { "messageformat": "Received invalid response from storage service. Please share your logs.", "description": "[Only shown to internal/beta users] An error popup when we noticed an invalid response (i.e. a web request response) from one of our servers" diff --git a/app/main.ts b/app/main.ts index 72f3992ff26..6103c991be6 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1595,7 +1595,7 @@ const runSQLCorruptionHandler = async () => { // This is a glorified event handler. Normally, this promise never resolves, // but if there is a corruption error triggered by any query that we run // against the database - the promise will resolve and we will call - // `onDatabaseError`. + // `onDatabaseInitializationError`. const error = await sql.whenCorrupted(); getLogger().error( @@ -1603,14 +1603,14 @@ const runSQLCorruptionHandler = async () => { `Restarting the application immediately. Error: ${error.message}` ); - await onDatabaseError(error); + await onDatabaseInitializationError(error); }; const runSQLReadonlyHandler = async () => { // This is a glorified event handler. Normally, this promise never resolves, // but if there is a corruption error triggered by any query that we run // against the database - the promise will resolve and we will call - // `onDatabaseError`. + // `onDatabaseInitializationError`. const error = await sql.whenReadonly(); getLogger().error( @@ -1779,10 +1779,19 @@ async function initializeSQL( drop(runSQLCorruptionHandler()); drop(runSQLReadonlyHandler()); + sql.onUnknownSqlError(onUnknownSqlError); + return { ok: true, error: undefined }; } -const onDatabaseError = async (error: Error) => { +function onUnknownSqlError(error: Error) { + getLogger().error('Unknown SQL Error:', Errors.toLogFormat(error)); + if (mainWindow) { + mainWindow.webContents.send('sql-error'); + } +} + +const onDatabaseInitializationError = async (error: Error) => { // Prevent window from re-opening ready = false; @@ -1885,11 +1894,11 @@ const onDatabaseError = async (error: Error) => { }); if (confirmationButtonIndex === confirmDeleteAllDataButtonIndex) { - getLogger().error('onDatabaseError: Deleting all data'); + getLogger().error('onDatabaseInitializationError: Deleting all data'); await sql.removeDB(); userConfig.remove(); getLogger().error( - 'onDatabaseError: Requesting immediate restart after quit' + 'onDatabaseInitializationError: Requesting immediate restart after quit' ); app.relaunch(); } @@ -1901,7 +1910,7 @@ const onDatabaseError = async (error: Error) => { ); } - getLogger().error('onDatabaseError: Quitting application'); + getLogger().error('onDatabaseInitializationError: Quitting application'); app.exit(1); }; @@ -2278,7 +2287,7 @@ app.on('ready', async () => { if (sqlError) { getLogger().error('sql.initialize was unsuccessful; returning early'); - await onDatabaseError(sqlError); + await onDatabaseInitializationError(sqlError); return; } diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index ffa00fa4461..545de515597 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -152,6 +152,8 @@ function getToast(toastType: ToastType): AnyToast { return { toastType: ToastType.ReportedSpam }; case ToastType.ReportedSpamAndBlocked: return { toastType: ToastType.ReportedSpamAndBlocked }; + case ToastType.SQLError: + return { toastType: ToastType.SQLError }; case ToastType.StickerPackInstallFailed: return { toastType: ToastType.StickerPackInstallFailed }; case ToastType.StoryMuted: diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index 9d7302de648..32456ec7339 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -506,6 +506,20 @@ export function renderToast({ ); } + if (toastType === ToastType.SQLError) { + return ( + + {i18n('icu:Toast--SQLError')} + + ); + } if (toastType === ToastType.StoryMuted) { return ( diff --git a/ts/sql/main.ts b/ts/sql/main.ts index 7f083d524d2..1b5234de5d8 100644 --- a/ts/sql/main.ts +++ b/ts/sql/main.ts @@ -92,6 +92,7 @@ type ResponseEntry = { type KnownErrorResolverType = Readonly<{ kind: SqliteErrorKind; resolve: (err: Error) => void; + once?: boolean; }>; type CreateWorkerResultType = Readonly<{ @@ -210,16 +211,31 @@ export class MainSQL { public whenCorrupted(): Promise { const { promise, resolve } = explodePromise(); - this.#errorResolvers.push({ kind: SqliteErrorKind.Corrupted, resolve }); + this.#errorResolvers.push({ + kind: SqliteErrorKind.Corrupted, + resolve, + once: true, + }); return promise; } public whenReadonly(): Promise { const { promise, resolve } = explodePromise(); - this.#errorResolvers.push({ kind: SqliteErrorKind.Readonly, resolve }); + this.#errorResolvers.push({ + kind: SqliteErrorKind.Readonly, + resolve, + once: true, + }); return promise; } + public onUnknownSqlError(callback: (error: Error) => void): void { + this.#errorResolvers.push({ + kind: SqliteErrorKind.Unknown, + resolve: callback, + }); + } + public async close(): Promise { if (this.#onReady) { try { @@ -375,15 +391,13 @@ export class MainSQL { } #onError(errorKind: SqliteErrorKind, error: Error): void { - if (errorKind === SqliteErrorKind.Unknown) { - return; - } - const resolvers = new Array<(error: Error) => void>(); this.#errorResolvers = this.#errorResolvers.filter(entry => { if (entry.kind === errorKind) { resolvers.push(entry.resolve); - return false; + if (entry.once) { + return false; + } } return true; }); diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index a83eea7ef0f..d7a013c9837 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -54,6 +54,7 @@ export enum ToastType { ReactionFailed = 'ReactionFailed', ReportedSpam = 'ReportedSpam', ReportedSpamAndBlocked = 'ReportedSpamAndBlocked', + SQLError = 'SQLError', StickerPackInstallFailed = 'StickerPackInstallFailed', StoryMuted = 'StoryMuted', StoryReact = 'StoryReact', @@ -153,6 +154,7 @@ export type AnyToast = | { toastType: ToastType.ReportedSpam } | { toastType: ToastType.ReportedSpamAndBlocked } | { toastType: ToastType.StickerPackInstallFailed } + | { toastType: ToastType.SQLError } | { toastType: ToastType.StoryMuted } | { toastType: ToastType.StoryReact } | { toastType: ToastType.StoryReply } diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index 1c5ee268845..b0c9e748810 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -22,6 +22,8 @@ import { DataReader } from '../../sql/Client'; import type { WindowsNotificationData } from '../../services/notifications'; import { AggregatedStats } from '../../textsecure/WebsocketResources'; import { UNAUTHENTICATED_CHANNEL_NAME } from '../../textsecure/SocketManager'; +import { isProduction } from '../../util/version'; +import { ToastType } from '../../types/Toast'; // It is important to call this as early as possible window.i18n = SignalContext.i18n; @@ -437,6 +439,20 @@ ipc.on('show-release-notes', () => { } }); +ipc.on('sql-error', () => { + if (!window.reduxActions) { + return; + } + + if (isProduction(window.getVersion())) { + return; + } + + window.reduxActions.toast.showToast({ + toastType: ToastType.SQLError, + }); +}); + ipc.on( 'art-creator:uploadStickerPack', async (