Context isolation for the debug log window
This commit is contained in:
parent
0f9608d9a3
commit
fa66ddde0f
28 changed files with 477 additions and 686 deletions
32
ts/components/DebugLogWindow.stories.tsx
Normal file
32
ts/components/DebugLogWindow.stories.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { DebugLogWindow, PropsType } from './DebugLogWindow';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import { sleep } from '../util/sleep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (): PropsType => ({
|
||||
closeWindow: action('closeWindow'),
|
||||
downloadLog: action('downloadLog'),
|
||||
i18n,
|
||||
fetchLogs: () => {
|
||||
action('fetchLogs')();
|
||||
return Promise.resolve('Sample logs');
|
||||
},
|
||||
uploadLogs: async (logs: string) => {
|
||||
action('uploadLogs')(logs);
|
||||
await sleep(5000);
|
||||
return 'https://picsum.photos/1800/900';
|
||||
},
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/DebugLogWindow', module);
|
||||
|
||||
story.add('DebugLogWindow', () => <DebugLogWindow {...createProps()} />);
|
207
ts/components/DebugLogWindow.tsx
Normal file
207
ts/components/DebugLogWindow.tsx
Normal file
|
@ -0,0 +1,207 @@
|
|||
// Copyright 2015-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { MouseEvent, useEffect, useState } from 'react';
|
||||
import copyText from 'copy-text-to-clipboard';
|
||||
import * as log from '../logging/log';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { Spinner } from './Spinner';
|
||||
import { ToastDebugLogError } from './ToastDebugLogError';
|
||||
import { ToastLinkCopied } from './ToastLinkCopied';
|
||||
import { ToastLoadingFullLogs } from './ToastLoadingFullLogs';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
|
||||
enum LoadState {
|
||||
NotStarted,
|
||||
Started,
|
||||
Loaded,
|
||||
Submitting,
|
||||
}
|
||||
|
||||
export type PropsType = {
|
||||
closeWindow: () => unknown;
|
||||
downloadLog: (text: string) => unknown;
|
||||
i18n: LocalizerType;
|
||||
fetchLogs: () => Promise<string>;
|
||||
uploadLogs: (logs: string) => Promise<string>;
|
||||
};
|
||||
|
||||
enum ToastType {
|
||||
Copied,
|
||||
Error,
|
||||
Loading,
|
||||
}
|
||||
|
||||
export const DebugLogWindow = ({
|
||||
closeWindow,
|
||||
downloadLog,
|
||||
i18n,
|
||||
fetchLogs,
|
||||
uploadLogs,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted);
|
||||
const [logText, setLogText] = useState<string | undefined>();
|
||||
const [publicLogURL, setPublicLogURL] = useState<string | undefined>();
|
||||
const [textAreaValue, setTextAreaValue] = useState<string>(i18n('loading'));
|
||||
const [toastType, setToastType] = useState<ToastType | undefined>();
|
||||
|
||||
useEscapeHandling(closeWindow);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadState(LoadState.Started);
|
||||
|
||||
let shouldCancel = false;
|
||||
|
||||
async function doFetchLogs() {
|
||||
const fetchedLogText = await fetchLogs();
|
||||
|
||||
if (shouldCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
setToastType(ToastType.Loading);
|
||||
setLogText(fetchedLogText);
|
||||
setLoadState(LoadState.Loaded);
|
||||
|
||||
// This number is somewhat arbitrary; we want to show enough that it's
|
||||
// clear that we need to scroll, but not so many that things get slow.
|
||||
const linesToShow = Math.ceil(Math.min(window.innerHeight, 2000) / 5);
|
||||
const value = fetchedLogText.split(/\n/g, linesToShow).join('\n');
|
||||
|
||||
setTextAreaValue(value);
|
||||
setToastType(undefined);
|
||||
}
|
||||
|
||||
doFetchLogs();
|
||||
|
||||
return () => {
|
||||
shouldCancel = true;
|
||||
};
|
||||
}, [fetchLogs]);
|
||||
|
||||
const handleSubmit = async (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const text = logText;
|
||||
|
||||
if (!text || text.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadState(LoadState.Submitting);
|
||||
|
||||
try {
|
||||
const publishedLogURL = await uploadLogs(text);
|
||||
setPublicLogURL(publishedLogURL);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'DebugLogWindow error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
setLoadState(LoadState.Loaded);
|
||||
setToastType(ToastType.Error);
|
||||
}
|
||||
};
|
||||
|
||||
function closeToast() {
|
||||
setToastType(undefined);
|
||||
}
|
||||
|
||||
let toastElement: JSX.Element | undefined;
|
||||
if (toastType === ToastType.Loading) {
|
||||
toastElement = <ToastLoadingFullLogs i18n={i18n} onClose={closeToast} />;
|
||||
} else if (toastType === ToastType.Copied) {
|
||||
toastElement = <ToastLinkCopied i18n={i18n} onClose={closeToast} />;
|
||||
} else if (toastType === ToastType.Error) {
|
||||
toastElement = <ToastDebugLogError i18n={i18n} onClose={closeToast} />;
|
||||
}
|
||||
|
||||
if (publicLogURL) {
|
||||
const copyLog = (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
copyText(publicLogURL);
|
||||
setToastType(ToastType.Copied);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="DebugLogWindow">
|
||||
<div>
|
||||
<div className="DebugLogWindow__title">{i18n('debugLogSuccess')}</div>
|
||||
<p className="DebugLogWindow__subtitle">
|
||||
{i18n('debugLogSuccessNextSteps')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="DebugLogWindow__container">
|
||||
<input
|
||||
className="DebugLogWindow__link"
|
||||
readOnly
|
||||
type="text"
|
||||
value={publicLogURL}
|
||||
/>
|
||||
</div>
|
||||
<div className="DebugLogWindow__footer">
|
||||
<Button
|
||||
onClick={() => {
|
||||
openLinkInWebBrowser(
|
||||
'https://support.signal.org/hc/requests/new'
|
||||
);
|
||||
}}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
{i18n('reportIssue')}
|
||||
</Button>
|
||||
<Button onClick={copyLog}>{i18n('debugLogCopy')}</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const canSubmit = Boolean(logText) && loadState !== LoadState.Submitting;
|
||||
const canSave = Boolean(logText);
|
||||
const isLoading =
|
||||
loadState === LoadState.Started || loadState === LoadState.Submitting;
|
||||
|
||||
return (
|
||||
<div className="DebugLogWindow">
|
||||
<div>
|
||||
<div className="DebugLogWindow__title">{i18n('submitDebugLog')}</div>
|
||||
<p className="DebugLogWindow__subtitle">
|
||||
{i18n('debugLogExplanation')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="DebugLogWindow__container">
|
||||
{isLoading ? (
|
||||
<Spinner svgSize="normal" />
|
||||
) : (
|
||||
<textarea
|
||||
className="DebugLogWindow__textarea"
|
||||
readOnly
|
||||
rows={5}
|
||||
spellCheck={false}
|
||||
value={textAreaValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="DebugLogWindow__footer">
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
if (logText) {
|
||||
downloadLog(logText);
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
{i18n('debugLogSave')}
|
||||
</Button>
|
||||
<Button disabled={!canSubmit} onClick={handleSubmit}>
|
||||
{i18n('submit')}
|
||||
</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
</div>
|
||||
);
|
||||
};
|
21
ts/components/ToastDebugLogError.stories.tsx
Normal file
21
ts/components/ToastDebugLogError.stories.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ToastDebugLogError } from './ToastDebugLogError';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
const story = storiesOf('Components/ToastDebugLogError', module);
|
||||
|
||||
story.add('ToastDebugLogError', () => <ToastDebugLogError {...defaultProps} />);
|
18
ts/components/ToastDebugLogError.tsx
Normal file
18
ts/components/ToastDebugLogError.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export const ToastDebugLogError = ({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element => {
|
||||
return <Toast onClose={onClose}>{i18n('debugLogError')}</Toast>;
|
||||
};
|
|
@ -114,19 +114,23 @@ const headerSection = (
|
|||
].join('\n');
|
||||
};
|
||||
|
||||
const getHeader = ({
|
||||
capabilities,
|
||||
remoteConfig,
|
||||
statistics,
|
||||
user,
|
||||
}: Omit<FetchLogIpcData, 'logEntries'>): string =>
|
||||
const getHeader = (
|
||||
{
|
||||
capabilities,
|
||||
remoteConfig,
|
||||
statistics,
|
||||
user,
|
||||
}: Omit<FetchLogIpcData, 'logEntries'>,
|
||||
nodeVersion: string,
|
||||
appVersion: string
|
||||
): string =>
|
||||
[
|
||||
headerSection('System info', {
|
||||
Time: Date.now(),
|
||||
'User agent': window.navigator.userAgent,
|
||||
'Node version': window.getNodeVersion(),
|
||||
'Node version': nodeVersion,
|
||||
Environment: getEnvironment(),
|
||||
'App version': window.getVersion(),
|
||||
'App version': appVersion,
|
||||
}),
|
||||
headerSection('User info', user),
|
||||
headerSection('Capabilities', capabilities),
|
||||
|
@ -154,7 +158,10 @@ function formatLine(mightBeEntry: unknown): string {
|
|||
return `${getLevel(entry.level)} ${entry.time} ${entry.msg}`;
|
||||
}
|
||||
|
||||
export function fetch(): Promise<string> {
|
||||
export function fetch(
|
||||
nodeVersion: string,
|
||||
appVersion: string
|
||||
): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
ipc.send('fetch-log');
|
||||
|
||||
|
@ -163,7 +170,7 @@ export function fetch(): Promise<string> {
|
|||
let body: string;
|
||||
if (isFetchLogIpcData(data)) {
|
||||
const { logEntries } = data;
|
||||
header = getHeader(data);
|
||||
header = getHeader(data, nodeVersion, appVersion);
|
||||
body = logEntries.map(formatLine).join('\n');
|
||||
} else {
|
||||
header = headerSectionTitle('Partial logs');
|
||||
|
|
|
@ -106,30 +106,6 @@
|
|||
"updated": "2018-09-18T19:19:27.699Z",
|
||||
"reasonDetail": "Very limited in what HTML can be injected - dark/light options specify colors for the light/dark parts of QRCode"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/debug_log_start.js",
|
||||
"line": "$(document).on('keydown', e => {",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/debug_log_start.js",
|
||||
"line": "const $body = $(document.body);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/debug_log_start.js",
|
||||
"line": "window.view.$el.appendTo($body);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/key_verification_view.js",
|
||||
|
@ -13306,202 +13282,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2021-09-17T21:51:57.475Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " template: () => $('#debug-log-link').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " template: () => $('#debug-log').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.textarea = this.$('.textarea').get(0);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.submit').attr('disabled', 'disabled');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.submit').removeAttr('disabled');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.buttons, .textarea').remove();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.result').addClass('loading');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " el: this.$('.result'),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.loading').removeClass('loading');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.link').focus().select();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.loading').removeClass('loading');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " this.$('.result').text(window.i18n('debugLogError'));",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " template: () => $('#debug-log-link').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "ts/views/debug_log_view.js",
|
||||
"line": " template: () => $('#debug-log').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " template: () => $('#debug-log-link').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " template: () => $('#debug-log').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.textarea = this.$('.textarea').get(0);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.submit').attr('disabled', 'disabled');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.submit').removeAttr('disabled');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.buttons, .textarea').remove();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.result').addClass('loading');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " el: this.$('.result'),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.loading').removeClass('loading');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.link').focus().select();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.loading').removeClass('loading');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " this.$('.result').text(window.i18n('debugLogError'));",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " template: () => $('#debug-log-link').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "ts/views/debug_log_view.ts",
|
||||
"line": " template: () => $('#debug-log').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/inbox_view.js",
|
||||
|
|
|
@ -1,192 +0,0 @@
|
|||
// Copyright 2015-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import copyText from 'copy-text-to-clipboard';
|
||||
import * as log from '../logging/log';
|
||||
import * as debugLog from '../logging/debuglogs';
|
||||
import { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
|
||||
import { ToastLinkCopied } from '../components/ToastLinkCopied';
|
||||
import { showToast } from '../util/showToast';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
const { Whisper } = window;
|
||||
|
||||
// This enum-like object describes the load state of `DebugLogView`. It's designed to be
|
||||
// unidirectional; `NotStarted` → `Started` → `LogsFetchedButNotInTextarea`, etc.
|
||||
const LoadState = {
|
||||
NotStarted: 0,
|
||||
Started: 1,
|
||||
LogsFetchedButNotInTextarea: 2,
|
||||
PuttingLogsInTextarea: 3,
|
||||
LogsInTextarea: 4,
|
||||
};
|
||||
|
||||
const DebugLogLinkView = Whisper.View.extend({
|
||||
template: () => $('#debug-log-link').html(),
|
||||
initialize(options: { url: string }) {
|
||||
this.url = options.url;
|
||||
},
|
||||
events: {
|
||||
'click .copy': 'copy',
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
url: this.url,
|
||||
reportIssue: window.i18n('reportIssue'),
|
||||
debugLogCopy: window.i18n('debugLogCopy'),
|
||||
debugLogCopyAlt: window.i18n('debugLogCopyAlt'),
|
||||
};
|
||||
},
|
||||
copy(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget as HTMLAnchorElement;
|
||||
copyText(target.href);
|
||||
showToast(ToastLinkCopied);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The bulk of the logic in this view involves grabbing the logs from disk and putting
|
||||
* them in a `<textarea>`. The first part isn't instant but is reasonably fast; setting
|
||||
* the textarea's `value` takes a long time.
|
||||
*
|
||||
* After loading the logs into memory, we only put a small number of lines into the
|
||||
* textarea. If the user clicks or scrolls the textarea, we pull the full logs, which
|
||||
* can cause the system to lock up for a bit.
|
||||
*
|
||||
* Ideally, we'd only show a sampling of the logs and allow the user to download and
|
||||
* edit them in their own editor. This is mostly a stopgap solution.
|
||||
*/
|
||||
export const DebugLogView = Whisper.View.extend({
|
||||
template: () => $('#debug-log').html(),
|
||||
className: 'debug-log modal',
|
||||
initialize() {
|
||||
this.render();
|
||||
|
||||
this.textarea = this.$('.textarea').get(0);
|
||||
if (!this.textarea) {
|
||||
throw new Error('textarea not found');
|
||||
}
|
||||
this.textarea.setAttribute('readonly', '');
|
||||
|
||||
this.loadState = LoadState.NotStarted;
|
||||
this.putFullLogsInTextareaPlease = false;
|
||||
|
||||
this.fetchLogs();
|
||||
},
|
||||
events: {
|
||||
'click .textarea': 'putFullLogsInTextarea',
|
||||
'scroll .textarea': 'putFullLogsInTextarea',
|
||||
'wheel .textarea': 'putFullLogsInTextarea',
|
||||
'click .submit': 'submit',
|
||||
'click .close': 'close',
|
||||
},
|
||||
render_attributes: {
|
||||
title: window.i18n('submitDebugLog'),
|
||||
cancel: window.i18n('cancel'),
|
||||
submit: window.i18n('submit'),
|
||||
close: window.i18n('gotIt'),
|
||||
debugLogExplanation: window.i18n('debugLogExplanation'),
|
||||
},
|
||||
async fetchLogs() {
|
||||
if (this.loadState !== LoadState.NotStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadState = LoadState.Started;
|
||||
this.textarea.value = window.i18n('loading');
|
||||
this.$('.submit').attr('disabled', 'disabled');
|
||||
|
||||
this.logText = await debugLog.fetch();
|
||||
this.loadState = LoadState.LogsFetchedButNotInTextarea;
|
||||
|
||||
// This number is somewhat arbitrary; we want to show enough that it's clear that
|
||||
// we need to scroll, but not so many that things get slow.
|
||||
const linesToShow = Math.ceil(Math.min(window.innerHeight, 2000) / 5);
|
||||
this.textarea.value = this.logText
|
||||
.split(/\n/g, linesToShow)
|
||||
.concat(['', window.i18n('loading')])
|
||||
.join('\n');
|
||||
|
||||
this.$('.submit').removeAttr('disabled');
|
||||
|
||||
if (this.putFullLogsInTextareaPlease) {
|
||||
this.putFullLogsInTextarea();
|
||||
}
|
||||
},
|
||||
putFullLogsInTextarea() {
|
||||
switch (this.loadState) {
|
||||
case LoadState.NotStarted:
|
||||
case LoadState.Started:
|
||||
this.putFullLogsInTextareaPlease = true;
|
||||
break;
|
||||
case LoadState.LogsInTextarea:
|
||||
case LoadState.PuttingLogsInTextarea:
|
||||
break;
|
||||
case LoadState.LogsFetchedButNotInTextarea:
|
||||
if (!this.logText) {
|
||||
throw new Error('Expected log text to be present');
|
||||
}
|
||||
this.loadState = LoadState.PuttingLogsInTextarea;
|
||||
showToast(ToastLoadingFullLogs);
|
||||
setTimeout(() => {
|
||||
this.textarea.value = this.logText;
|
||||
this.textarea.removeAttribute('readonly');
|
||||
this.loadState = LoadState.LogsInTextarea;
|
||||
}, 0);
|
||||
break;
|
||||
default:
|
||||
// When we can, we should make this throw a `missingCaseError`.
|
||||
break;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
window.closeDebugLog();
|
||||
},
|
||||
async submit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
let text;
|
||||
switch (this.loadState) {
|
||||
case LoadState.NotStarted:
|
||||
case LoadState.Started:
|
||||
return;
|
||||
case LoadState.LogsFetchedButNotInTextarea:
|
||||
text = this.logText;
|
||||
break;
|
||||
case LoadState.LogsInTextarea:
|
||||
text = this.textarea.value;
|
||||
break;
|
||||
default:
|
||||
// When we can, we should make this throw a `missingCaseError`.
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$('.buttons, .textarea').remove();
|
||||
this.$('.result').addClass('loading');
|
||||
|
||||
try {
|
||||
const publishedLogURL = await debugLog.upload(text, window.getVersion());
|
||||
const view = new DebugLogLinkView({
|
||||
url: publishedLogURL,
|
||||
el: this.$('.result'),
|
||||
});
|
||||
this.$('.loading').removeClass('loading');
|
||||
view.render();
|
||||
this.$('.link').focus().select();
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'DebugLogView error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
this.$('.loading').removeClass('loading');
|
||||
this.$('.result').text(window.i18n('debugLogError'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
window.Whisper.DebugLogView = DebugLogView;
|
5
ts/window.d.ts
vendored
5
ts/window.d.ts
vendored
|
@ -111,7 +111,6 @@ import { QualifiedAddress } from './types/QualifiedAddress';
|
|||
import { CI } from './CI';
|
||||
import { IPCEventsType, IPCEventsValuesType } from './util/createIPCEvents';
|
||||
import { ConversationView } from './views/conversation_view';
|
||||
import { DebugLogView } from './views/debug_log_view';
|
||||
import { LoggerType } from './types/Logging';
|
||||
import { SettingType } from './util/preload';
|
||||
|
||||
|
@ -162,7 +161,6 @@ declare global {
|
|||
startApp: () => void;
|
||||
|
||||
QRCode: any;
|
||||
closeDebugLog: () => unknown;
|
||||
removeSetupMenuItems: () => unknown;
|
||||
showPermissionsPopup: () => unknown;
|
||||
|
||||
|
@ -209,7 +207,6 @@ declare global {
|
|||
getLocale: () => ElectronLocaleType;
|
||||
getMediaCameraPermissions: () => Promise<boolean>;
|
||||
getMediaPermissions: () => Promise<boolean>;
|
||||
getNodeVersion: () => string;
|
||||
getServerPublicParams: () => string;
|
||||
getSfuUrl: () => string;
|
||||
getSocketStatus: () => SocketStatus;
|
||||
|
@ -499,6 +496,7 @@ declare global {
|
|||
context: SignalContext;
|
||||
getAppInstance: () => string | undefined;
|
||||
getEnvironment: () => string;
|
||||
getNodeVersion: () => string;
|
||||
getVersion: () => string;
|
||||
i18n: LocalizerType;
|
||||
log: LoggerType;
|
||||
|
@ -567,7 +565,6 @@ export class BasicReactWrapperViewClass extends AnyViewClass {
|
|||
export type WhisperType = {
|
||||
Conversation: typeof ConversationModel;
|
||||
ConversationCollection: typeof ConversationModelCollectionType;
|
||||
DebugLogView: typeof DebugLogView;
|
||||
Message: typeof MessageModel;
|
||||
MessageCollection: typeof MessageModelCollectionType;
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ export const SignalWindow = {
|
|||
getAppInstance: (): string | undefined =>
|
||||
config.appInstance ? String(config.appInstance) : undefined,
|
||||
getEnvironment,
|
||||
getNodeVersion: (): string => String(config.node_version),
|
||||
getVersion: (): string => String(config.version),
|
||||
i18n: setupI18n(locale, localeMessages),
|
||||
log: window.SignalWindow.log,
|
||||
|
|
44
ts/windows/debuglog/preload.ts
Normal file
44
ts/windows/debuglog/preload.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// It is important to call this as early as possible
|
||||
import '../context';
|
||||
|
||||
import { SignalWindow } from '../configure';
|
||||
import { DebugLogWindow } from '../../components/DebugLogWindow';
|
||||
import * as debugLog from '../../logging/debuglogs';
|
||||
|
||||
contextBridge.exposeInMainWorld('SignalWindow', {
|
||||
...SignalWindow,
|
||||
renderWindow: () => {
|
||||
const environmentText: Array<string> = [SignalWindow.getEnvironment()];
|
||||
|
||||
const appInstance = SignalWindow.getAppInstance();
|
||||
if (appInstance) {
|
||||
environmentText.push(appInstance);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(DebugLogWindow, {
|
||||
closeWindow: () => ipcRenderer.send('close-debug-log'),
|
||||
downloadLog: (logText: string) =>
|
||||
ipcRenderer.send('show-debug-log-save-dialog', logText),
|
||||
i18n: SignalWindow.i18n,
|
||||
fetchLogs() {
|
||||
return debugLog.fetch(
|
||||
SignalWindow.getNodeVersion(),
|
||||
SignalWindow.getVersion()
|
||||
);
|
||||
},
|
||||
uploadLogs(logs: string) {
|
||||
return debugLog.upload(logs, SignalWindow.getVersion());
|
||||
},
|
||||
}),
|
||||
document.getElementById('app')
|
||||
);
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue