Files
mattermostest/src/main/diagnostics/steps/internal/utils.ts
Tasos Boulis 161ae99e94 [MM-47202] - Desktop diagnostics (#2439)
* Add custom assertion to match object in array (#2358)

* Add custom assertion to match object in array

* Remove test that is not implemented yet

* [MM-48213] Add "Run diagnostics" menu item under Help (#2359)

* Add submenu item that runs diagnostics

* Add custom assertion to match object in array

* Remove test that is not implemented yet

* Add tests

* Add translation

* [MM-47206] Diagnostics steps setup (#2361)

* Add baseline code for diagnostics and their steps

* Fix failing test

* [MM-47206] [MM-48155] Obfuscate logs (#2369)

* Add logging hooks to mask sensitive data

* Add hook that truncates long strings in diagnostics logs

* Add template file for creating steps

* Add readme inside diagnostics

* [MM-48145] Diagnostics step 2 - internet connectivity (#2372)

* Add diagnostics step 2 - internet connectivity check

* Update tests

* [MM-48144] Diagnostics Step - Configure logger (#2390)

* Configure logger

* Move configure logger into step1

* Add file extension to fileName variable

* Diagnostics Step 2: Validate configuration (#2391)

* Resolve conflicts with base branch

* Update test and implement Code review suggestion

* Fix failing test

* [MM-48147]Diagnostics step 3 - server connectivity (#2397)

* Add step3: Check server connectivity by using the /api/v4/system/ping endpoint

* Fix failing tests

* Add better obfuscator functions that mask all types of data (#2399)

* Add better obfuscator functions that mask all types of data(string, array, objects)

* Update tests

* [MM-48148] Add Diagnostics step 4 - session validation (#2398)

* Add diagnostics step 4 - session data validation

* Fix failing tests

* [MM-48152] Add diagnostics step 5 - BrowserWindows checks (#2404)

* Add diagnostics step 5 - browserwindow checks for main window

* Add tests

* [MM-48151] Diagnostics step 6 - Permissions (#2409)

* Add diagnostics step 6 - Permissions check

* Check permissions for microphone ond screen onn mac, windows

* Update tests count in tests

* [MM-48551] Diagnostics step 7 - Performance & Memory (#2410)

* Add diagnostics step 6 - Permissions check

* Check permissions for microphone ond screen onn mac, windows

* Update tests count in tests

* Add diagnostics step 7 - performance and memory

* Fix failing tests

* [MM-48153] Add diagnostics step 8 - Log heuristics (#2418)

* Add diagnostics step 8 - Log heuristics

* Add diagnostics step 9 - config (#2422)

* [MM-48556] Diagnostics Step 10 - Crash reports (#2423)

* Add diagnostics step 9 - config

* Add diagnostics step 10 - include crash reports

* Update tests

* Add diagnostics step 11 - cookies report (#2427)

* [MM-48157] Diagnostics report (#2432)

* Add better logging and pretty print report

* Update last step

* Update log message

* Move log after hooks so that path is masked

* Use correct directory for diagnostics files
2022-12-02 16:33:42 +02:00

243 lines
6.8 KiB
TypeScript

// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import fs from 'fs';
import https from 'https';
import readline from 'readline';
import {BrowserWindow, Rectangle, WebContents} from 'electron';
import log, {ElectronLog, LogLevel} from 'electron-log';
import {AddDurationToFnReturnObject, LogFileLineData, LogLevelAmounts, WindowStatus} from 'types/diagnostics';
import {IS_ONLINE_ENDPOINT, LOGS_MAX_STRING_LENGTH, REGEX_LOG_FILE_LINE} from 'common/constants';
export function dateTimeInFilename(date?: Date) {
const now = date ?? new Date();
return `${now.getDate()}-${now.getMonth()}-${now.getFullYear()}_${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}-${now.getMilliseconds()}`;
}
export function boundsOk(bounds?: Rectangle, strict = false): boolean {
if (!bounds) {
return false;
}
if (typeof bounds !== 'object') {
return false;
}
const propertiesOk = ['x', 'y', 'width', 'height'].every((key) => Object.prototype.hasOwnProperty.call(bounds, key));
const valueTypesOk = Object.values(bounds).every((value) => typeof value === 'number');
if (!propertiesOk || !valueTypesOk) {
return false;
}
if (strict) {
return bounds.height > 0 && bounds.width > 0 && bounds.x >= 0 && bounds.y >= 0;
}
return bounds.height >= 0 && bounds.width >= 0 && bounds.x >= 0 && bounds.y >= 0;
}
export const addDurationToFnReturnObject: AddDurationToFnReturnObject = (run) => {
return async (logger) => {
const startTime = Date.now();
const runReturnValues = await run(logger);
return {
...runReturnValues,
duration: Date.now() - startTime,
};
};
};
export function truncateString(str: string, maxLength = LOGS_MAX_STRING_LENGTH): string {
if (typeof str === 'string') {
const length = str.length;
if (length >= maxLength) {
return `${str.substring(0, 4)}...${str.substring(length - 2, length)}`;
}
}
return str;
}
export async function isOnline(logger: ElectronLog = log, url = IS_ONLINE_ENDPOINT): Promise<boolean> {
return new Promise<boolean>((resolve) => {
https.get(url, (resp) => {
let data = '';
// A chunk of data has been received.
resp.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Print out the result.
resp.on('end', () => {
logger.debug('resp.on.end', {data});
const respBody = JSON.parse(data);
if (respBody.status === 'OK') {
resolve(true);
return;
}
resolve(false);
});
}).on('error', (err) => {
logger.error('diagnostics isOnline Error', {err});
resolve(false);
});
});
}
export function browserWindowVisibilityStatus(name: string, bWindow?: BrowserWindow): WindowStatus {
const status: WindowStatus = [];
if (!bWindow) {
status.push({
name: 'windowExists',
ok: false,
});
return status;
}
const bounds = bWindow.getBounds();
const opacity = bWindow.getOpacity();
const destroyed = bWindow.isDestroyed();
const visible = bWindow.isVisible();
const enabled = bWindow.isEnabled();
const browserViewsBounds = bWindow.getBrowserViews()?.map((view) => view.getBounds());
status.push({
name: 'windowExists',
ok: true,
});
status.push({
name: 'bounds',
ok: boundsOk(bounds, true),
data: bounds,
});
status.push({
name: 'opacity',
ok: opacity > 0 && opacity <= 1,
data: opacity,
});
status.push({
name: 'destroyed',
ok: !destroyed,
});
status.push({
name: 'visible',
ok: visible,
});
status.push({
name: 'enabled',
ok: enabled,
});
status.push({
name: 'browserViewsBounds',
ok: browserViewsBounds.every((bounds) => boundsOk(bounds)),
data: browserViewsBounds,
});
return status;
}
export function webContentsCheck(webContents?: WebContents) {
if (!webContents) {
return false;
}
return !webContents.isCrashed() && !webContents.isDestroyed() && !webContents.isWaitingForResponse();
}
export async function checkPathPermissions(path?: fs.PathLike, mode?: number) {
try {
if (!path) {
throw new Error('Invalid path');
}
await fs.promises.access(path, mode);
return {
ok: true,
};
} catch (error) {
return {
ok: false,
error,
};
}
}
function parseLogFileLine(line: string, lineMatchPattern: RegExp): LogFileLineData {
const data = line.match(lineMatchPattern);
return {
text: line,
date: data?.[1],
logLevel: data?.[2] as LogLevel,
};
}
/**
* The current setup of `electron-log` rotates the file when it reaches ~1mb. It's safe to assume that the file will not be large enough to cause
* issues reading it in the same process. If this read function ever causes performance issues we should either execute it in a child process or
* read up to X amount of lines (eg 10.000)
*/
export async function readFileLineByLine(path: fs.PathLike, lineMatchPattern = REGEX_LOG_FILE_LINE): Promise<{lines: LogFileLineData[]; logLevelAmounts: LogLevelAmounts}> {
const logLevelAmounts = {
silly: 0,
debug: 0,
verbose: 0,
info: 0,
warn: 0,
error: 0,
};
const lines: LogFileLineData[] = [];
if (!path) {
return {
lines,
logLevelAmounts,
};
}
const fileStream = fs.createReadStream(path);
const rl = readline.createInterface({
input: fileStream,
/**
* Note: we use the crlfDelay option to recognize all instances of CR LF
* ('\r\n') in input.txt as a single line break.
*/
crlfDelay: Infinity,
});
let i = -1;
for await (const line of rl) {
const isValidLine = new RegExp(lineMatchPattern, 'gi').test(line);
if (isValidLine || i === -1) {
i++;
const lineData = parseLogFileLine(line, lineMatchPattern);
if (lineData.logLevel) {
logLevelAmounts[lineData.logLevel]++;
}
//push in array as new line
lines.push(lineData);
} else {
//concat with previous line
lines[i].text = `${lines[i].text}${line}`;
}
// exit loop in edge case of very large file or infinite loop
if (i >= 100000) {
break;
}
}
return {
lines,
logLevelAmounts,
};
}