Show error toast on database errors

This commit is contained in:
trevor-signal 2025-05-28 14:24:11 -04:00 committed by GitHub
commit 60f55f1749
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 76 additions and 15 deletions

View file

@ -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"

View file

@ -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;
}

View file

@ -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:

View file

@ -506,6 +506,20 @@ export function renderToast({
</Toast>
);
}
if (toastType === ToastType.SQLError) {
return (
<Toast
onClose={hideToast}
toastAction={{
label: i18n('icu:Toast__ActionLabel--SubmitLog'),
onClick: onShowDebugLog,
}}
autoDismissDisabled
>
{i18n('icu:Toast--SQLError')}
</Toast>
);
}
if (toastType === ToastType.StoryMuted) {
return (

View file

@ -92,6 +92,7 @@ type ResponseEntry<T> = {
type KnownErrorResolverType = Readonly<{
kind: SqliteErrorKind;
resolve: (err: Error) => void;
once?: boolean;
}>;
type CreateWorkerResultType = Readonly<{
@ -210,16 +211,31 @@ export class MainSQL {
public whenCorrupted(): Promise<Error> {
const { promise, resolve } = explodePromise<Error>();
this.#errorResolvers.push({ kind: SqliteErrorKind.Corrupted, resolve });
this.#errorResolvers.push({
kind: SqliteErrorKind.Corrupted,
resolve,
once: true,
});
return promise;
}
public whenReadonly(): Promise<Error> {
const { promise, resolve } = explodePromise<Error>();
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<void> {
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;
});

View file

@ -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 }

View file

@ -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 (