diff --git a/app/attachment_channel.ts b/app/attachment_channel.ts index 01cccd1bdb..f8243cde28 100644 --- a/app/attachment_channel.ts +++ b/app/attachment_channel.ts @@ -443,12 +443,15 @@ function deleteOrphanedAttachments({ await sql.sqlRead('finishGetKnownMessageAttachments', cursor); } } - log.info( - `cleanupOrphanedAttachments: ${totalAttachmentsFound} message ` + - `attachments; ${orphanedAttachments.size} remain` + `cleanupOrphanedAttachments: ${totalAttachmentsFound} attachments and \ + ${totalDownloadsFound} downloads found on disk` ); + if (orphanedAttachments.size > 0) { + log.error(`${orphanedAttachments.size} orphaned attachment(s) found`); + } + if (totalMissing > 0) { log.warn( `cleanupOrphanedAttachments: ${totalMissing} message attachments were not found on disk` @@ -460,10 +463,10 @@ function deleteOrphanedAttachments({ attachments: Array.from(orphanedAttachments), }); - log.info( - `cleanupOrphanedAttachments: found ${totalDownloadsFound} downloads ` + - `${orphanedDownloads.size} remain` - ); + if (orphanedDownloads.size > 0) { + log.error(`${orphanedDownloads.size} orphaned download(s) found`); + } + await deleteAllDownloads({ userDataPath, downloads: Array.from(orphanedDownloads), diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 5adaec1f27..fef90df2a1 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -28,6 +28,7 @@ const KnownConfigKeys = [ 'desktop.donations', 'desktop.donations.prod', 'desktop.internalUser', + 'desktop.loggingErrorToasts', 'desktop.mediaQuality.levels', 'desktop.messageCleanup', 'desktop.retryRespondMaxAge', diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 5041ec651a..9b669c0d47 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -152,6 +152,11 @@ function getToast(toastType: ToastType): AnyToast { return { toastType: ToastType.LinkCopied }; case ToastType.LoadingFullLogs: return { toastType: ToastType.LoadingFullLogs }; + case ToastType._InternalMainProcessLoggingError: + return { + toastType: ToastType._InternalMainProcessLoggingError, + parameters: { logLines: ['error1', 'error2'], count: 2 }, + }; case ToastType.MaxAttachments: return { toastType: ToastType.MaxAttachments }; case ToastType.MediaNoLongerAvailable: diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index 95bda157a7..d93854c1b4 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -19,6 +19,7 @@ import type { LocalizerType } from '../types/Util.js'; import type { AnyToast } from '../types/Toast.js'; import type { AnyActionableMegaphone } from '../types/Megaphone.js'; import type { Location } from '../types/Nav.js'; +import { tw } from '../axo/tw.js'; export type PropsType = { changeLocation: (newLocation: Location) => unknown; @@ -586,6 +587,40 @@ export function renderToast({ ); } + if (toastType === ToastType._InternalMainProcessLoggingError) { + return ( + +

+ [INTERNAL]: {toast.parameters.count} error(s) from main process, + please submit log. +

+ + {toast.parameters.count > toast.parameters.logLines.length ? ( +

{`Showing only last ${toast.parameters.logLines.length} errors`}

+ ) : null} + +
+          {toast.parameters.logLines.join('\n')}
+        
+
+ ); + } + if (toastType === ToastType.PinnedConversationsFull) { return ( {i18n('icu:pinnedConversationsFull')} diff --git a/ts/logging/log.ts b/ts/logging/log.ts index a55712e5db..9e9a880194 100644 --- a/ts/logging/log.ts +++ b/ts/logging/log.ts @@ -38,6 +38,13 @@ const SUBSYSTEM_COLORS = new LRUCache({ max: 500, }); +let onLogCallback: (level: number, logLine: string, msgPrefix?: string) => void; +export function setOnLogCallback( + cb: (level: number, logLine: string, msgPrefix?: string) => void +): void { + onLogCallback = cb; +} + // Only for unpackaged app function getSubsystemColor(name: string): string { const cached = SUBSYSTEM_COLORS.get(name); @@ -168,6 +175,8 @@ const pinoInstance = pino( typeof item === 'string' ? item : reallyJsonStringify(item) ) .join(' '); + onLogCallback?.(level, line, this.msgPrefix); + return method.call(this, line); }, }, diff --git a/ts/logging/main_process_logging.ts b/ts/logging/main_process_logging.ts index f6d112e338..5c7df6a1d3 100644 --- a/ts/logging/main_process_logging.ts +++ b/ts/logging/main_process_logging.ts @@ -27,10 +27,11 @@ import * as Errors from '../types/errors.js'; import { createRotatingPinoDest } from '../util/rotatingPinoDest.js'; import { redactAll } from '../util/privacy.js'; -import { setPinoDestination, log } from './log.js'; +import { setPinoDestination, log, setOnLogCallback } from './log.js'; import type { FetchLogIpcData, LogEntryType } from './shared.js'; import { LogLevel, isLogEntry } from './shared.js'; +import { isProduction } from '../util/version.js'; const { filter, flatten, map, pick, sortBy } = lodash; @@ -47,6 +48,17 @@ export async function initialize( } isInitialized = true; + if (!isProduction(app.getVersion())) { + setOnLogCallback((level, logLine, msgPrefix) => { + if (level >= LogLevel.Error) { + getMainWindow()?.webContents.send( + 'logging-error', + `${msgPrefix ? `${msgPrefix}` : ''}${logLine}` + ); + } + }); + } + const basePath = app.getPath('userData'); const logPath = join(basePath, 'logs'); mkdirSync(logPath, { recursive: true }); diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index e1de4c0a37..e2cf6aeeb8 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -54,6 +54,7 @@ export enum ToastType { LeftGroup = 'LeftGroup', LinkCopied = 'LinkCopied', LoadingFullLogs = 'LoadingFullLogs', + _InternalMainProcessLoggingError = '_InternalMainProcessLoggingError', MaxAttachments = 'MaxAttachments', MediaNoLongerAvailable = 'MediaNoLongerAvailable', MessageBodyTooLong = 'MessageBodyTooLong', @@ -165,6 +166,10 @@ export type AnyToast = | { toastType: ToastType.LeftGroup } | { toastType: ToastType.LinkCopied } | { toastType: ToastType.LoadingFullLogs } + | { + toastType: ToastType._InternalMainProcessLoggingError; + parameters: { count: number; logLines: Array }; + } | { toastType: ToastType.MaxAttachments } | { toastType: ToastType.MediaNoLongerAvailable } | { toastType: ToastType.MessageBodyTooLong } diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index 07d1f63606..375ce91775 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -4,7 +4,7 @@ import EventEmitter from 'node:events'; import { ipcRenderer as ipc } from 'electron'; import * as semver from 'semver'; -import lodash from 'lodash'; +import lodash, { throttle } from 'lodash'; import PQueue from 'p-queue'; import type { IPCType } from '../../window.d.ts'; @@ -35,6 +35,7 @@ import { conversationJobQueue, conversationQueueJobEnum, } from '../../jobs/conversationJobQueue.js'; +import { isEnabled } from '../../RemoteConfig.js'; const { groupBy, mapValues } = lodash; @@ -499,6 +500,56 @@ ipc.on('sql-error', () => { }); }); +let untoastedMainProcessErrorLogCount = 0; +let untoastedMainProcessErrorLogs: Array = []; +const MAX_MAIN_PROCESS_ERROR_LOGS_TO_CACHE = 5; + +ipc.on('logging-error', (_event, logLine) => { + if (isProduction(window.getVersion())) { + return; + } + + if (!isEnabled('desktop.loggingErrorToasts')) { + return; + } + + untoastedMainProcessErrorLogCount += 1; + const numCached = untoastedMainProcessErrorLogs.unshift(logLine); + if (numCached > MAX_MAIN_PROCESS_ERROR_LOGS_TO_CACHE) { + untoastedMainProcessErrorLogs.pop(); + } + + throttledHandleMainProcessErrors(); +}); + +const throttledHandleMainProcessErrors = throttle( + _handleMainProcessErrors, + 5000 +); + +function _handleMainProcessErrors() { + if (!window.reduxActions) { + // Try again in a bit! + throttledHandleMainProcessErrors(); + return; + } + + if (untoastedMainProcessErrorLogs.length === 0) { + return; + } + + window.reduxActions.toast.showToast({ + toastType: ToastType._InternalMainProcessLoggingError, + parameters: { + count: untoastedMainProcessErrorLogCount, + logLines: untoastedMainProcessErrorLogs, + }, + }); + + untoastedMainProcessErrorLogCount = 0; + untoastedMainProcessErrorLogs = []; +} + ipc.on( 'art-creator:uploadStickerPack', async (