From c5dfa4c0555ecc8e3c6bb7b01e4b89ffe6edc2a5 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Fri, 26 Apr 2024 16:07:16 +0100 Subject: [PATCH] Desktop: Attach log to crash dump when the application crashes --- packages/app-desktop/app.ts | 2 ++ packages/app-desktop/bridge.ts | 51 ++++++++++++++++++++++++++++++++-- packages/utils/Logger.test.ts | 43 +++++++++++++--------------- packages/utils/Logger.ts | 27 ++++-------------- packages/utils/bytes.ts | 5 +++- packages/utils/package.json | 1 + 6 files changed, 81 insertions(+), 48 deletions(-) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 42a21363f64..5593e99e456 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -390,6 +390,8 @@ class Application extends BaseApplication { argv = await super.start(argv, startOptions); + bridge().setLogFilePath(Logger.globalLogger.logFilePath()); + await this.applySettingsSideEffects(); if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) { diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index 3e01b177118..185abf1295e 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -6,11 +6,14 @@ import { dirname, toSystemSlashes } from '@joplin/lib/path-utils'; import { fileUriToPath } from '@joplin/utils/url'; import { urlDecode } from '@joplin/lib/string-utils'; import * as Sentry from '@sentry/electron/main'; +import { ErrorEvent } from '@sentry/types/types'; import { homedir } from 'os'; import { msleep } from '@joplin/utils/time'; -import { pathExists, writeFileSync } from 'fs-extra'; +import { pathExists, pathExistsSync, writeFileSync } from 'fs-extra'; import { extname, normalize } from 'path'; import isSafeToOpen from './utils/isSafeToOpen'; +import { closeSync, openSync, readSync, statSync } from 'fs'; +import { KB } from '@joplin/utils/bytes'; interface LastSelectedPath { file: string; @@ -38,6 +41,7 @@ export class Bridge { private rootProfileDir_: string; private appName_: string; private appId_: string; + private logFilePath_ = ''; private extraAllowedExtensions_: string[] = []; private onAllowedExtensionsChangeListener_: OnAllowedExtensionsChange = ()=>{}; @@ -56,12 +60,53 @@ export class Bridge { this.sentryInit(); } + public setLogFilePath(v: string) { + this.logFilePath_ = v; + } + private sentryInit() { + const getLogLines = () => { + try { + if (!this.logFilePath_ || !pathExistsSync(this.logFilePath_)) return ''; + const { size } = statSync(this.logFilePath_); + if (!size) return ''; + + const bytesToRead = Math.min(size, 100 * KB); + const handle = openSync(this.logFilePath_, 'r'); + const position = size - bytesToRead; + const buffer = Buffer.alloc(bytesToRead); + readSync(handle, buffer, 0, bytesToRead, position); + closeSync(handle); + return buffer.toString('utf-8'); + } catch (error) { + // Can't do anything in this context + return ''; + } + }; + + const getLogAttachment = () => { + const lines = getLogLines(); + if (!lines) return null; + return { filename: 'joplin-log.txt', data: lines }; + }; + const options: Sentry.ElectronMainOptions = { - beforeSend: event => { + beforeSend: (event, hint) => { try { + const logAttachment = getLogAttachment(); + if (logAttachment) hint.attachments = [logAttachment]; const date = (new Date()).toISOString().replace(/[:-]/g, '').split('.')[0]; - writeFileSync(`${homedir()}/joplin_crash_dump_${date}.json`, JSON.stringify(event, null, '\t'), 'utf-8'); + + interface ErrorEventWithLog extends ErrorEvent { + log: string[]; + } + + const errorEventWithLog: ErrorEventWithLog = { + ...event, + log: logAttachment ? logAttachment.data.trim().split('\n') : [], + }; + + writeFileSync(`${homedir()}/joplin_crash_dump_${date}.json`, JSON.stringify(errorEventWithLog, null, '\t'), 'utf-8'); } catch (error) { // Ignore the error since we can't handle it here } diff --git a/packages/utils/Logger.test.ts b/packages/utils/Logger.test.ts index efd5c240b61..5b4e5019490 100644 --- a/packages/utils/Logger.test.ts +++ b/packages/utils/Logger.test.ts @@ -54,36 +54,33 @@ describe('Logger', () => { '', ].join('\n')); - // Shouldn't have kept any line, since keptLineCount was not set - expect(logger.keptLines).toEqual([]); - jest.useRealTimers(); }); - it('should keep the last lines', async () => { - jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); + // it('should keep the last lines', async () => { + // jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); - const logger = createLogger(); - logger.keptLineCount = 2; - logger.info('one'); - logger.info('two'); - logger.info('three'); + // const logger = createLogger(); + // logger.keptLineCount = 2; + // logger.info('one'); + // logger.info('two'); + // logger.info('three'); - await logger.waitForFileWritesToComplete_(); + // await logger.waitForFileWritesToComplete_(); - expect(await getLogContent()).toBe([ - '2020-01-01 00:00:00: testing: one', - '2020-01-01 00:00:00: testing: two', - '2020-01-01 00:00:00: testing: three', - '', - ].join('\n')); + // expect(await getLogContent()).toBe([ + // '2020-01-01 00:00:00: testing: one', + // '2020-01-01 00:00:00: testing: two', + // '2020-01-01 00:00:00: testing: three', + // '', + // ].join('\n')); - expect(logger.keptLines).toEqual([ - '2020-01-01 00:00:00: testing: two', - '2020-01-01 00:00:00: testing: three', - ]); + // expect(logger.keptLines).toEqual([ + // '2020-01-01 00:00:00: testing: two', + // '2020-01-01 00:00:00: testing: three', + // ]); - jest.useRealTimers(); - }); + // jest.useRealTimers(); + // }); }); diff --git a/packages/utils/Logger.ts b/packages/utils/Logger.ts index 32c93dfca65..ce1ba2669e3 100644 --- a/packages/utils/Logger.ts +++ b/packages/utils/Logger.ts @@ -78,8 +78,6 @@ class Logger { private level_: LogLevel = LogLevel.Info; private lastDbCleanup_: number = Date.now(); private enabled_ = true; - private keptLineCount_ = 0; - private keptLines_: string[] = []; public static fsDriver() { if (!Logger.fsDriver_) Logger.fsDriver_ = dummyFsDriver; @@ -94,18 +92,6 @@ class Logger { this.enabled_ = v; } - public get keptLineCount() { - return this.keptLineCount_; - } - - public set keptLineCount(v: number) { - this.keptLineCount_ = v; - } - - public get keptLines() { - return this.keptLines_; - } - public status(): string { const output: string[] = []; output.push(`Enabled: ${this.enabled}`); @@ -118,6 +104,12 @@ class Logger { this.globalLogger_ = logger; } + public logFilePath() { + const fileTarget = this.targets().find(t => t.type === TargetType.File); + if (!fileTarget) return ''; + return fileTarget.path || ''; + } + public static get globalLogger(): Logger { if (!this.globalLogger_) { // The global logger normally is initialized early, so we shouldn't @@ -361,13 +353,6 @@ class Logger { target.database.transactionExecBatch(queries); } } - - if (this.keptLineCount) { - if (!logLine) logLine = this.logInfoToString(level, prefix, ...object); - this.keptLines_.push(logLine); - const toDelete = this.keptLines_.length - this.keptLineCount; - if (toDelete >= 0) this.keptLines_.splice(0, toDelete); - } } // For tests diff --git a/packages/utils/bytes.ts b/packages/utils/bytes.ts index 1fc224dc98e..36fed721b91 100644 --- a/packages/utils/bytes.ts +++ b/packages/utils/bytes.ts @@ -1,4 +1,7 @@ -// eslint-disable-next-line import/prefer-default-export +export const KB = 1024; +export const MB = KB * KB; +export const GB = KB * MB; + export const bytesToHuman = (bytes: number) => { const units = ['Bytes', 'KB', 'MB', 'GB']; let unitIndex = 0; diff --git a/packages/utils/package.json b/packages/utils/package.json index fc57ea98771..d1048283d7f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -5,6 +5,7 @@ "repository": "https://github.com/laurent22/joplin/tree/dev/packages/utils", "exports": { ".": "./dist/index.js", + "./bytes": "./dist/bytes.js", "./dom": "./dist/dom.js", "./env": "./dist/env.js", "./object": "./dist/object.js",