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", "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." "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": { "icu:Toast--InvalidStorageServiceHeaders": {
"messageformat": "Received invalid response from storage service. Please share your logs.", "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" "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, // 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 // 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 // against the database - the promise will resolve and we will call
// `onDatabaseError`. // `onDatabaseInitializationError`.
const error = await sql.whenCorrupted(); const error = await sql.whenCorrupted();
getLogger().error( getLogger().error(
@ -1603,14 +1603,14 @@ const runSQLCorruptionHandler = async () => {
`Restarting the application immediately. Error: ${error.message}` `Restarting the application immediately. Error: ${error.message}`
); );
await onDatabaseError(error); await onDatabaseInitializationError(error);
}; };
const runSQLReadonlyHandler = async () => { const runSQLReadonlyHandler = async () => {
// This is a glorified event handler. Normally, this promise never resolves, // 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 // 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 // against the database - the promise will resolve and we will call
// `onDatabaseError`. // `onDatabaseInitializationError`.
const error = await sql.whenReadonly(); const error = await sql.whenReadonly();
getLogger().error( getLogger().error(
@ -1779,10 +1779,19 @@ async function initializeSQL(
drop(runSQLCorruptionHandler()); drop(runSQLCorruptionHandler());
drop(runSQLReadonlyHandler()); drop(runSQLReadonlyHandler());
sql.onUnknownSqlError(onUnknownSqlError);
return { ok: true, error: undefined }; 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 // Prevent window from re-opening
ready = false; ready = false;
@ -1885,11 +1894,11 @@ const onDatabaseError = async (error: Error) => {
}); });
if (confirmationButtonIndex === confirmDeleteAllDataButtonIndex) { if (confirmationButtonIndex === confirmDeleteAllDataButtonIndex) {
getLogger().error('onDatabaseError: Deleting all data'); getLogger().error('onDatabaseInitializationError: Deleting all data');
await sql.removeDB(); await sql.removeDB();
userConfig.remove(); userConfig.remove();
getLogger().error( getLogger().error(
'onDatabaseError: Requesting immediate restart after quit' 'onDatabaseInitializationError: Requesting immediate restart after quit'
); );
app.relaunch(); app.relaunch();
} }
@ -1901,7 +1910,7 @@ const onDatabaseError = async (error: Error) => {
); );
} }
getLogger().error('onDatabaseError: Quitting application'); getLogger().error('onDatabaseInitializationError: Quitting application');
app.exit(1); app.exit(1);
}; };
@ -2278,7 +2287,7 @@ app.on('ready', async () => {
if (sqlError) { if (sqlError) {
getLogger().error('sql.initialize was unsuccessful; returning early'); getLogger().error('sql.initialize was unsuccessful; returning early');
await onDatabaseError(sqlError); await onDatabaseInitializationError(sqlError);
return; return;
} }

View file

@ -152,6 +152,8 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.ReportedSpam }; return { toastType: ToastType.ReportedSpam };
case ToastType.ReportedSpamAndBlocked: case ToastType.ReportedSpamAndBlocked:
return { toastType: ToastType.ReportedSpamAndBlocked }; return { toastType: ToastType.ReportedSpamAndBlocked };
case ToastType.SQLError:
return { toastType: ToastType.SQLError };
case ToastType.StickerPackInstallFailed: case ToastType.StickerPackInstallFailed:
return { toastType: ToastType.StickerPackInstallFailed }; return { toastType: ToastType.StickerPackInstallFailed };
case ToastType.StoryMuted: case ToastType.StoryMuted:

View file

@ -506,6 +506,20 @@ export function renderToast({
</Toast> </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) { if (toastType === ToastType.StoryMuted) {
return ( return (

View file

@ -92,6 +92,7 @@ type ResponseEntry<T> = {
type KnownErrorResolverType = Readonly<{ type KnownErrorResolverType = Readonly<{
kind: SqliteErrorKind; kind: SqliteErrorKind;
resolve: (err: Error) => void; resolve: (err: Error) => void;
once?: boolean;
}>; }>;
type CreateWorkerResultType = Readonly<{ type CreateWorkerResultType = Readonly<{
@ -210,16 +211,31 @@ export class MainSQL {
public whenCorrupted(): Promise<Error> { public whenCorrupted(): Promise<Error> {
const { promise, resolve } = explodePromise<Error>(); const { promise, resolve } = explodePromise<Error>();
this.#errorResolvers.push({ kind: SqliteErrorKind.Corrupted, resolve }); this.#errorResolvers.push({
kind: SqliteErrorKind.Corrupted,
resolve,
once: true,
});
return promise; return promise;
} }
public whenReadonly(): Promise<Error> { public whenReadonly(): Promise<Error> {
const { promise, resolve } = explodePromise<Error>(); const { promise, resolve } = explodePromise<Error>();
this.#errorResolvers.push({ kind: SqliteErrorKind.Readonly, resolve }); this.#errorResolvers.push({
kind: SqliteErrorKind.Readonly,
resolve,
once: true,
});
return promise; return promise;
} }
public onUnknownSqlError(callback: (error: Error) => void): void {
this.#errorResolvers.push({
kind: SqliteErrorKind.Unknown,
resolve: callback,
});
}
public async close(): Promise<void> { public async close(): Promise<void> {
if (this.#onReady) { if (this.#onReady) {
try { try {
@ -375,16 +391,14 @@ export class MainSQL {
} }
#onError(errorKind: SqliteErrorKind, error: Error): void { #onError(errorKind: SqliteErrorKind, error: Error): void {
if (errorKind === SqliteErrorKind.Unknown) {
return;
}
const resolvers = new Array<(error: Error) => void>(); const resolvers = new Array<(error: Error) => void>();
this.#errorResolvers = this.#errorResolvers.filter(entry => { this.#errorResolvers = this.#errorResolvers.filter(entry => {
if (entry.kind === errorKind) { if (entry.kind === errorKind) {
resolvers.push(entry.resolve); resolvers.push(entry.resolve);
if (entry.once) {
return false; return false;
} }
}
return true; return true;
}); });

View file

@ -54,6 +54,7 @@ export enum ToastType {
ReactionFailed = 'ReactionFailed', ReactionFailed = 'ReactionFailed',
ReportedSpam = 'ReportedSpam', ReportedSpam = 'ReportedSpam',
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked', ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
SQLError = 'SQLError',
StickerPackInstallFailed = 'StickerPackInstallFailed', StickerPackInstallFailed = 'StickerPackInstallFailed',
StoryMuted = 'StoryMuted', StoryMuted = 'StoryMuted',
StoryReact = 'StoryReact', StoryReact = 'StoryReact',
@ -153,6 +154,7 @@ export type AnyToast =
| { toastType: ToastType.ReportedSpam } | { toastType: ToastType.ReportedSpam }
| { toastType: ToastType.ReportedSpamAndBlocked } | { toastType: ToastType.ReportedSpamAndBlocked }
| { toastType: ToastType.StickerPackInstallFailed } | { toastType: ToastType.StickerPackInstallFailed }
| { toastType: ToastType.SQLError }
| { toastType: ToastType.StoryMuted } | { toastType: ToastType.StoryMuted }
| { toastType: ToastType.StoryReact } | { toastType: ToastType.StoryReact }
| { toastType: ToastType.StoryReply } | { toastType: ToastType.StoryReply }

View file

@ -22,6 +22,8 @@ import { DataReader } from '../../sql/Client';
import type { WindowsNotificationData } from '../../services/notifications'; import type { WindowsNotificationData } from '../../services/notifications';
import { AggregatedStats } from '../../textsecure/WebsocketResources'; import { AggregatedStats } from '../../textsecure/WebsocketResources';
import { UNAUTHENTICATED_CHANNEL_NAME } from '../../textsecure/SocketManager'; 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 // It is important to call this as early as possible
window.i18n = SignalContext.i18n; 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( ipc.on(
'art-creator:uploadStickerPack', 'art-creator:uploadStickerPack',
async ( async (