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 (