
* 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
243 lines
6.8 KiB
TypeScript
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,
|
|
};
|
|
}
|