diff --git a/app/crashReports.ts b/app/crashReports.ts index 34b9f8311c6a..13c61e480344 100644 --- a/app/crashReports.ts +++ b/app/crashReports.ts @@ -1,17 +1,34 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { app, clipboard, crashReporter, ipcMain as ipc } from 'electron'; -import { realpath, readdir, readFile, unlink } from 'fs-extra'; +import { app, crashReporter, ipcMain as ipc } from 'electron'; +import { realpath, readdir, readFile, unlink, stat } from 'fs-extra'; import { basename, join } from 'path'; +import { toJSONString as dumpToJSONString } from '@signalapp/libsignal-client/dist/Minidump'; +import z from 'zod'; import type { LoggerType } from '../ts/types/Logging'; import * as Errors from '../ts/types/errors'; import { isAlpha } from '../ts/util/version'; -import { upload as uploadDebugLog } from '../ts/logging/uploadDebugLog'; -import { SignalService as Proto } from '../ts/protobuf'; import OS from '../ts/util/os/osMain'; +const dumpSchema = z + .object({ + crashing_thread: z + .object({ + frames: z + .object({ + registers: z.unknown(), + }) + .passthrough() + .array() + .optional(), + }) + .passthrough() + .optional(), + }) + .passthrough(); + async function getPendingDumps(): Promise> { const crashDumpsPath = await realpath(app.getPath('crashDumps')); let pendingDir: string; @@ -46,7 +63,11 @@ async function eraseDumps( ); } -export function setup(getLogger: () => LoggerType, forceEnable = false): void { +export function setup( + getLogger: () => LoggerType, + showDebugLogWindow: () => Promise, + forceEnable = false +): void { const isEnabled = isAlpha(app.getVersion()) || forceEnable; if (isEnabled) { @@ -68,7 +89,7 @@ export function setup(getLogger: () => LoggerType, forceEnable = false): void { return pendingDumps.length; }); - ipc.handle('crash-reports:upload', async () => { + ipc.handle('crash-reports:write-to-log', async () => { if (!isEnabled) { return; } @@ -79,17 +100,26 @@ export function setup(getLogger: () => LoggerType, forceEnable = false): void { } const logger = getLogger(); - logger.warn(`crashReports: uploading ${pendingDumps.length} dumps`); + logger.warn(`crashReports: logging ${pendingDumps.length} dumps`); - const maybeDumps = await Promise.all( + await Promise.all( pendingDumps.map(async fullPath => { try { - return { - filename: basename(fullPath), - content: await readFile(fullPath), - }; - } catch (error) { + const content = await readFile(fullPath); + const { mtime } = await stat(fullPath); + + const dump = dumpSchema.parse(JSON.parse(dumpToJSONString(content))); + for (const frame of dump.crashing_thread?.frames ?? []) { + delete frame.registers; + } + logger.warn( + `crashReports: dump=${basename(fullPath)} ` + + `mtime=${JSON.stringify(mtime)}`, + JSON.stringify(dump, null, 2) + ); + } catch (error) { + logger.error( `crashReports: failed to read crash report ${fullPath} due to error`, Errors.toLogFormat(error) ); @@ -98,30 +128,8 @@ export function setup(getLogger: () => LoggerType, forceEnable = false): void { }) ); - const content = Proto.CrashReportList.encode({ - reports: maybeDumps.filter( - (dump): dump is { filename: string; content: Buffer } => { - return dump !== undefined; - } - ), - }).finish(); - - try { - const url = await uploadDebugLog({ - content, - appVersion: app.getVersion(), - logger, - extension: 'dmp', - contentType: 'application/octet-stream', - compress: false, - prefix: 'desktop-crash-', - }); - - logger.info(`crashReports: upload complete, ${url}`); - clipboard.writeText(url); - } finally { - await eraseDumps(logger, pendingDumps); - } + await eraseDumps(logger, pendingDumps); + await showDebugLogWindow(); }); ipc.handle('crash-reports:erase', async () => { diff --git a/app/main.ts b/app/main.ts index 0606dcf90e26..091dcfee470c 100644 --- a/app/main.ts +++ b/app/main.ts @@ -211,7 +211,7 @@ const FORCE_ENABLE_CRASH_REPORTS = process.argv.some( const CLI_LANG = cliOptions.lang as string | undefined; -setupCrashReports(getLogger, FORCE_ENABLE_CRASH_REPORTS); +setupCrashReports(getLogger, showDebugLogWindow, FORCE_ENABLE_CRASH_REPORTS); let sendDummyKeystroke: undefined | (() => void); if (OS.isWindows()) { diff --git a/package.json b/package.json index 5a6bb45919f5..61bdb627d2d0 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@react-aria/utils": "3.16.0", "@react-spring/web": "9.5.5", "@signalapp/better-sqlite3": "8.6.0", - "@signalapp/libsignal-client": "0.39.1", + "@signalapp/libsignal-client": "0.40.0", "@signalapp/ringrtc": "2.37.1", "@signalapp/windows-dummy-keystroke": "1.0.0", "@types/fabric": "4.5.3", diff --git a/protos/CrashReports.proto b/protos/CrashReports.proto deleted file mode 100644 index 6c93d8b604aa..000000000000 --- a/protos/CrashReports.proto +++ /dev/null @@ -1,15 +0,0 @@ -syntax = "proto3"; - -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -package signalservice; - -message CrashReport { - string filename = 1; - bytes content = 2; -} - -message CrashReportList { - repeated CrashReport reports = 1; -} diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index 16dcf607903a..bfe91a9050cd 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -1356,7 +1356,9 @@ export class ConversationController { async _forgetE164(e164: string): Promise { const { server } = window.textsecure; strictAssert(server, 'Server must be initialized'); - const serviceIdMap = await getServiceIdsForE164s(server, [e164]); + const { entries: serviceIdMap } = await getServiceIdsForE164s(server, [ + e164, + ]); const pni = serviceIdMap.get(e164)?.pni; diff --git a/ts/components/CrashReportDialog.stories.tsx b/ts/components/CrashReportDialog.stories.tsx index dc45a2dde1e0..114abfeda87f 100644 --- a/ts/components/CrashReportDialog.stories.tsx +++ b/ts/components/CrashReportDialog.stories.tsx @@ -24,9 +24,9 @@ export function Basic(): JSX.Element { { + writeCrashReportsToLog={async () => { setIsPending(true); - action('uploadCrashReports')(); + action('writeCrashReportsToLog')(); await sleep(5000); setIsPending(false); }} diff --git a/ts/components/CrashReportDialog.tsx b/ts/components/CrashReportDialog.tsx index 4f20b62c3c40..7e3f87205014 100644 --- a/ts/components/CrashReportDialog.tsx +++ b/ts/components/CrashReportDialog.tsx @@ -9,7 +9,7 @@ import { Modal } from './Modal'; import { Spinner } from './Spinner'; type PropsActionsType = { - uploadCrashReports: () => void; + writeCrashReportsToLog: () => void; eraseCrashReports: () => void; }; @@ -19,7 +19,7 @@ export type PropsType = { } & PropsActionsType; export function CrashReportDialog(props: Readonly): JSX.Element { - const { i18n, isPending, uploadCrashReports, eraseCrashReports } = props; + const { i18n, isPending, writeCrashReportsToLog, eraseCrashReports } = props; const onEraseClick = (event: React.MouseEvent) => { event.preventDefault(); @@ -30,7 +30,7 @@ export function CrashReportDialog(props: Readonly): JSX.Element { const onSubmitClick = (event: React.MouseEvent) => { event.preventDefault(); - uploadCrashReports(); + writeCrashReportsToLog(); }; const footer = ( diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index 22a5398019fa..c29b7bf51792 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -240,7 +240,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { ), diff --git a/ts/scripts/symbolicate-crash-reports.ts b/ts/scripts/symbolicate-crash-reports.ts deleted file mode 100644 index cec9dfbc92f7..000000000000 --- a/ts/scripts/symbolicate-crash-reports.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import fs from 'fs/promises'; -import path from 'path'; -import http from 'http'; -import https from 'https'; -import { tmpdir } from 'os'; -import { execFile as rawExecFile } from 'child_process'; -import { promisify } from 'util'; - -import { strictAssert } from '../util/assert'; -import { wrapEventEmitterOnce } from '../util/wrapEventEmitterOnce'; -import { SignalService as Proto } from '../protobuf'; - -const execFile = promisify(rawExecFile); - -const TARGET_URL = 'https://symbols.electronjs.org'; -const CLI_OPTIONS = []; -const CLI_ARGS = []; - -for (const arg of process.argv.slice(2)) { - if (arg.startsWith('--')) { - CLI_OPTIONS.push(arg.slice(2)); - } else { - CLI_ARGS.push(arg); - } -} - -const [OUTPUT_DIR, ...CRASH_REPORTS] = CLI_ARGS; - -main(OUTPUT_DIR, CRASH_REPORTS, CLI_OPTIONS).catch(error => { - console.error(error.stack); - process.exit(1); -}); - -async function main( - outDir: string, - fileNames: ReadonlyArray, - options: ReadonlyArray -): Promise { - await fs.mkdir(outDir, { recursive: true }); - - const substitutions = new Map(); - await Promise.all( - options.map(async option => { - const match = option.match(/^sub:(.*)=(.*)$/); - if (!match) { - return; - } - - substitutions.set(match[1], await fs.readFile(match[2])); - }) - ); - - const proxyServer = http - .createServer(async ({ method, url = '/' }, res) => { - console.log(`Proxy server got request ${method} ${url}`); - if (method !== 'GET') { - throw new Error('Unsupported'); - } - - for (const [name, buffer] of substitutions) { - if (url.includes(name)) { - console.log(`Providing substitution for ${url}`); - res.end(buffer); - return; - } - } - - // eslint-disable-next-line no-useless-escape - const patchedURL = url.replace(/signal-desktop-[^\/.]+/g, 'electron'); - - https.get(`${TARGET_URL}${patchedURL}`, remoteRes => { - res.writeHead(remoteRes.statusCode ?? 500, remoteRes.headers); - - remoteRes.pipe(res); - }); - }) - .unref(); - - proxyServer.listen(0); - - await wrapEventEmitterOnce(proxyServer, 'listening'); - const addr = proxyServer.address(); - strictAssert( - typeof addr === 'object' && addr != null, - 'Address has to be an object' - ); - - const { port: proxyPort } = addr; - - console.log(`Proxy server listening on ${proxyPort}`); - - await Promise.all( - fileNames.map(fileName => symbolicate(outDir, fileName, proxyPort)) - ); - - proxyServer.close(); -} - -async function symbolicate( - outDir: string, - fileName: string, - proxyPort: number -): Promise { - const tmpDir = await fs.mkdtemp(path.join(tmpdir(), 'signal-crashe')); - await fs.mkdir(tmpDir, { recursive: true }); - - const encoded = await fs.readFile(fileName); - let reports: ReadonlyArray; - if (fileName.endsWith('.raw')) { - reports = [ - { - filename: 'report.dmp', - content: encoded, - }, - ]; - } else { - ({ reports } = Proto.CrashReportList.decode(encoded)); - } - - const { name: prefix } = path.parse(fileName); - - await Promise.all( - reports.map(async ({ filename: reportName, content }) => { - if (!reportName || !content) { - return; - } - - const { base, name, ext } = path.parse(reportName); - if (ext !== '.dmp') { - console.log(`Ignoring ${reportName}, wrong extension`); - return; - } - - const dumpFile = path.join(tmpDir, `${prefix}-${base}`); - console.log(`Extracting to ${dumpFile}`); - await fs.writeFile(dumpFile, content); - - const outFile = path.join(outDir, `${prefix}-${name}.txt`); - - await execFile('minidump-stackwalk', [ - '--symbols-url', - `http://127.0.0.1:${proxyPort}`, - '--output-file', - outFile, - dumpFile, - ]); - console.log(`Symbolicating ${dumpFile} to ${outFile}`); - }) - ); -} diff --git a/ts/state/ducks/accounts.ts b/ts/state/ducks/accounts.ts index 650aeea4b493..19fe362bc55b 100644 --- a/ts/state/ducks/accounts.ts +++ b/ts/state/ducks/accounts.ts @@ -93,7 +93,7 @@ function checkForAccount( log.info(`checkForAccount: looking ${phoneNumber} up on server`); try { - const serviceIdLookup = await getServiceIdsForE164s(server, [ + const { entries: serviceIdLookup } = await getServiceIdsForE164s(server, [ phoneNumber, ]); const maybePair = serviceIdLookup.get(phoneNumber); diff --git a/ts/state/ducks/crashReports.ts b/ts/state/ducks/crashReports.ts index 9b9a24a22823..bdb16be56fcd 100644 --- a/ts/state/ducks/crashReports.ts +++ b/ts/state/ducks/crashReports.ts @@ -22,7 +22,7 @@ export type CrashReportsStateType = ReadonlyDeep<{ // Actions const SET_COUNT = 'crashReports/SET_COUNT'; -const UPLOAD = 'crashReports/UPLOAD'; +const WRITE_TO_LOG = 'crashReports/WRITE_TO_LOG'; const ERASE = 'crashReports/ERASE'; type SetCrashReportCountActionType = ReadonlyDeep<{ @@ -32,7 +32,7 @@ type SetCrashReportCountActionType = ReadonlyDeep<{ type CrashReportsActionType = ReadonlyDeep< | SetCrashReportCountActionType - | PromiseAction + | PromiseAction | PromiseAction >; @@ -40,7 +40,7 @@ type CrashReportsActionType = ReadonlyDeep< export const actions = { setCrashReportCount, - uploadCrashReports, + writeCrashReportsToLog, eraseCrashReports, }; @@ -48,23 +48,22 @@ function setCrashReportCount(count: number): SetCrashReportCountActionType { return { type: SET_COUNT, payload: count }; } -function uploadCrashReports(): ThunkAction< +function writeCrashReportsToLog(): ThunkAction< void, RootStateType, unknown, - PromiseAction | ShowToastActionType + PromiseAction | ShowToastActionType > { return dispatch => { async function run() { try { - await window.IPC.crashReports.upload(); - dispatch(showToast({ toastType: ToastType.LinkCopied })); + await window.IPC.crashReports.writeToLog(); } catch (error) { dispatch(showToast({ toastType: ToastType.DebugLogError })); throw error; } } - dispatch({ type: UPLOAD, payload: run() }); + dispatch({ type: WRITE_TO_LOG, payload: run() }); }; } @@ -108,7 +107,7 @@ export function reducer( } if ( - action.type === `${UPLOAD}_PENDING` || + action.type === `${WRITE_TO_LOG}_PENDING` || action.type === `${ERASE}_PENDING` ) { return { @@ -118,7 +117,7 @@ export function reducer( } if ( - action.type === `${UPLOAD}_FULFILLED` || + action.type === `${WRITE_TO_LOG}_FULFILLED` || action.type === `${ERASE}_FULFILLED` ) { return { @@ -129,13 +128,13 @@ export function reducer( } if ( - action.type === (`${UPLOAD}_REJECTED` as const) || + action.type === (`${WRITE_TO_LOG}_REJECTED` as const) || action.type === (`${ERASE}_REJECTED` as const) ) { const { error } = action; log.error( - `Failed to upload crash report due to error ${Errors.toLogFormat(error)}` + `Failed to write crash report due to error ${Errors.toLogFormat(error)}` ); return { diff --git a/ts/test-electron/updateConversationsWithUuidLookup_test.ts b/ts/test-electron/updateConversationsWithUuidLookup_test.ts index 723eff90a072..9970f5d493a6 100644 --- a/ts/test-electron/updateConversationsWithUuidLookup_test.ts +++ b/ts/test-electron/updateConversationsWithUuidLookup_test.ts @@ -153,7 +153,9 @@ describe('updateConversationsWithUuidLookup', () => { sinonSandbox.stub(window.Signal.Data, 'updateConversation'); - fakeCdsLookup = sinonSandbox.stub().resolves(new Map()); + fakeCdsLookup = sinonSandbox.stub().resolves({ + entries: new Map(), + }); fakeCheckAccountExistence = sinonSandbox.stub().resolves(false); fakeServer = { cdsLookup: fakeCdsLookup, @@ -198,12 +200,12 @@ describe('updateConversationsWithUuidLookup', () => { const aci1 = generateAci(); const aci2 = generateAci(); - fakeCdsLookup.resolves( - new Map([ + fakeCdsLookup.resolves({ + entries: new Map([ ['+13215559876', { aci: aci1, pni: undefined }], ['+16545559876', { aci: aci2, pni: undefined }], - ]) - ); + ]), + }); await updateConversationsWithUuidLookup({ conversationController: new FakeConversationController([ diff --git a/ts/textsecure/cds/CDSSocketBase.ts b/ts/textsecure/cds/CDSSocketBase.ts index 8edb491e0a2d..63ed87817b8d 100644 --- a/ts/textsecure/cds/CDSSocketBase.ts +++ b/ts/textsecure/cds/CDSSocketBase.ts @@ -122,7 +122,7 @@ export abstract class CDSSocketBase< } log.info('CDSSocket.request(): done'); - return resultMap; + return { debugPermitsUsed: 0, entries: resultMap }; } // Abstract methods diff --git a/ts/updateConversationsWithUuidLookup.ts b/ts/updateConversationsWithUuidLookup.ts index 0f32af113eeb..21d7bf17f3ed 100644 --- a/ts/updateConversationsWithUuidLookup.ts +++ b/ts/updateConversationsWithUuidLookup.ts @@ -27,7 +27,7 @@ export async function updateConversationsWithUuidLookup({ return; } - const serverLookup = await getServiceIdsForE164s(server, e164s); + const { entries: serverLookup } = await getServiceIdsForE164s(server, e164s); await Promise.all( conversations.map(async conversation => { diff --git a/ts/util/lookupConversationWithoutServiceId.ts b/ts/util/lookupConversationWithoutServiceId.ts index 130ec07f009a..8498832ebfd2 100644 --- a/ts/util/lookupConversationWithoutServiceId.ts +++ b/ts/util/lookupConversationWithoutServiceId.ts @@ -69,7 +69,9 @@ export async function lookupConversationWithoutServiceId( try { let conversationId: string | undefined; if (options.type === 'e164') { - const serverLookup = await getServiceIdsForE164s(server, [options.e164]); + const { entries: serverLookup } = await getServiceIdsForE164s(server, [ + options.e164, + ]); const maybePair = serverLookup.get(options.e164); diff --git a/ts/window.d.ts b/ts/window.d.ts index 58a043b69430..ac4221578c32 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -61,7 +61,7 @@ export type IPCType = { closeAbout: () => void; crashReports: { getCount: () => Promise; - upload: () => Promise; + writeToLog: () => Promise; erase: () => Promise; }; drawAttention: () => void; diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index 44321cef2dfd..6cfc7037b0c9 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -78,7 +78,7 @@ const IPC: IPCType = { closeAbout: () => ipc.send('close-about'), crashReports: { getCount: () => ipc.invoke('crash-reports:get-count'), - upload: () => ipc.invoke('crash-reports:upload'), + writeToLog: () => ipc.invoke('crash-reports:write-to-log'), erase: () => ipc.invoke('crash-reports:erase'), }, drawAttention: () => { diff --git a/yarn.lock b/yarn.lock index cc0bfe475909..f235b7aecf6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3955,10 +3955,10 @@ bindings "^1.5.0" tar "^6.1.0" -"@signalapp/libsignal-client@0.39.1": - version "0.39.1" - resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.39.1.tgz#15b41f15c516ae3eecf8a098a9c9c7aac00444d7" - integrity sha512-Drna/0rQTa/jB475KssoBA86Da/DLdJYDznkbiFG2YD/OeWEKoDpi64bp+BIpnc2o16GnVhGLFzNvMfVkI41eQ== +"@signalapp/libsignal-client@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.40.0.tgz#2997f44b69d4d73aa474550243998b628d0d3a1a" + integrity sha512-RrVe46KDHSfspvz+rmqkP2KMrlctWb6voejEFzhxSB8yXbnmEQuwEu7bqJj+uYsLhv8nKpIfC1qniNQc0SDPeA== dependencies: node-gyp-build "^4.2.3" type-fest "^3.5.0"