Detect startup after recent crashes
This commit is contained in:
parent
02a732c511
commit
91f1b62bc7
23 changed files with 650 additions and 101 deletions
|
@ -6625,6 +6625,22 @@
|
||||||
"message": "Please close it manually and click Retry to continue.",
|
"message": "Please close it manually and click Retry to continue.",
|
||||||
"description": "Second line of the dialog displayed when Windows installer can't close application automatically and needs user intervention to complete the installation."
|
"description": "Second line of the dialog displayed when Windows installer can't close application automatically and needs user intervention to complete the installation."
|
||||||
},
|
},
|
||||||
|
"CrashReportDialog__title": {
|
||||||
|
"message": "Application crashed",
|
||||||
|
"description": "A title of the dialog displayed when starting an application after a recent crash"
|
||||||
|
},
|
||||||
|
"CrashReportDialog__body": {
|
||||||
|
"message": "Signal restarted after a crash. You can submit a crash a report to help Signal investigate the issue.",
|
||||||
|
"description": "The body of the dialog displayed when starting an application after a recent crash"
|
||||||
|
},
|
||||||
|
"CrashReportDialog__submit": {
|
||||||
|
"message": "Send",
|
||||||
|
"description": "A button label for submission of the crash reporter data after a recent crash"
|
||||||
|
},
|
||||||
|
"CrashReportDialog__erase": {
|
||||||
|
"message": "Don't Send",
|
||||||
|
"description": "A button label for erasure of the crash reporter data after a recent crash and continuing to start the app"
|
||||||
|
},
|
||||||
"CustomizingPreferredReactions__title": {
|
"CustomizingPreferredReactions__title": {
|
||||||
"message": "Customize reactions",
|
"message": "Customize reactions",
|
||||||
"description": "Shown in the header of the modal for customizing the preferred reactions. Also shown in the tooltip for the button that opens this modal."
|
"description": "Shown in the header of the modal for customizing the preferred reactions. Also shown in the tooltip for the button that opens this modal."
|
||||||
|
|
127
app/crashReports.ts
Normal file
127
app/crashReports.ts
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
// 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 { basename, join } from 'path';
|
||||||
|
|
||||||
|
import type { LoggerType } from '../ts/types/Logging';
|
||||||
|
import * as Errors from '../ts/types/errors';
|
||||||
|
import { isProduction } from '../ts/util/version';
|
||||||
|
import { upload as uploadDebugLog } from '../ts/logging/uploadDebugLog';
|
||||||
|
import { SignalService as Proto } from '../ts/protobuf';
|
||||||
|
|
||||||
|
async function getPendingDumps(): Promise<ReadonlyArray<string>> {
|
||||||
|
const crashDumpsPath = await realpath(app.getPath('crashDumps'));
|
||||||
|
const pendingDir = join(crashDumpsPath, 'pending');
|
||||||
|
const files = await readdir(pendingDir);
|
||||||
|
|
||||||
|
return files.map(file => join(pendingDir, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function eraseDumps(
|
||||||
|
logger: LoggerType,
|
||||||
|
files: ReadonlyArray<string>
|
||||||
|
): Promise<void> {
|
||||||
|
logger.warn(`crashReports: erasing ${files.length} pending dumps`);
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async fullPath => {
|
||||||
|
try {
|
||||||
|
await unlink(fullPath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`crashReports: failed to unlink crash report ${fullPath} due to error`,
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setup(getLogger: () => LoggerType): void {
|
||||||
|
const isEnabled = !isProduction(app.getVersion());
|
||||||
|
|
||||||
|
if (isEnabled) {
|
||||||
|
getLogger().info('crashReporter: enabled');
|
||||||
|
crashReporter.start({ uploadToServer: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc.handle('crash-reports:get-count', async () => {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingDumps = await getPendingDumps();
|
||||||
|
if (pendingDumps.length !== 0) {
|
||||||
|
getLogger().warn(
|
||||||
|
`crashReports: ${pendingDumps.length} pending dumps found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return pendingDumps.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.handle('crash-reports:upload', async () => {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingDumps = await getPendingDumps();
|
||||||
|
if (pendingDumps.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = getLogger();
|
||||||
|
logger.warn(`crashReports: uploading ${pendingDumps.length} dumps`);
|
||||||
|
|
||||||
|
const maybeDumps = await Promise.all(
|
||||||
|
pendingDumps.map(async fullPath => {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
filename: basename(fullPath),
|
||||||
|
content: await readFile(fullPath),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`crashReports: failed to read crash report ${fullPath} due to error`,
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('crashReports: upload complete');
|
||||||
|
clipboard.writeText(url);
|
||||||
|
} finally {
|
||||||
|
await eraseDumps(logger, pendingDumps);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipc.handle('crash-reports:erase', async () => {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingDumps = await getPendingDumps();
|
||||||
|
|
||||||
|
await eraseDumps(getLogger(), pendingDumps);
|
||||||
|
});
|
||||||
|
}
|
11
app/main.ts
11
app/main.ts
|
@ -7,7 +7,6 @@ import * as os from 'os';
|
||||||
import { chmod, realpath, writeFile } from 'fs-extra';
|
import { chmod, realpath, writeFile } from 'fs-extra';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
import pify from 'pify';
|
|
||||||
import normalizePath from 'normalize-path';
|
import normalizePath from 'normalize-path';
|
||||||
import fastGlob from 'fast-glob';
|
import fastGlob from 'fast-glob';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
@ -30,6 +29,7 @@ import { z } from 'zod';
|
||||||
|
|
||||||
import packageJson from '../package.json';
|
import packageJson from '../package.json';
|
||||||
import * as GlobalErrors from './global_errors';
|
import * as GlobalErrors from './global_errors';
|
||||||
|
import { setup as setupCrashReports } from './crashReports';
|
||||||
import { setup as setupSpellChecker } from './spell_check';
|
import { setup as setupSpellChecker } from './spell_check';
|
||||||
import { redactAll, addSensitivePath } from '../ts/util/privacy';
|
import { redactAll, addSensitivePath } from '../ts/util/privacy';
|
||||||
import { strictAssert } from '../ts/util/assert';
|
import { strictAssert } from '../ts/util/assert';
|
||||||
|
@ -100,7 +100,6 @@ import { load as loadLocale } from './locale';
|
||||||
import type { LoggerType } from '../ts/types/Logging';
|
import type { LoggerType } from '../ts/types/Logging';
|
||||||
|
|
||||||
const animationSettings = systemPreferences.getAnimationSettings();
|
const animationSettings = systemPreferences.getAnimationSettings();
|
||||||
const getRealPath = pify(realpath);
|
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
|
@ -1402,10 +1401,12 @@ function getAppLocale(): string {
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
let ready = false;
|
let ready = false;
|
||||||
app.on('ready', async () => {
|
app.on('ready', async () => {
|
||||||
const userDataPath = await getRealPath(app.getPath('userData'));
|
const userDataPath = await realpath(app.getPath('userData'));
|
||||||
|
|
||||||
logger = await logging.initialize(getMainWindow);
|
logger = await logging.initialize(getMainWindow);
|
||||||
|
|
||||||
|
setupCrashReports(getLogger);
|
||||||
|
|
||||||
if (!locale) {
|
if (!locale) {
|
||||||
const appLocale = getAppLocale();
|
const appLocale = getAppLocale();
|
||||||
locale = loadLocale({ appLocale, logger });
|
locale = loadLocale({ appLocale, logger });
|
||||||
|
@ -1447,7 +1448,7 @@ app.on('ready', async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const installPath = await getRealPath(app.getAppPath());
|
const installPath = await realpath(app.getAppPath());
|
||||||
|
|
||||||
addSensitivePath(userDataPath);
|
addSensitivePath(userDataPath);
|
||||||
|
|
||||||
|
@ -2081,7 +2082,7 @@ async function ensureFilePermissions(onlyFiles?: Array<string>) {
|
||||||
getLogger().info('Begin ensuring permissions');
|
getLogger().info('Begin ensuring permissions');
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const userDataPath = await getRealPath(app.getPath('userData'));
|
const userDataPath = await realpath(app.getPath('userData'));
|
||||||
// fast-glob uses `/` for all platforms
|
// fast-glob uses `/` for all platforms
|
||||||
const userDataGlob = normalizePath(join(userDataPath, '**', '*'));
|
const userDataGlob = normalizePath(join(userDataPath, '**', '*'));
|
||||||
|
|
||||||
|
|
15
protos/CrashReports.proto
Normal file
15
protos/CrashReports.proto
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
// Copyright 2020-2021 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;
|
||||||
|
}
|
|
@ -982,6 +982,10 @@ export async function startApp(): Promise<void> {
|
||||||
actionCreators.conversations,
|
actionCreators.conversations,
|
||||||
store.dispatch
|
store.dispatch
|
||||||
),
|
),
|
||||||
|
crashReports: bindActionCreators(
|
||||||
|
actionCreators.crashReports,
|
||||||
|
store.dispatch
|
||||||
|
),
|
||||||
emojis: bindActionCreators(actionCreators.emojis, store.dispatch),
|
emojis: bindActionCreators(actionCreators.emojis, store.dispatch),
|
||||||
expiration: bindActionCreators(actionCreators.expiration, store.dispatch),
|
expiration: bindActionCreators(actionCreators.expiration, store.dispatch),
|
||||||
globalModals: bindActionCreators(
|
globalModals: bindActionCreators(
|
||||||
|
@ -2382,6 +2386,11 @@ export async function startApp(): Promise<void> {
|
||||||
await window.Signal.Data.saveMessages(messagesToSave, {
|
await window.Signal.Data.saveMessages(messagesToSave, {
|
||||||
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Process crash reports if any
|
||||||
|
window.reduxActions.crashReports.setCrashReportCount(
|
||||||
|
await window.crashReports.getCount()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
function onReconnect() {
|
function onReconnect() {
|
||||||
// We disable notifications on first connect, but the same applies to reconnect. In
|
// We disable notifications on first connect, but the same applies to reconnect. In
|
||||||
|
|
33
ts/components/CrashReportDialog.stories.tsx
Normal file
33
ts/components/CrashReportDialog.stories.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
import { CrashReportDialog } from './CrashReportDialog';
|
||||||
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
import { sleep } from '../util/sleep';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const story = storiesOf('Components/CrashReportDialog', module);
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
story.add('CrashReportDialog', () => {
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CrashReportDialog
|
||||||
|
i18n={i18n}
|
||||||
|
isPending={isPending}
|
||||||
|
uploadCrashReports={async () => {
|
||||||
|
setIsPending(true);
|
||||||
|
action('uploadCrashReports')();
|
||||||
|
await sleep(5000);
|
||||||
|
setIsPending(false);
|
||||||
|
}}
|
||||||
|
eraseCrashReports={action('eraseCrashReports')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
68
ts/components/CrashReportDialog.tsx
Normal file
68
ts/components/CrashReportDialog.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { Spinner } from './Spinner';
|
||||||
|
|
||||||
|
type PropsActionsType = {
|
||||||
|
uploadCrashReports: () => void;
|
||||||
|
eraseCrashReports: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
isPending: boolean;
|
||||||
|
} & PropsActionsType;
|
||||||
|
|
||||||
|
export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element {
|
||||||
|
const { i18n, isPending, uploadCrashReports, eraseCrashReports } = props;
|
||||||
|
|
||||||
|
const onEraseClick = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
eraseCrashReports();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitClick = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
uploadCrashReports();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
moduleClassName="module-Modal--important"
|
||||||
|
i18n={i18n}
|
||||||
|
title={i18n('CrashReportDialog__title')}
|
||||||
|
hasXButton
|
||||||
|
onClose={eraseCrashReports}
|
||||||
|
>
|
||||||
|
<section>{i18n('CrashReportDialog__body')}</section>
|
||||||
|
<Modal.ButtonFooter>
|
||||||
|
<Button
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={onEraseClick}
|
||||||
|
variant={ButtonVariant.Secondary}
|
||||||
|
>
|
||||||
|
{i18n('CrashReportDialog__erase')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={onSubmitClick}
|
||||||
|
ref={button => button?.focus()}
|
||||||
|
variant={ButtonVariant.Primary}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Spinner size="22px" svgSize="small" />
|
||||||
|
) : (
|
||||||
|
i18n('CrashReportDialog__submit')
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Modal.ButtonFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { storiesOf } from '@storybook/react';
|
||||||
import type { PropsType } from './LeftPane';
|
import type { PropsType } from './LeftPane';
|
||||||
import { LeftPane, LeftPaneMode } from './LeftPane';
|
import { LeftPane, LeftPaneMode } from './LeftPane';
|
||||||
import { CaptchaDialog } from './CaptchaDialog';
|
import { CaptchaDialog } from './CaptchaDialog';
|
||||||
|
import { CrashReportDialog } from './CrashReportDialog';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import { MessageSearchResult } from './conversationList/MessageSearchResult';
|
import { MessageSearchResult } from './conversationList/MessageSearchResult';
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
@ -104,6 +105,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
['idle', 'required', 'pending'],
|
['idle', 'required', 'pending'],
|
||||||
'idle'
|
'idle'
|
||||||
),
|
),
|
||||||
|
crashReportCount: select('challengeReportCount', [0, 1], 0),
|
||||||
setChallengeStatus: action('setChallengeStatus'),
|
setChallengeStatus: action('setChallengeStatus'),
|
||||||
renderExpiredBuildDialog: () => <div />,
|
renderExpiredBuildDialog: () => <div />,
|
||||||
renderMainHeader: () => <div />,
|
renderMainHeader: () => <div />,
|
||||||
|
@ -134,6 +136,14 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
onSkip={action('onCaptchaSkip')}
|
onSkip={action('onCaptchaSkip')}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
renderCrashReportDialog: () => (
|
||||||
|
<CrashReportDialog
|
||||||
|
i18n={i18n}
|
||||||
|
isPending={false}
|
||||||
|
uploadCrashReports={action('uploadCrashReports')}
|
||||||
|
eraseCrashReports={action('eraseCrashReports')}
|
||||||
|
/>
|
||||||
|
),
|
||||||
selectedConversationId: undefined,
|
selectedConversationId: undefined,
|
||||||
selectedMessageId: undefined,
|
selectedMessageId: undefined,
|
||||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||||
|
@ -633,6 +643,24 @@ story.add('Captcha dialog: pending', () => (
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Crash report flow
|
||||||
|
|
||||||
|
story.add('Crash report dialog', () => (
|
||||||
|
<LeftPane
|
||||||
|
{...useProps({
|
||||||
|
modeSpecificProps: {
|
||||||
|
mode: LeftPaneMode.Inbox,
|
||||||
|
pinnedConversations,
|
||||||
|
conversations: defaultConversations,
|
||||||
|
archivedConversations: [],
|
||||||
|
isAboutToSearchInAConversation: false,
|
||||||
|
startSearchCounter: 0,
|
||||||
|
},
|
||||||
|
crashReportCount: 42,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
// Set group metadata
|
// Set group metadata
|
||||||
|
|
||||||
story.add('Group Metadata: No Timer', () => (
|
story.add('Group Metadata: No Timer', () => (
|
||||||
|
|
|
@ -92,6 +92,7 @@ export type PropsType = {
|
||||||
canResizeLeftPane: boolean;
|
canResizeLeftPane: boolean;
|
||||||
challengeStatus: 'idle' | 'required' | 'pending';
|
challengeStatus: 'idle' | 'required' | 'pending';
|
||||||
setChallengeStatus: (status: 'idle') => void;
|
setChallengeStatus: (status: 'idle') => void;
|
||||||
|
crashReportCount: number;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -144,12 +145,14 @@ export type PropsType = {
|
||||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
||||||
|
renderCrashReportDialog: () => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LeftPane: React.FC<PropsType> = ({
|
export const LeftPane: React.FC<PropsType> = ({
|
||||||
cantAddContactToGroup,
|
cantAddContactToGroup,
|
||||||
canResizeLeftPane,
|
canResizeLeftPane,
|
||||||
challengeStatus,
|
challengeStatus,
|
||||||
|
crashReportCount,
|
||||||
clearGroupCreationError,
|
clearGroupCreationError,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
closeCantAddContactToGroupModal,
|
closeCantAddContactToGroupModal,
|
||||||
|
@ -165,6 +168,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
preferredWidthFromStorage,
|
preferredWidthFromStorage,
|
||||||
renderCaptchaDialog,
|
renderCaptchaDialog,
|
||||||
|
renderCrashReportDialog,
|
||||||
renderExpiredBuildDialog,
|
renderExpiredBuildDialog,
|
||||||
renderMainHeader,
|
renderMainHeader,
|
||||||
renderMessageSearchResult,
|
renderMessageSearchResult,
|
||||||
|
@ -641,6 +645,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
setChallengeStatus('idle');
|
setChallengeStatus('idle');
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
{crashReportCount > 0 && renderCrashReportDialog()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,15 +4,6 @@
|
||||||
import { memoize, sortBy } from 'lodash';
|
import { memoize, sortBy } from 'lodash';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { ipcRenderer as ipc } from 'electron';
|
import { ipcRenderer as ipc } from 'electron';
|
||||||
import { z } from 'zod';
|
|
||||||
import FormData from 'form-data';
|
|
||||||
import { gzip } from 'zlib';
|
|
||||||
import pify from 'pify';
|
|
||||||
import type { Response } from 'got';
|
|
||||||
import got from 'got';
|
|
||||||
import { getUserAgent } from '../util/getUserAgent';
|
|
||||||
import { maybeParseUrl } from '../util/url';
|
|
||||||
import * as log from './log';
|
|
||||||
import { reallyJsonStringify } from '../util/reallyJsonStringify';
|
import { reallyJsonStringify } from '../util/reallyJsonStringify';
|
||||||
import type { FetchLogIpcData, LogEntryType } from './shared';
|
import type { FetchLogIpcData, LogEntryType } from './shared';
|
||||||
import {
|
import {
|
||||||
|
@ -25,78 +16,6 @@ import {
|
||||||
import { redactAll } from '../util/privacy';
|
import { redactAll } from '../util/privacy';
|
||||||
import { getEnvironment } from '../environment';
|
import { getEnvironment } from '../environment';
|
||||||
|
|
||||||
const BASE_URL = 'https://debuglogs.org';
|
|
||||||
|
|
||||||
const tokenBodySchema = z
|
|
||||||
.object({
|
|
||||||
fields: z.record(z.unknown()),
|
|
||||||
url: z.string(),
|
|
||||||
})
|
|
||||||
.nonstrict();
|
|
||||||
|
|
||||||
const parseTokenBody = (
|
|
||||||
rawBody: unknown
|
|
||||||
): { fields: Record<string, unknown>; url: string } => {
|
|
||||||
const body = tokenBodySchema.parse(rawBody);
|
|
||||||
|
|
||||||
const parsedUrl = maybeParseUrl(body.url);
|
|
||||||
if (!parsedUrl) {
|
|
||||||
throw new Error("Token body's URL was not a valid URL");
|
|
||||||
}
|
|
||||||
if (parsedUrl.protocol !== 'https:') {
|
|
||||||
throw new Error("Token body's URL was not HTTPS");
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const upload = async (
|
|
||||||
content: string,
|
|
||||||
appVersion: string
|
|
||||||
): Promise<string> => {
|
|
||||||
const headers = { 'User-Agent': getUserAgent(appVersion) };
|
|
||||||
|
|
||||||
const signedForm = await got.get(BASE_URL, { responseType: 'json', headers });
|
|
||||||
const { fields, url } = parseTokenBody(signedForm.body);
|
|
||||||
|
|
||||||
const uploadKey = `${fields.key}.gz`;
|
|
||||||
|
|
||||||
const form = new FormData();
|
|
||||||
// The API expects `key` to be the first field:
|
|
||||||
form.append('key', uploadKey);
|
|
||||||
Object.entries(fields)
|
|
||||||
.filter(([key]) => key !== 'key')
|
|
||||||
.forEach(([key, value]) => {
|
|
||||||
form.append(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const contentBuffer = await pify(gzip)(Buffer.from(content, 'utf8'));
|
|
||||||
const contentType = 'application/gzip';
|
|
||||||
form.append('Content-Type', contentType);
|
|
||||||
form.append('file', contentBuffer, {
|
|
||||||
contentType,
|
|
||||||
filename: `signal-desktop-debug-log-${appVersion}.txt.gz`,
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info('Debug log upload starting...');
|
|
||||||
try {
|
|
||||||
const { statusCode, body } = await got.post(url, { headers, body: form });
|
|
||||||
if (statusCode !== 204) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to upload to S3, got status ${statusCode}, body '${body}'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const response = error.response as Response<string>;
|
|
||||||
throw new Error(
|
|
||||||
`Got threw on upload to S3, got status ${response?.statusCode}, body '${response?.body}' `
|
|
||||||
);
|
|
||||||
}
|
|
||||||
log.info('Debug log upload complete.');
|
|
||||||
|
|
||||||
return `${BASE_URL}/${uploadKey}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The mechanics of preparing a log for publish
|
// The mechanics of preparing a log for publish
|
||||||
|
|
||||||
const headerSectionTitle = (title: string) => `========= ${title} =========`;
|
const headerSectionTitle = (title: string) => `========= ${title} =========`;
|
||||||
|
|
109
ts/logging/uploadDebugLog.ts
Normal file
109
ts/logging/uploadDebugLog.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2018-2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { Response } from 'got';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import got from 'got';
|
||||||
|
import { gzip } from 'zlib';
|
||||||
|
import pify from 'pify';
|
||||||
|
import { getUserAgent } from '../util/getUserAgent';
|
||||||
|
import { maybeParseUrl } from '../util/url';
|
||||||
|
import * as durations from '../util/durations';
|
||||||
|
import type { LoggerType } from '../types/Logging';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://debuglogs.org';
|
||||||
|
|
||||||
|
const UPLOAD_TIMEOUT = { request: durations.MINUTE };
|
||||||
|
|
||||||
|
const tokenBodySchema = z
|
||||||
|
.object({
|
||||||
|
fields: z.record(z.unknown()),
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
.nonstrict();
|
||||||
|
|
||||||
|
const parseTokenBody = (
|
||||||
|
rawBody: unknown
|
||||||
|
): { fields: Record<string, unknown>; url: string } => {
|
||||||
|
const body = tokenBodySchema.parse(rawBody);
|
||||||
|
|
||||||
|
const parsedUrl = maybeParseUrl(body.url);
|
||||||
|
if (!parsedUrl) {
|
||||||
|
throw new Error("Token body's URL was not a valid URL");
|
||||||
|
}
|
||||||
|
if (parsedUrl.protocol !== 'https:') {
|
||||||
|
throw new Error("Token body's URL was not HTTPS");
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadOptionsType = Readonly<{
|
||||||
|
content: string | Buffer | Uint8Array;
|
||||||
|
appVersion: string;
|
||||||
|
logger: LoggerType;
|
||||||
|
extension?: string;
|
||||||
|
contentType?: string;
|
||||||
|
compress?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const upload = async ({
|
||||||
|
content,
|
||||||
|
appVersion,
|
||||||
|
logger,
|
||||||
|
extension = 'gz',
|
||||||
|
contentType = 'application/gzip',
|
||||||
|
compress = true,
|
||||||
|
}: UploadOptionsType): Promise<string> => {
|
||||||
|
const headers = { 'User-Agent': getUserAgent(appVersion) };
|
||||||
|
|
||||||
|
const signedForm = await got.get(BASE_URL, {
|
||||||
|
responseType: 'json',
|
||||||
|
headers,
|
||||||
|
timeout: UPLOAD_TIMEOUT,
|
||||||
|
});
|
||||||
|
const { fields, url } = parseTokenBody(signedForm.body);
|
||||||
|
|
||||||
|
const uploadKey = `${fields.key}.${extension}`;
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
// The API expects `key` to be the first field:
|
||||||
|
form.append('key', uploadKey);
|
||||||
|
Object.entries(fields)
|
||||||
|
.filter(([key]) => key !== 'key')
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
form.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentBuffer = compress
|
||||||
|
? await pify(gzip)(Buffer.from(content))
|
||||||
|
: Buffer.from(content);
|
||||||
|
form.append('Content-Type', contentType);
|
||||||
|
form.append('file', contentBuffer, {
|
||||||
|
contentType,
|
||||||
|
filename: `signal-desktop-debug-log-${appVersion}.txt.gz`,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Debug log upload starting...');
|
||||||
|
try {
|
||||||
|
const { statusCode, body } = await got.post(url, {
|
||||||
|
headers,
|
||||||
|
body: form,
|
||||||
|
timeout: UPLOAD_TIMEOUT,
|
||||||
|
});
|
||||||
|
if (statusCode !== 204) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to upload to S3, got status ${statusCode}, body '${body}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const response = error.response as Response<string>;
|
||||||
|
throw new Error(
|
||||||
|
`Got threw on upload to S3, got status ${response?.statusCode}, body '${response?.body}' `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.info('Debug log upload complete.');
|
||||||
|
|
||||||
|
return `${BASE_URL}/${uploadKey}`;
|
||||||
|
};
|
31
ts/scripts/unpack-crash-reports.ts
Normal file
31
ts/scripts/unpack-crash-reports.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
|
||||||
|
async function main(fileName: string, outDir: string) {
|
||||||
|
await fs.mkdir(outDir, { recursive: true });
|
||||||
|
|
||||||
|
const encoded = await fs.readFile(fileName);
|
||||||
|
const { reports } = Proto.CrashReportList.decode(encoded);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
reports.map(async ({ filename, content }) => {
|
||||||
|
if (!filename || !content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outFile = path.join(outDir, path.basename(filename));
|
||||||
|
console.log(`Extracting to ${outFile}`);
|
||||||
|
await fs.writeFile(outFile, content);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main(process.argv[2], process.argv[3]).catch(error => {
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
|
@ -9,6 +9,7 @@ import { actions as badges } from './ducks/badges';
|
||||||
import { actions as calling } from './ducks/calling';
|
import { actions as calling } from './ducks/calling';
|
||||||
import { actions as composer } from './ducks/composer';
|
import { actions as composer } from './ducks/composer';
|
||||||
import { actions as conversations } from './ducks/conversations';
|
import { actions as conversations } from './ducks/conversations';
|
||||||
|
import { actions as crashReports } from './ducks/crashReports';
|
||||||
import { actions as emojis } from './ducks/emojis';
|
import { actions as emojis } from './ducks/emojis';
|
||||||
import { actions as expiration } from './ducks/expiration';
|
import { actions as expiration } from './ducks/expiration';
|
||||||
import { actions as globalModals } from './ducks/globalModals';
|
import { actions as globalModals } from './ducks/globalModals';
|
||||||
|
@ -31,6 +32,7 @@ export const actionCreators: ReduxActions = {
|
||||||
calling,
|
calling,
|
||||||
composer,
|
composer,
|
||||||
conversations,
|
conversations,
|
||||||
|
crashReports,
|
||||||
emojis,
|
emojis,
|
||||||
expiration,
|
expiration,
|
||||||
globalModals,
|
globalModals,
|
||||||
|
@ -53,6 +55,7 @@ export const mapDispatchToProps = {
|
||||||
...calling,
|
...calling,
|
||||||
...composer,
|
...composer,
|
||||||
...conversations,
|
...conversations,
|
||||||
|
...crashReports,
|
||||||
...emojis,
|
...emojis,
|
||||||
...expiration,
|
...expiration,
|
||||||
...globalModals,
|
...globalModals,
|
||||||
|
|
135
ts/state/ducks/crashReports.ts
Normal file
135
ts/state/ducks/crashReports.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as log from '../../logging/log';
|
||||||
|
import { showToast } from '../../util/showToast';
|
||||||
|
import * as Errors from '../../types/errors';
|
||||||
|
import { ToastLinkCopied } from '../../components/ToastLinkCopied';
|
||||||
|
import { ToastDebugLogError } from '../../components/ToastDebugLogError';
|
||||||
|
|
||||||
|
// State
|
||||||
|
|
||||||
|
export type CrashReportsStateType = {
|
||||||
|
count: number;
|
||||||
|
isPending: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
const SET_COUNT = 'crashReports/SET_COUNT';
|
||||||
|
const UPLOAD = 'crashReports/UPLOAD';
|
||||||
|
const ERASE = 'crashReports/ERASE';
|
||||||
|
|
||||||
|
type SetCrashReportCountActionType = {
|
||||||
|
type: typeof SET_COUNT;
|
||||||
|
payload: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromiseAction<Type extends string, Payload = void> =
|
||||||
|
| {
|
||||||
|
type: Type;
|
||||||
|
payload: Promise<Payload>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: `${Type}_PENDING`;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: `${Type}_FULFILLED`;
|
||||||
|
payload: Payload;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: `${Type}_REJECTED`;
|
||||||
|
error: true;
|
||||||
|
payload: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CrashReportsActionType =
|
||||||
|
| SetCrashReportCountActionType
|
||||||
|
| PromiseAction<typeof UPLOAD>
|
||||||
|
| PromiseAction<typeof ERASE>;
|
||||||
|
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
setCrashReportCount,
|
||||||
|
uploadCrashReports,
|
||||||
|
eraseCrashReports,
|
||||||
|
};
|
||||||
|
|
||||||
|
function setCrashReportCount(count: number): SetCrashReportCountActionType {
|
||||||
|
return { type: SET_COUNT, payload: count };
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadCrashReports(): PromiseAction<typeof UPLOAD> {
|
||||||
|
return { type: UPLOAD, payload: window.crashReports.upload() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraseCrashReports(): PromiseAction<typeof ERASE> {
|
||||||
|
return { type: ERASE, payload: window.crashReports.erase() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
|
||||||
|
export function getEmptyState(): CrashReportsStateType {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
isPending: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducer(
|
||||||
|
state: Readonly<CrashReportsStateType> = getEmptyState(),
|
||||||
|
action: Readonly<CrashReportsActionType>
|
||||||
|
): CrashReportsStateType {
|
||||||
|
if (action.type === SET_COUNT) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
count: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.type === `${UPLOAD}_PENDING` ||
|
||||||
|
action.type === `${ERASE}_PENDING`
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isPending: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.type === `${UPLOAD}_FULFILLED` ||
|
||||||
|
action.type === `${ERASE}_FULFILLED`
|
||||||
|
) {
|
||||||
|
if (action.type === `${UPLOAD}_FULFILLED`) {
|
||||||
|
showToast(ToastLinkCopied);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
count: 0,
|
||||||
|
isPending: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.type === (`${UPLOAD}_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)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
showToast(ToastDebugLogError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
count: 0,
|
||||||
|
isPending: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { reducer as badges } from './ducks/badges';
|
||||||
import { reducer as calling } from './ducks/calling';
|
import { reducer as calling } from './ducks/calling';
|
||||||
import { reducer as composer } from './ducks/composer';
|
import { reducer as composer } from './ducks/composer';
|
||||||
import { reducer as conversations } from './ducks/conversations';
|
import { reducer as conversations } from './ducks/conversations';
|
||||||
|
import { reducer as crashReports } from './ducks/crashReports';
|
||||||
import { reducer as emojis } from './ducks/emojis';
|
import { reducer as emojis } from './ducks/emojis';
|
||||||
import { reducer as expiration } from './ducks/expiration';
|
import { reducer as expiration } from './ducks/expiration';
|
||||||
import { reducer as globalModals } from './ducks/globalModals';
|
import { reducer as globalModals } from './ducks/globalModals';
|
||||||
|
@ -33,6 +34,7 @@ export const reducer = combineReducers({
|
||||||
calling,
|
calling,
|
||||||
composer,
|
composer,
|
||||||
conversations,
|
conversations,
|
||||||
|
crashReports,
|
||||||
emojis,
|
emojis,
|
||||||
expiration,
|
expiration,
|
||||||
globalModals,
|
globalModals,
|
||||||
|
|
19
ts/state/smart/CrashReportDialog.tsx
Normal file
19
ts/state/smart/CrashReportDialog.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { CrashReportDialog } from '../../components/CrashReportDialog';
|
||||||
|
import type { StateType } from '../reducer';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType) => {
|
||||||
|
return {
|
||||||
|
...state.crashReports,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartCrashReportDialog = smart(CrashReportDialog);
|
|
@ -58,6 +58,7 @@ import { SmartNetworkStatus } from './NetworkStatus';
|
||||||
import { SmartRelinkDialog } from './RelinkDialog';
|
import { SmartRelinkDialog } from './RelinkDialog';
|
||||||
import { SmartUpdateDialog } from './UpdateDialog';
|
import { SmartUpdateDialog } from './UpdateDialog';
|
||||||
import { SmartCaptchaDialog } from './CaptchaDialog';
|
import { SmartCaptchaDialog } from './CaptchaDialog';
|
||||||
|
import { SmartCrashReportDialog } from './CrashReportDialog';
|
||||||
|
|
||||||
function renderExpiredBuildDialog(
|
function renderExpiredBuildDialog(
|
||||||
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||||
|
@ -88,6 +89,9 @@ function renderUpdateDialog(
|
||||||
function renderCaptchaDialog({ onSkip }: { onSkip(): void }): JSX.Element {
|
function renderCaptchaDialog({ onSkip }: { onSkip(): void }): JSX.Element {
|
||||||
return <SmartCaptchaDialog onSkip={onSkip} />;
|
return <SmartCaptchaDialog onSkip={onSkip} />;
|
||||||
}
|
}
|
||||||
|
function renderCrashReportDialog(): JSX.Element {
|
||||||
|
return <SmartCrashReportDialog />;
|
||||||
|
}
|
||||||
|
|
||||||
const getModeSpecificProps = (
|
const getModeSpecificProps = (
|
||||||
state: StateType
|
state: StateType
|
||||||
|
@ -185,6 +189,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
regionCode: getRegionCode(state),
|
regionCode: getRegionCode(state),
|
||||||
challengeStatus: state.network.challengeStatus,
|
challengeStatus: state.network.challengeStatus,
|
||||||
|
crashReportCount: state.crashReports.count,
|
||||||
renderExpiredBuildDialog,
|
renderExpiredBuildDialog,
|
||||||
renderMainHeader,
|
renderMainHeader,
|
||||||
renderMessageSearchResult,
|
renderMessageSearchResult,
|
||||||
|
@ -192,6 +197,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
renderRelinkDialog,
|
renderRelinkDialog,
|
||||||
renderUpdateDialog,
|
renderUpdateDialog,
|
||||||
renderCaptchaDialog,
|
renderCaptchaDialog,
|
||||||
|
renderCrashReportDialog,
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { actions as badges } from './ducks/badges';
|
||||||
import type { actions as calling } from './ducks/calling';
|
import type { actions as calling } from './ducks/calling';
|
||||||
import type { actions as composer } from './ducks/composer';
|
import type { actions as composer } from './ducks/composer';
|
||||||
import type { actions as conversations } from './ducks/conversations';
|
import type { actions as conversations } from './ducks/conversations';
|
||||||
|
import type { actions as crashReports } from './ducks/crashReports';
|
||||||
import type { actions as emojis } from './ducks/emojis';
|
import type { actions as emojis } from './ducks/emojis';
|
||||||
import type { actions as expiration } from './ducks/expiration';
|
import type { actions as expiration } from './ducks/expiration';
|
||||||
import type { actions as globalModals } from './ducks/globalModals';
|
import type { actions as globalModals } from './ducks/globalModals';
|
||||||
|
@ -30,6 +31,7 @@ export type ReduxActions = {
|
||||||
calling: typeof calling;
|
calling: typeof calling;
|
||||||
composer: typeof composer;
|
composer: typeof composer;
|
||||||
conversations: typeof conversations;
|
conversations: typeof conversations;
|
||||||
|
crashReports: typeof crashReports;
|
||||||
emojis: typeof emojis;
|
emojis: typeof emojis;
|
||||||
expiration: typeof expiration;
|
expiration: typeof expiration;
|
||||||
globalModals: typeof globalModals;
|
globalModals: typeof globalModals;
|
||||||
|
|
|
@ -8,7 +8,9 @@ import FormData from 'form-data';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import * as zlib from 'zlib';
|
import * as zlib from 'zlib';
|
||||||
|
|
||||||
import { upload } from '../../logging/debuglogs';
|
import * as durations from '../../util/durations';
|
||||||
|
import { upload } from '../../logging/uploadDebugLog';
|
||||||
|
import * as logger from '../../logging/log';
|
||||||
|
|
||||||
const gzip: (_: zlib.InputType) => Promise<Buffer> = util.promisify(zlib.gzip);
|
const gzip: (_: zlib.InputType) => Promise<Buffer> = util.promisify(zlib.gzip);
|
||||||
|
|
||||||
|
@ -39,7 +41,7 @@ describe('upload', () => {
|
||||||
|
|
||||||
it('makes a request to get the S3 bucket, then uploads it there', async function test() {
|
it('makes a request to get the S3 bucket, then uploads it there', async function test() {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
await upload('hello world', '1.2.3'),
|
await upload({ content: 'hello world', appVersion: '1.2.3', logger }),
|
||||||
'https://debuglogs.org/abc123.gz'
|
'https://debuglogs.org/abc123.gz'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -47,6 +49,7 @@ describe('upload', () => {
|
||||||
sinon.assert.calledWith(this.fakeGet, 'https://debuglogs.org', {
|
sinon.assert.calledWith(this.fakeGet, 'https://debuglogs.org', {
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
headers: { 'User-Agent': 'Signal-Desktop/1.2.3 Linux' },
|
headers: { 'User-Agent': 'Signal-Desktop/1.2.3 Linux' },
|
||||||
|
timeout: { request: durations.MINUTE },
|
||||||
});
|
});
|
||||||
|
|
||||||
const compressedContent = await gzip('hello world');
|
const compressedContent = await gzip('hello world');
|
||||||
|
@ -54,6 +57,7 @@ describe('upload', () => {
|
||||||
sinon.assert.calledOnce(this.fakePost);
|
sinon.assert.calledOnce(this.fakePost);
|
||||||
sinon.assert.calledWith(this.fakePost, 'https://example.com/fake-upload', {
|
sinon.assert.calledWith(this.fakePost, 'https://example.com/fake-upload', {
|
||||||
headers: { 'User-Agent': 'Signal-Desktop/1.2.3 Linux' },
|
headers: { 'User-Agent': 'Signal-Desktop/1.2.3 Linux' },
|
||||||
|
timeout: { request: durations.MINUTE },
|
||||||
body: sinon.match((value: unknown) => {
|
body: sinon.match((value: unknown) => {
|
||||||
if (!(value instanceof FormData)) {
|
if (!(value instanceof FormData)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -76,7 +80,7 @@ describe('upload', () => {
|
||||||
|
|
||||||
let err: unknown;
|
let err: unknown;
|
||||||
try {
|
try {
|
||||||
await upload('hello world', '1.2.3');
|
await upload({ content: 'hello world', appVersion: '1.2.3', logger });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
|
@ -102,7 +106,7 @@ describe('upload', () => {
|
||||||
try {
|
try {
|
||||||
// Again, these should be run serially.
|
// Again, these should be run serially.
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await upload('hello world', '1.2.3');
|
await upload({ content: 'hello world', appVersion: '1.2.3', logger });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
|
@ -115,7 +119,7 @@ describe('upload', () => {
|
||||||
|
|
||||||
let err: unknown;
|
let err: unknown;
|
||||||
try {
|
try {
|
||||||
await upload('hello world', '1.2.3');
|
await upload({ content: 'hello world', appVersion: '1.2.3', logger });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7888,56 +7888,56 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.js",
|
"path": "ts/logging/uploadDebugLog.js",
|
||||||
"line": " form.append('key', uploadKey);",
|
"line": " form.append('key', uploadKey);",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.js",
|
"path": "ts/logging/uploadDebugLog.js",
|
||||||
"line": " form.append(key, value);",
|
"line": " form.append(key, value);",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.js",
|
"path": "ts/logging/uploadDebugLog.js",
|
||||||
"line": " form.append('Content-Type', contentType);",
|
"line": " form.append('Content-Type', contentType);",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.js",
|
"path": "ts/logging/uploadDebugLog.js",
|
||||||
"line": " form.append('file', contentBuffer, {",
|
"line": " form.append('file', contentBuffer, {",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.ts",
|
"path": "ts/logging/uploadDebugLog.ts",
|
||||||
"line": " form.append('key', uploadKey);",
|
"line": " form.append('key', uploadKey);",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.ts",
|
"path": "ts/logging/uploadDebugLog.ts",
|
||||||
"line": " form.append(key, value);",
|
"line": " form.append(key, value);",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.ts",
|
"path": "ts/logging/uploadDebugLog.ts",
|
||||||
"line": " form.append('Content-Type', contentType);",
|
"line": " form.append('Content-Type', contentType);",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.ts",
|
"path": "ts/logging/uploadDebugLog.ts",
|
||||||
"line": " form.append('file', contentBuffer, {",
|
"line": " form.append('file', contentBuffer, {",
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-12-17T18:08:07.752Z"
|
"updated": "2020-12-17T18:08:07.752Z"
|
||||||
|
|
5
ts/window.d.ts
vendored
5
ts/window.d.ts
vendored
|
@ -190,6 +190,11 @@ declare global {
|
||||||
baseAttachmentsPath: string;
|
baseAttachmentsPath: string;
|
||||||
baseStickersPath: string;
|
baseStickersPath: string;
|
||||||
baseTempPath: string;
|
baseTempPath: string;
|
||||||
|
crashReports: {
|
||||||
|
getCount: () => Promise<number>;
|
||||||
|
upload: () => Promise<void>;
|
||||||
|
erase: () => Promise<void>;
|
||||||
|
};
|
||||||
drawAttention: () => void;
|
drawAttention: () => void;
|
||||||
enterKeyboardMode: () => void;
|
enterKeyboardMode: () => void;
|
||||||
enterMouseMode: () => void;
|
enterMouseMode: () => void;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { contextBridge, ipcRenderer } from 'electron';
|
||||||
import { SignalContext } from '../context';
|
import { SignalContext } from '../context';
|
||||||
import { DebugLogWindow } from '../../components/DebugLogWindow';
|
import { DebugLogWindow } from '../../components/DebugLogWindow';
|
||||||
import * as debugLog from '../../logging/debuglogs';
|
import * as debugLog from '../../logging/debuglogs';
|
||||||
|
import { upload } from '../../logging/uploadDebugLog';
|
||||||
|
import * as logger from '../../logging/log';
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('SignalContext', {
|
contextBridge.exposeInMainWorld('SignalContext', {
|
||||||
...SignalContext,
|
...SignalContext,
|
||||||
|
@ -32,7 +34,11 @@ contextBridge.exposeInMainWorld('SignalContext', {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
uploadLogs(logs: string) {
|
uploadLogs(logs: string) {
|
||||||
return debugLog.upload(logs, SignalContext.getVersion());
|
return upload({
|
||||||
|
content: logs,
|
||||||
|
appVersion: SignalContext.getVersion(),
|
||||||
|
logger,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
|
|
|
@ -71,3 +71,9 @@ window.getMediaPermissions = () => ipc.invoke('settings:get:mediaPermissions');
|
||||||
|
|
||||||
window.getMediaCameraPermissions = () =>
|
window.getMediaCameraPermissions = () =>
|
||||||
ipc.invoke('settings:get:mediaCameraPermissions');
|
ipc.invoke('settings:get:mediaCameraPermissions');
|
||||||
|
|
||||||
|
window.crashReports = {
|
||||||
|
getCount: () => ipc.invoke('crash-reports:get-count'),
|
||||||
|
upload: () => ipc.invoke('crash-reports:upload'),
|
||||||
|
erase: () => ipc.invoke('crash-reports:erase'),
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue