From 62a328f74d9318b8efe40602a2f53e7f5b2abefc Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sat, 30 Sep 2023 23:36:54 +0200 Subject: [PATCH 01/11] fix: Re-work Sentry minidump loader --- .../integrations/sentry-minidump/index.ts | 110 ++++------- .../sentry-minidump/minidump-loader.ts | 177 ++++++++++-------- 2 files changed, 139 insertions(+), 148 deletions(-) diff --git a/src/main/integrations/sentry-minidump/index.ts b/src/main/integrations/sentry-minidump/index.ts index efe1723b..371e3550 100644 --- a/src/main/integrations/sentry-minidump/index.ts +++ b/src/main/integrations/sentry-minidump/index.ts @@ -1,7 +1,7 @@ import { captureEvent, getCurrentHub, Scope } from '@sentry/core'; import { NodeClient } from '@sentry/node'; import { Event, Integration } from '@sentry/types'; -import { basename, logger, SentryError } from '@sentry/utils'; +import { logger, SentryError } from '@sentry/utils'; import { app, crashReporter } from 'electron'; import { mergeEvents } from '../../../common'; @@ -12,7 +12,7 @@ import { getRendererProperties, trackRendererProperties } from '../../renderers' import { ElectronMainOptions } from '../../sdk'; import { checkPreviousSession, sessionCrashed } from '../../sessions'; import { BufferedWriteStore } from '../../store'; -import { deleteMinidump, getMinidumpLoader, MinidumpLoader } from './minidump-loader'; +import { getMinidumpLoader, MinidumpLoader } from './minidump-loader'; interface PreviousRun { scope: Scope; @@ -231,79 +231,51 @@ export class SentryMinidump implements Integration { // about it. Just use the breadcrumbs and context information we have // right now and hope that the delay was not too long. - let event: Event | null = eventIn; - if (this._minidumpLoader === undefined) { throw new SentryError('Invariant violation: Native crashes not enabled'); } - try { - const minidumps = await this._minidumpLoader(); - - if (minidumps.length > 0) { - const hub = getCurrentHub(); - const client = hub.getClient(); - - if (!client) { - return true; - } - - const enabled = client.getOptions().enabled; - - // If the SDK is not enabled, we delete the minidump files so they - // don't accumulate and/or get sent later - if (enabled === false) { - minidumps.forEach(deleteMinidump); - return false; - } - - // If this is a native main process crash, we need to apply the scope and context from the previous run - if (event?.tags?.['event.process'] === 'browser') { - const previousRun = await this._scopeLastRun; - - const storedScope = Scope.clone(previousRun?.scope); - event = await storedScope.applyToEvent(event); - - if (event && previousRun) { - event.release = previousRun.event?.release || event.release; - event.environment = previousRun.event?.environment || event.environment; - event.contexts = previousRun.event?.contexts || event.contexts; - } - } - - const hubScope = hub.getScope(); - event = hubScope && event ? await hubScope.applyToEvent(event) : event; - - if (!event) { - return false; - } - - for (const minidump of minidumps) { - const data = await minidump.load(); - - if (data) { - captureEvent(event, { - attachments: [ - { - attachmentType: 'event.minidump', - filename: basename(minidump.path), - data, - }, - ], - }); - } - - void deleteMinidump(minidump); - } - - // Unset to recover memory - this._scopeLastRun = undefined; - return true; + const hub = getCurrentHub(); + const client = hub.getClient(); + + if (!client) { + return true; + } + + let event: Event | null = eventIn; + + // If this is a native main process crash, we need to apply the scope and context from the previous run + if (event.tags?.['event.process'] === 'browser') { + const previousRun = await this._scopeLastRun; + + const storedScope = Scope.clone(previousRun?.scope); + event = await storedScope.applyToEvent(event); + + if (event && previousRun) { + event.release = previousRun.event?.release || event.release; + event.environment = previousRun.event?.environment || event.environment; + event.contexts = previousRun.event?.contexts || event.contexts; } - } catch (_oO) { - logger.error('Error while sending native crash.'); } - return false; + const hubScope = hub.getScope(); + event = hubScope && event ? await hubScope.applyToEvent(event) : event; + + if (!event) { + return false; + } + + // If the SDK is not enabled, tell the loader to delete all minidumps + const deleteAll = client.getOptions().enabled === false; + + let minidumpSent = false; + for await (const attachment of this._minidumpLoader(deleteAll)) { + captureEvent(event, { attachments: [attachment] }); + minidumpSent = true; + } + + // Unset to recover memory + this._scopeLastRun = undefined; + return minidumpSent; } } diff --git a/src/main/integrations/sentry-minidump/minidump-loader.ts b/src/main/integrations/sentry-minidump/minidump-loader.ts index 20318b95..b6591b57 100644 --- a/src/main/integrations/sentry-minidump/minidump-loader.ts +++ b/src/main/integrations/sentry-minidump/minidump-loader.ts @@ -1,4 +1,5 @@ -import { logger } from '@sentry/utils'; +import { Attachment } from '@sentry/types'; +import { basename, logger } from '@sentry/utils'; import { join } from 'path'; import { getCrashesDirectory, usesCrashpad } from '../../electron-normalize'; @@ -6,70 +7,112 @@ import { readDirAsync, readFileAsync, statAsync, unlinkAsync } from '../../fs'; /** Maximum number of days to keep a minidump before deleting it. */ const MAX_AGE = 30; +/** Minimum number of seconds a minidump should not be modified for before we assume writing is complete */ +const MIN_NOT_MODIFIED = 2; +const MINIDUMP_HEADER = 'MDMP'; -export interface MinidumpFile { - path: string; - load(): Promise; +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); } -export type MinidumpLoader = () => Promise; +export type MinidumpLoader = (deleteAll: boolean) => AsyncGenerator; -async function filterAsync( - array: T[], - predicate: (item: T) => Promise | boolean, - thisArg?: any, -): Promise { - const verdicts = await Promise.all(array.map(predicate, thisArg)); - return array.filter((_, index) => verdicts[index]); -} +/** Creates a minidump loader */ +export function createMinidumpLoader( + getMinidumpPaths: () => Promise, + preProcessFile: (file: Buffer) => Buffer = (file) => file, +): MinidumpLoader { + // Keep track of which minidumps we are currently processing in case this function is called before we're finished + const processingPaths: Set = new Set(); -/** Deletes a minidump */ -export async function deleteMinidump(dump: MinidumpFile): Promise { - try { - await unlinkAsync(dump.path); - } catch (e) { - logger.warn('Could not delete', dump.path); + /** Deletes a file and removes it from the processing paths */ + async function cleanup(path: string): Promise { + try { + await unlinkAsync(path); + } catch (e) { + logger.warn('Could not delete', path); + } finally { + processingPaths.delete(path); + } } -} -function createMinidumpLoader(fetchMinidumpsImpl: MinidumpLoader): MinidumpLoader { - const knownPaths: string[] = []; + /** The generator function */ + async function* getMinidumps(deleteAll: boolean): AsyncGenerator { + for (const path of await getMinidumpPaths()) { + try { + // Ignore non-minidump files + if (!path.endsWith('.dmp')) { + continue; + } - return async () => { - const minidumps = await fetchMinidumpsImpl(); - logger.log(`Found ${minidumps.length} minidumps`); + // Ignore minidumps we are already processing + if (processingPaths.has(path)) { + continue; + } - const oldestMs = new Date().getTime() - MAX_AGE * 24 * 3_600 * 1_000; - return filterAsync(minidumps, async (dump) => { - // Skip files that we have seen before - if (knownPaths.indexOf(dump.path) >= 0) { - return false; - } + processingPaths.add(path); - // Lock this minidump until we have uploaded it or an error occurs and we - // remove it from the file system. - knownPaths.push(dump.path); + if (deleteAll) { + await cleanup(path); + continue; + } - const stats = await statAsync(dump.path); + logger.log('Found minidump', path); - // We do not want to upload minidumps that have been generated before a - // certain threshold. Those old files can be deleted immediately. - const tooOld = stats.birthtimeMs < oldestMs; - const tooSmall = stats.size < 1024; + let stats = await statAsync(path); - if (tooSmall) { - logger.log('Minidump too small to be valid', dump.path); - } + const thirtyDaysAgo = new Date().getTime() - MAX_AGE * 24 * 3_600 * 1_000; + + if (stats.birthtimeMs < thirtyDaysAgo) { + logger.log(`Ignoring minidump as it is over ${MAX_AGE} days old`); + await cleanup(path); + continue; + } + + let retries = 0; + + while (retries <= 10) { + const twoSecondsAgo = new Date().getTime() - MIN_NOT_MODIFIED * 1_000; + + if (stats.mtimeMs < twoSecondsAgo) { + const file = await readFileAsync(path); + const data = preProcessFile(file); + await cleanup(path); - if (tooOld || tooSmall) { - await deleteMinidump(dump); - knownPaths.splice(knownPaths.indexOf(dump.path), 1); - return false; + if (data.length < 10_000 || data.subarray(0, 4).toString() !== MINIDUMP_HEADER) { + logger.warn('Dropping minidump as it appears invalid.'); + break; + } + + logger.log('Sending minidump'); + + yield { + attachmentType: 'event.minidump', + filename: basename(path), + data, + }; + + break; + } + + logger.log(`Minidump has been modified in the last ${MIN_NOT_MODIFIED} seconds. Checking again in a second.`); + retries += 1; + await delay(1_000); + stats = await statAsync(path); + } + + if (retries >= 10) { + logger.warn('Timed out waiting for minidump to stop being modified'); + await cleanup(path); + } + } catch (e) { + logger.error('Failed to load minidump', e); + await cleanup(path); } + } + } - return true; - }); - }; + return getMinidumps; } /** Attempts to remove the metadata file so Crashpad doesn't output `failed to stat report` errors to the console */ @@ -119,24 +162,15 @@ function crashpadMinidumpLoader(): MinidumpLoader { return createMinidumpLoader(async () => { await deleteCrashpadMetadataFile(crashesDirectory).catch((error) => logger.error(error)); - - const files = await readDirsAsync(dumpDirectories); - return files - .filter((file) => file.endsWith('.dmp')) - .map((path) => { - return { - path, - load: () => readFileAsync(path), - }; - }); + return readDirsAsync(dumpDirectories); }); } /** Crudely parses the minidump from the Breakpad multipart file */ -function minidumpFromBreakpadMultipart(file: Buffer): Buffer | undefined { +function minidumpFromBreakpadMultipart(file: Buffer): Buffer { const binaryStart = file.lastIndexOf('Content-Type: application/octet-stream'); if (binaryStart > 0) { - const dumpStart = file.indexOf('MDMP', binaryStart); + const dumpStart = file.indexOf(MINIDUMP_HEADER, binaryStart); const dumpEnd = file.lastIndexOf('----------------------------'); if (dumpStart > 0 && dumpEnd > 0 && dumpEnd > dumpStart) { @@ -144,7 +178,7 @@ function minidumpFromBreakpadMultipart(file: Buffer): Buffer | undefined { } } - return undefined; + return file; } function removeBreakpadMetadata(crashesDirectory: string, paths: string[]): void { @@ -170,24 +204,9 @@ function breakpadMinidumpLoader(): MinidumpLoader { // Breakpad stores all minidump files along with a metadata file directly in // the crashes directory. const files = await readDirAsync(crashesDirectory); - removeBreakpadMetadata(crashesDirectory, files); - - return files - .filter((file) => file.endsWith('.dmp')) - .map((file) => { - const path = join(crashesDirectory, file); - - return { - path, - load: async () => { - const file = await readFileAsync(path); - return minidumpFromBreakpadMultipart(file) || file; - }, - }; - }) - .filter((m) => !!m); - }); + return files; + }, minidumpFromBreakpadMultipart); } /** From f931c668f039a4190effd45763e5250b0e508102 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 1 Oct 2023 00:47:15 +0200 Subject: [PATCH 02/11] Use a callback since asyncIterator is not supported in older versions of node --- src/main/integrations/sentry-minidump/index.ts | 6 +++--- .../integrations/sentry-minidump/minidump-loader.ts | 13 +++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/integrations/sentry-minidump/index.ts b/src/main/integrations/sentry-minidump/index.ts index 371e3550..a285460b 100644 --- a/src/main/integrations/sentry-minidump/index.ts +++ b/src/main/integrations/sentry-minidump/index.ts @@ -269,10 +269,10 @@ export class SentryMinidump implements Integration { const deleteAll = client.getOptions().enabled === false; let minidumpSent = false; - for await (const attachment of this._minidumpLoader(deleteAll)) { - captureEvent(event, { attachments: [attachment] }); + await this._minidumpLoader(deleteAll, (attachment) => { + captureEvent(event as Event, { attachments: [attachment] }); minidumpSent = true; - } + }); // Unset to recover memory this._scopeLastRun = undefined; diff --git a/src/main/integrations/sentry-minidump/minidump-loader.ts b/src/main/integrations/sentry-minidump/minidump-loader.ts index b6591b57..238e4947 100644 --- a/src/main/integrations/sentry-minidump/minidump-loader.ts +++ b/src/main/integrations/sentry-minidump/minidump-loader.ts @@ -15,7 +15,7 @@ function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -export type MinidumpLoader = (deleteAll: boolean) => AsyncGenerator; +export type MinidumpLoader = (deleteAll: boolean, callback: (attachment: Attachment) => void) => Promise; /** Creates a minidump loader */ export function createMinidumpLoader( @@ -36,8 +36,7 @@ export function createMinidumpLoader( } } - /** The generator function */ - async function* getMinidumps(deleteAll: boolean): AsyncGenerator { + return async (deleteAll, callback) => { for (const path of await getMinidumpPaths()) { try { // Ignore non-minidump files @@ -86,11 +85,11 @@ export function createMinidumpLoader( logger.log('Sending minidump'); - yield { + callback({ attachmentType: 'event.minidump', filename: basename(path), data, - }; + }); break; } @@ -110,9 +109,7 @@ export function createMinidumpLoader( await cleanup(path); } } - } - - return getMinidumps; + }; } /** Attempts to remove the metadata file so Crashpad doesn't output `failed to stat report` errors to the console */ From 799ef5544ee588158b425b875cb307337af5ecd9 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 1 Oct 2023 01:33:15 +0200 Subject: [PATCH 03/11] fix breakpad paths --- src/main/integrations/sentry-minidump/minidump-loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/integrations/sentry-minidump/minidump-loader.ts b/src/main/integrations/sentry-minidump/minidump-loader.ts index 238e4947..31fda21d 100644 --- a/src/main/integrations/sentry-minidump/minidump-loader.ts +++ b/src/main/integrations/sentry-minidump/minidump-loader.ts @@ -202,7 +202,7 @@ function breakpadMinidumpLoader(): MinidumpLoader { // the crashes directory. const files = await readDirAsync(crashesDirectory); removeBreakpadMetadata(crashesDirectory, files); - return files; + return files.map((file) => join(crashesDirectory, file)); }, minidumpFromBreakpadMultipart); } From b38e05bfc38457552e0b651e5fed35c5ab58470b Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 1 Oct 2023 02:08:35 +0200 Subject: [PATCH 04/11] Extend test times --- test/e2e/context.ts | 2 +- .../other/main-process-module/event.json | 39 +------------------ .../other/main-process-module/src/main.js | 2 +- .../src/main.js | 2 +- .../sessions/abnormal-exit/src/main.js | 2 +- .../src/main.js | 2 +- .../src/main.js | 2 +- .../native-crash-renderer/src/main.js | 2 +- 8 files changed, 8 insertions(+), 45 deletions(-) diff --git a/test/e2e/context.ts b/test/e2e/context.ts index 028482d2..80a7ed2c 100644 --- a/test/e2e/context.ts +++ b/test/e2e/context.ts @@ -136,7 +136,7 @@ export class TestContext { public async waitForTrue( method: () => boolean | Promise, message: () => string = () => 'Timeout', - timeout: number = 8_000, + timeout: number = 12_000, ): Promise { if (!this.mainProcess) { throw new Error('Invariant violation: Call .start() first'); diff --git a/test/e2e/test-apps/other/main-process-module/event.json b/test/e2e/test-apps/other/main-process-module/event.json index 805e3cb8..9557d8f7 100644 --- a/test/e2e/test-apps/other/main-process-module/event.json +++ b/test/e2e/test-apps/other/main-process-module/event.json @@ -95,44 +95,7 @@ "event_id": "{{id}}", "platform": "node", "timestamp": 0, - "breadcrumbs": [ - { - "timestamp": 0, - "category": "electron", - "message": "app.will-finish-launching", - "type": "ui" - }, - { - "timestamp": 0, - "category": "electron", - "message": "app.ready", - "type": "ui" - }, - { - "timestamp": 0, - "category": "electron", - "message": "app.session-created", - "type": "ui" - }, - { - "timestamp": 0, - "category": "electron", - "message": "app.web-contents-created", - "type": "ui" - }, - { - "timestamp": 0, - "category": "electron", - "message": "app.browser-window-created", - "type": "ui" - }, - { - "timestamp": 0, - "category": "electron", - "message": "renderer.dom-ready", - "type": "ui" - } - ], + "breadcrumbs": [], "tags": { "event.environment": "javascript", "event.origin": "electron", diff --git a/test/e2e/test-apps/other/main-process-module/src/main.js b/test/e2e/test-apps/other/main-process-module/src/main.js index 5aa1da82..bd094b99 100644 --- a/test/e2e/test-apps/other/main-process-module/src/main.js +++ b/test/e2e/test-apps/other/main-process-module/src/main.js @@ -32,5 +32,5 @@ app.on('ready', () => { setTimeout(() => { throw new Error('Some main error'); - }, 500); + }, 2000); }); diff --git a/test/e2e/test-apps/sessions/abnormal-exit-electron-uploader/src/main.js b/test/e2e/test-apps/sessions/abnormal-exit-electron-uploader/src/main.js index ee658220..3004ed38 100644 --- a/test/e2e/test-apps/sessions/abnormal-exit-electron-uploader/src/main.js +++ b/test/e2e/test-apps/sessions/abnormal-exit-electron-uploader/src/main.js @@ -37,4 +37,4 @@ setTimeout(() => { } else { app.quit(); } -}, 2000); +}, 4000); diff --git a/test/e2e/test-apps/sessions/abnormal-exit/src/main.js b/test/e2e/test-apps/sessions/abnormal-exit/src/main.js index 3611302d..ccb41cd2 100644 --- a/test/e2e/test-apps/sessions/abnormal-exit/src/main.js +++ b/test/e2e/test-apps/sessions/abnormal-exit/src/main.js @@ -30,4 +30,4 @@ setTimeout(() => { } else { app.quit(); } -}, 2000); +}, 4000); diff --git a/test/e2e/test-apps/sessions/native-crash-main-electron-uploader/src/main.js b/test/e2e/test-apps/sessions/native-crash-main-electron-uploader/src/main.js index 0b7294b6..4f168635 100644 --- a/test/e2e/test-apps/sessions/native-crash-main-electron-uploader/src/main.js +++ b/test/e2e/test-apps/sessions/native-crash-main-electron-uploader/src/main.js @@ -37,4 +37,4 @@ setTimeout(() => { } else { app.quit(); } -}, 4000); +}, 6000); diff --git a/test/e2e/test-apps/sessions/native-crash-renderer-electron-uploader/src/main.js b/test/e2e/test-apps/sessions/native-crash-renderer-electron-uploader/src/main.js index aa89b617..958d8ca5 100644 --- a/test/e2e/test-apps/sessions/native-crash-renderer-electron-uploader/src/main.js +++ b/test/e2e/test-apps/sessions/native-crash-renderer-electron-uploader/src/main.js @@ -31,4 +31,4 @@ app.on('ready', () => { setTimeout(() => { app.quit(); -}, 4000); +}, 6000); diff --git a/test/e2e/test-apps/sessions/native-crash-renderer/src/main.js b/test/e2e/test-apps/sessions/native-crash-renderer/src/main.js index 600e177b..529b0d6d 100644 --- a/test/e2e/test-apps/sessions/native-crash-renderer/src/main.js +++ b/test/e2e/test-apps/sessions/native-crash-renderer/src/main.js @@ -24,4 +24,4 @@ app.on('ready', () => { setTimeout(() => { app.quit(); -}, 4000); +}, 6000); From d4e994a7c015224c653a8e06c20795f673b43ebe Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 1 Oct 2023 13:46:25 +0200 Subject: [PATCH 05/11] Use the `Mutex` --- .../sentry-minidump/minidump-loader.ts | 128 ++++++++---------- 1 file changed, 58 insertions(+), 70 deletions(-) diff --git a/src/main/integrations/sentry-minidump/minidump-loader.ts b/src/main/integrations/sentry-minidump/minidump-loader.ts index 31fda21d..aea07703 100644 --- a/src/main/integrations/sentry-minidump/minidump-loader.ts +++ b/src/main/integrations/sentry-minidump/minidump-loader.ts @@ -2,6 +2,7 @@ import { Attachment } from '@sentry/types'; import { basename, logger } from '@sentry/utils'; import { join } from 'path'; +import { Mutex } from '../../../common/mutex'; import { getCrashesDirectory, usesCrashpad } from '../../electron-normalize'; import { readDirAsync, readFileAsync, statAsync, unlinkAsync } from '../../fs'; @@ -17,98 +18,84 @@ function delay(ms: number): Promise { export type MinidumpLoader = (deleteAll: boolean, callback: (attachment: Attachment) => void) => Promise; -/** Creates a minidump loader */ +/** + * Creates a minidump loader + * @param getMinidumpPaths A function that returns paths to minidumps + * @param preProcessFile A function that pre-processes the minidump file + * @returns A function to fetch minidumps + */ export function createMinidumpLoader( getMinidumpPaths: () => Promise, preProcessFile: (file: Buffer) => Buffer = (file) => file, ): MinidumpLoader { - // Keep track of which minidumps we are currently processing in case this function is called before we're finished - const processingPaths: Set = new Set(); - - /** Deletes a file and removes it from the processing paths */ - async function cleanup(path: string): Promise { - try { - await unlinkAsync(path); - } catch (e) { - logger.warn('Could not delete', path); - } finally { - processingPaths.delete(path); - } - } + // The mutex protects against a whole host of reentrancy issues and race conditions. + const mutex = new Mutex(); return async (deleteAll, callback) => { - for (const path of await getMinidumpPaths()) { - try { - // Ignore non-minidump files - if (!path.endsWith('.dmp')) { - continue; - } + // any calls to this function will be queued and run exclusively + await mutex.runExclusive(async () => { + for (const path of await getMinidumpPaths()) { + try { + if (deleteAll) { + continue; + } - // Ignore minidumps we are already processing - if (processingPaths.has(path)) { - continue; - } + logger.log('Found minidump', path); - processingPaths.add(path); + let stats = await statAsync(path); - if (deleteAll) { - await cleanup(path); - continue; - } + const thirtyDaysAgo = new Date().getTime() - MAX_AGE * 24 * 3_600 * 1_000; - logger.log('Found minidump', path); + if (stats.birthtimeMs < thirtyDaysAgo) { + logger.log(`Ignoring minidump as it is over ${MAX_AGE} days old`); + continue; + } - let stats = await statAsync(path); + let retries = 0; - const thirtyDaysAgo = new Date().getTime() - MAX_AGE * 24 * 3_600 * 1_000; + while (retries <= 10) { + const twoSecondsAgo = new Date().getTime() - MIN_NOT_MODIFIED * 1_000; - if (stats.birthtimeMs < thirtyDaysAgo) { - logger.log(`Ignoring minidump as it is over ${MAX_AGE} days old`); - await cleanup(path); - continue; - } + if (stats.mtimeMs < twoSecondsAgo) { + const file = await readFileAsync(path); + const data = preProcessFile(file); - let retries = 0; + if (data.length < 10_000 || data.subarray(0, 4).toString() !== MINIDUMP_HEADER) { + logger.warn('Dropping minidump as it appears invalid.'); + break; + } - while (retries <= 10) { - const twoSecondsAgo = new Date().getTime() - MIN_NOT_MODIFIED * 1_000; + logger.log('Sending minidump'); - if (stats.mtimeMs < twoSecondsAgo) { - const file = await readFileAsync(path); - const data = preProcessFile(file); - await cleanup(path); + callback({ + attachmentType: 'event.minidump', + filename: basename(path), + data, + }); - if (data.length < 10_000 || data.subarray(0, 4).toString() !== MINIDUMP_HEADER) { - logger.warn('Dropping minidump as it appears invalid.'); break; } - logger.log('Sending minidump'); - - callback({ - attachmentType: 'event.minidump', - filename: basename(path), - data, - }); - - break; + logger.log(`Waiting. Minidump has been modified in the last ${MIN_NOT_MODIFIED} seconds.`); + retries += 1; + await delay(1_000); + stats = await statAsync(path); } - logger.log(`Minidump has been modified in the last ${MIN_NOT_MODIFIED} seconds. Checking again in a second.`); - retries += 1; - await delay(1_000); - stats = await statAsync(path); - } - - if (retries >= 10) { - logger.warn('Timed out waiting for minidump to stop being modified'); - await cleanup(path); + if (retries >= 10) { + logger.warn('Timed out waiting for minidump to stop being modified'); + } + } catch (e) { + logger.error('Failed to load minidump', e); + } finally { + try { + await unlinkAsync(path); + } catch (e) { + logger.warn('Could not delete', path); + } } - } catch (e) { - logger.error('Failed to load minidump', e); - await cleanup(path); } - } + }); }; } @@ -159,7 +146,8 @@ function crashpadMinidumpLoader(): MinidumpLoader { return createMinidumpLoader(async () => { await deleteCrashpadMetadataFile(crashesDirectory).catch((error) => logger.error(error)); - return readDirsAsync(dumpDirectories); + const files = await readDirsAsync(dumpDirectories); + return files.filter((file) => file.endsWith('.dmp')); }); } @@ -202,7 +190,7 @@ function breakpadMinidumpLoader(): MinidumpLoader { // the crashes directory. const files = await readDirAsync(crashesDirectory); removeBreakpadMetadata(crashesDirectory, files); - return files.map((file) => join(crashesDirectory, file)); + return files.filter((file) => file.endsWith('.dmp')).map((file) => join(crashesDirectory, file)); }, minidumpFromBreakpadMultipart); } From a5e88e7077f681a33a4b7a6b14997a6325ab9189 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 1 Oct 2023 14:07:44 +0200 Subject: [PATCH 06/11] Add extra session --- .../native-crash-renderer/session-next.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/e2e/test-apps/sessions/native-crash-renderer/session-next.json diff --git a/test/e2e/test-apps/sessions/native-crash-renderer/session-next.json b/test/e2e/test-apps/sessions/native-crash-renderer/session-next.json new file mode 100644 index 00000000..71a106c7 --- /dev/null +++ b/test/e2e/test-apps/sessions/native-crash-renderer/session-next.json @@ -0,0 +1,17 @@ +{ + "appId": "277345", + "sentryKey": "37f8a2ee37c0409d8970bc7559c7c7e4", + "method": "envelope", + "data": { + "sid": "{{id}}", + "init": false, + "started": 0, + "timestamp": 0, + "status": "ok", + "errors": 0, + "duration": 0, + "attrs": { + "release": "session-native-crash-renderer@1.0.0" + } + } +} From 6a45ab46b8f56b0325771e1aefcb04140beec0dc Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 1 Oct 2023 16:58:48 +0200 Subject: [PATCH 07/11] Add constants --- .../sentry-minidump/minidump-loader.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/integrations/sentry-minidump/minidump-loader.ts b/src/main/integrations/sentry-minidump/minidump-loader.ts index aea07703..10ec2e3c 100644 --- a/src/main/integrations/sentry-minidump/minidump-loader.ts +++ b/src/main/integrations/sentry-minidump/minidump-loader.ts @@ -8,8 +8,12 @@ import { readDirAsync, readFileAsync, statAsync, unlinkAsync } from '../../fs'; /** Maximum number of days to keep a minidump before deleting it. */ const MAX_AGE = 30; -/** Minimum number of seconds a minidump should not be modified for before we assume writing is complete */ -const MIN_NOT_MODIFIED = 2; +/** Minimum number of milliseconds a minidump should not be modified for before we assume writing is complete */ +const MIN_NOT_MODIFIED = 1_000; +const MAX_RETRY_TIME = 5_000; +const TIME_BETWEEN_RETRIES = 200; +const MAX_RETRIES = MAX_RETRY_TIME / TIME_BETWEEN_RETRIES; + const MINIDUMP_HEADER = 'MDMP'; function delay(ms: number): Promise { @@ -53,8 +57,8 @@ export function createMinidumpLoader( let retries = 0; - while (retries <= 10) { - const twoSecondsAgo = new Date().getTime() - MIN_NOT_MODIFIED * 1_000; + while (retries <= MAX_RETRIES) { + const twoSecondsAgo = new Date().getTime() - MIN_NOT_MODIFIED; if (stats.mtimeMs < twoSecondsAgo) { const file = await readFileAsync(path); @@ -76,13 +80,13 @@ export function createMinidumpLoader( break; } - logger.log(`Waiting. Minidump has been modified in the last ${MIN_NOT_MODIFIED} seconds.`); + logger.log(`Waiting. Minidump has been modified in the last ${MIN_NOT_MODIFIED} milliseconds.`); retries += 1; - await delay(1_000); + await delay(TIME_BETWEEN_RETRIES); stats = await statAsync(path); } - if (retries >= 10) { + if (retries >= MAX_RETRIES) { logger.warn('Timed out waiting for minidump to stop being modified'); } } catch (e) { From 0d0e1427ad55df5eed65c98bbdb5f17e1c965494 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 2 Oct 2023 00:33:39 +0200 Subject: [PATCH 08/11] Add unit tests --- test/unit/minidump-loader.test.ts | 126 ++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 test/unit/minidump-loader.test.ts diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts new file mode 100644 index 00000000..d1d85795 --- /dev/null +++ b/test/unit/minidump-loader.test.ts @@ -0,0 +1,126 @@ +import { uuid4 } from '@sentry/utils'; +import { expect } from 'chai'; +import { existsSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import * as tmp from 'tmp'; + +import { createMinidumpLoader } from '../../src/main/integrations/sentry-minidump/minidump-loader'; + +function dumpFileName(): string { + return `${uuid4()}.dmp`; +} + +const VALID_LOOKING_MINIDUMP = Buffer.from(`MDMP${'x'.repeat(12_000)}`); +const LOOKS_NOTHING_LIKE_A_MINIDUMP = Buffer.from('X'.repeat(12_000)); + +describe('createMinidumpLoader', () => { + let tempDir: tmp.DirResult; + beforeEach(() => { + tempDir = tmp.dirSync({ unsafeCleanup: true }); + }); + + afterEach(() => { + if (tempDir) { + tempDir.removeCallback(); + } + }); + + it('creates attachment from minidump', (done) => { + const name = dumpFileName(); + const dumpPath = join(tempDir.name, name); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + void loader(false, (attachment) => { + expect(attachment).to.eql({ + data: VALID_LOOKING_MINIDUMP, + filename: name, + attachmentType: 'event.minidump', + }); + + setTimeout(() => { + expect(existsSync(dumpPath)).to.be.false; + done(); + }, 1_000); + }); + }); + + it("doesn't send invalid minidumps", (done) => { + const dumpPath = join(tempDir.name, dumpFileName()); + writeFileSync(dumpPath, LOOKS_NOTHING_LIKE_A_MINIDUMP); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + let returnedAttachment = false; + void loader(false, () => { + returnedAttachment = true; + }); + + setTimeout(() => { + expect(returnedAttachment).to.be.false; + expect(existsSync(dumpPath)).to.be.false; + done(); + }, 2_000); + }); + + it('deletes minidumps when instructed', (done) => { + const dumpPath = join(tempDir.name, dumpFileName()); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + let returnedAttachment = false; + void loader(true, () => { + returnedAttachment = true; + }); + + setTimeout(() => { + expect(returnedAttachment).to.be.false; + expect(existsSync(dumpPath)).to.be.false; + done(); + }, 2_000); + }); + + it('waits for minidump to stop being modified', (done) => { + const dumpPath = join(tempDir.name, dumpFileName()); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + + let count = 0; + // Write the file every 500ms + const timer = setInterval(() => { + count += 500; + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + }, 500); + + // Stop writing after 3 seconds + setTimeout(() => { + clearInterval(timer); + }, 3_200); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + void loader(false, (_) => { + expect(count).to.be.greaterThanOrEqual(3_000); + done(); + }); + }); + + it('sending continues after loading failures', (done) => { + const missingPath = join(tempDir.name, dumpFileName()); + const name = dumpFileName(); + const dumpPath = join(tempDir.name, name); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + + const loader = createMinidumpLoader(() => Promise.resolve([missingPath, dumpPath])); + + void loader(false, (attachment) => { + expect(attachment.filename).to.eql(name); + + setTimeout(() => { + expect(existsSync(dumpPath)).to.be.false; + done(); + }, 1_000); + }); + }); +}); From 77b55ec5bec4e0eb80d3ce4f3952ba9a58e3f8dd Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 2 Oct 2023 00:48:07 +0200 Subject: [PATCH 09/11] also ensure old minidumps are not sent! --- test/unit/minidump-loader.test.ts | 47 +++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts index d1d85795..d7419559 100644 --- a/test/unit/minidump-loader.test.ts +++ b/test/unit/minidump-loader.test.ts @@ -1,6 +1,6 @@ import { uuid4 } from '@sentry/utils'; import { expect } from 'chai'; -import { existsSync, writeFileSync } from 'fs'; +import { existsSync, utimesSync, writeFileSync } from 'fs'; import { join } from 'path'; import * as tmp from 'tmp'; @@ -12,14 +12,15 @@ function dumpFileName(): string { const VALID_LOOKING_MINIDUMP = Buffer.from(`MDMP${'x'.repeat(12_000)}`); const LOOKS_NOTHING_LIKE_A_MINIDUMP = Buffer.from('X'.repeat(12_000)); +const MINIDUMP_HEADER_BUT_TOO_SMALL = Buffer.from('MDMPdflahfalfhalkfnaklsfnalfkn'); describe('createMinidumpLoader', () => { let tempDir: tmp.DirResult; - beforeEach(() => { + before(() => { tempDir = tmp.dirSync({ unsafeCleanup: true }); }); - afterEach(() => { + after(() => { if (tempDir) { tempDir.removeCallback(); } @@ -47,36 +48,60 @@ describe('createMinidumpLoader', () => { }); it("doesn't send invalid minidumps", (done) => { + const missingHeaderDump = join(tempDir.name, dumpFileName()); + writeFileSync(missingHeaderDump, LOOKS_NOTHING_LIKE_A_MINIDUMP); + const tooSmallDump = join(tempDir.name, dumpFileName()); + writeFileSync(tooSmallDump, MINIDUMP_HEADER_BUT_TOO_SMALL); + + const loader = createMinidumpLoader(() => Promise.resolve([missingHeaderDump, tooSmallDump])); + + let passedAttachment = false; + void loader(false, () => { + passedAttachment = true; + }); + + setTimeout(() => { + expect(passedAttachment).to.be.false; + expect(existsSync(missingHeaderDump)).to.be.false; + expect(existsSync(tooSmallDump)).to.be.false; + done(); + }, 2_000); + }); + + it("doesn't send minidumps that are over 30 days old", (done) => { const dumpPath = join(tempDir.name, dumpFileName()); - writeFileSync(dumpPath, LOOKS_NOTHING_LIKE_A_MINIDUMP); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + const now = new Date().getTime() / 1000; + const thirtyOneDaysAgo = now - 31 * 24 * 3_600; + utimesSync(dumpPath, thirtyOneDaysAgo, thirtyOneDaysAgo); const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); - let returnedAttachment = false; + let passedAttachment = false; void loader(false, () => { - returnedAttachment = true; + passedAttachment = true; }); setTimeout(() => { - expect(returnedAttachment).to.be.false; + expect(passedAttachment).to.be.false; expect(existsSync(dumpPath)).to.be.false; done(); }, 2_000); }); - it('deletes minidumps when instructed', (done) => { + it('deletes minidumps when sdk is disabled', (done) => { const dumpPath = join(tempDir.name, dumpFileName()); writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); - let returnedAttachment = false; + let passedAttachment = false; void loader(true, () => { - returnedAttachment = true; + passedAttachment = true; }); setTimeout(() => { - expect(returnedAttachment).to.be.false; + expect(passedAttachment).to.be.false; expect(existsSync(dumpPath)).to.be.false; done(); }, 2_000); From 380f2cc2ad52375ef35f2eb84135677ff65b7db5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 2 Oct 2023 00:50:55 +0200 Subject: [PATCH 10/11] Fix tests --- .github/workflows/build.yml | 1 + package.json | 2 +- .../sentry-minidump/minidump-loader.ts | 29 ++++++++++++------- test/unit/minidump-loader.test.ts | 19 ++++++++---- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f870740c..aaf6df2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,7 @@ on: env: ELECTRON_CACHE_DIR: ${{ github.workspace }} FAILURE_LOG: true + CI: true jobs: build: diff --git a/package.json b/package.json index e7a36d0b..b3872b4e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", "fix:eslint": "eslint . --format stylish --fix", "pretest": "yarn build", - "test": "cross-env TS_NODE_PROJECT=tsconfig.json xvfb-maybe electron-mocha --require ts-node/register/transpile-only --timeout 120000 ./test/unit/**/*.ts", + "test": "cross-env TS_NODE_PROJECT=tsconfig.json xvfb-maybe electron-mocha --require ts-node/register/transpile-only --timeout 12000 ./test/unit/**/*.ts", "pree2e": "rimraf test/e2e/dist/**/node_modules/@sentry/** test/e2e/dist/**/yarn.lock test/e2e/dist/**/package-lock.json && node scripts/clean-cache.js && yarn build && npm pack", "e2e": "cross-env TS_NODE_PROJECT=tsconfig.json xvfb-maybe mocha --require ts-node/register/transpile-only --retries 3 ./test/e2e/*.ts" }, diff --git a/src/main/integrations/sentry-minidump/minidump-loader.ts b/src/main/integrations/sentry-minidump/minidump-loader.ts index 10ec2e3c..32a47c47 100644 --- a/src/main/integrations/sentry-minidump/minidump-loader.ts +++ b/src/main/integrations/sentry-minidump/minidump-loader.ts @@ -7,12 +7,12 @@ import { getCrashesDirectory, usesCrashpad } from '../../electron-normalize'; import { readDirAsync, readFileAsync, statAsync, unlinkAsync } from '../../fs'; /** Maximum number of days to keep a minidump before deleting it. */ -const MAX_AGE = 30; +const MAX_AGE_DAYS = 30; /** Minimum number of milliseconds a minidump should not be modified for before we assume writing is complete */ -const MIN_NOT_MODIFIED = 1_000; -const MAX_RETRY_TIME = 5_000; -const TIME_BETWEEN_RETRIES = 200; -const MAX_RETRIES = MAX_RETRY_TIME / TIME_BETWEEN_RETRIES; +const NOT_MODIFIED_MS = 1_000; +const MAX_RETRY_MS = 5_000; +const RETRY_DELAY_MS = 500; +const MAX_RETRIES = MAX_RETRY_MS / RETRY_DELAY_MS; const MINIDUMP_HEADER = 'MDMP'; @@ -20,6 +20,11 @@ function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +/** + * A function that loads minidumps + * @param deleteAll Whether to just delete all minidumps + * @param callback A callback to call with the attachment ready to send + */ export type MinidumpLoader = (deleteAll: boolean, callback: (attachment: Attachment) => void) => Promise; /** @@ -48,17 +53,17 @@ export function createMinidumpLoader( let stats = await statAsync(path); - const thirtyDaysAgo = new Date().getTime() - MAX_AGE * 24 * 3_600 * 1_000; + const thirtyDaysAgo = new Date().getTime() - MAX_AGE_DAYS * 24 * 3_600 * 1_000; if (stats.birthtimeMs < thirtyDaysAgo) { - logger.log(`Ignoring minidump as it is over ${MAX_AGE} days old`); + logger.log(`Ignoring minidump as it is over ${MAX_AGE_DAYS} days old`); continue; } let retries = 0; while (retries <= MAX_RETRIES) { - const twoSecondsAgo = new Date().getTime() - MIN_NOT_MODIFIED; + const twoSecondsAgo = new Date().getTime() - NOT_MODIFIED_MS; if (stats.mtimeMs < twoSecondsAgo) { const file = await readFileAsync(path); @@ -80,9 +85,10 @@ export function createMinidumpLoader( break; } - logger.log(`Waiting. Minidump has been modified in the last ${MIN_NOT_MODIFIED} milliseconds.`); + logger.log(`Waiting. Minidump has been modified in the last ${NOT_MODIFIED_MS} milliseconds.`); retries += 1; - await delay(TIME_BETWEEN_RETRIES); + await delay(RETRY_DELAY_MS); + // update the stats stats = await statAsync(path); } @@ -92,10 +98,11 @@ export function createMinidumpLoader( } catch (e) { logger.error('Failed to load minidump', e); } finally { + // We always attempt to delete the minidump try { await unlinkAsync(path); } catch (e) { - logger.warn('Could not delete', path); + logger.warn('Could not delete minidump', path); } } } diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts index d7419559..3378befd 100644 --- a/test/unit/minidump-loader.test.ts +++ b/test/unit/minidump-loader.test.ts @@ -1,6 +1,6 @@ import { uuid4 } from '@sentry/utils'; import { expect } from 'chai'; -import { existsSync, utimesSync, writeFileSync } from 'fs'; +import { closeSync, existsSync, openSync, utimesSync, writeFileSync, writeSync } from 'fs'; import { join } from 'path'; import * as tmp from 'tmp'; @@ -10,7 +10,7 @@ function dumpFileName(): string { return `${uuid4()}.dmp`; } -const VALID_LOOKING_MINIDUMP = Buffer.from(`MDMP${'x'.repeat(12_000)}`); +const VALID_LOOKING_MINIDUMP = Buffer.from(`MDMP${'X'.repeat(12_000)}`); const LOOKS_NOTHING_LIKE_A_MINIDUMP = Buffer.from('X'.repeat(12_000)); const MINIDUMP_HEADER_BUT_TOO_SMALL = Buffer.from('MDMPdflahfalfhalkfnaklsfnalfkn'); @@ -69,6 +69,12 @@ describe('createMinidumpLoader', () => { }); it("doesn't send minidumps that are over 30 days old", (done) => { + // Updating the file times does not appear to work in GitHub Actions on Windows and Linux + if (process.env.CI) { + done(); + return; + } + const dumpPath = join(tempDir.name, dumpFileName()); writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); const now = new Date().getTime() / 1000; @@ -109,19 +115,20 @@ describe('createMinidumpLoader', () => { it('waits for minidump to stop being modified', (done) => { const dumpPath = join(tempDir.name, dumpFileName()); - writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + const file = openSync(dumpPath, 'w'); + writeSync(file, VALID_LOOKING_MINIDUMP); let count = 0; // Write the file every 500ms const timer = setInterval(() => { count += 500; - writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + writeSync(file, 'X'); }, 500); - // Stop writing after 3 seconds setTimeout(() => { clearInterval(timer); - }, 3_200); + closeSync(file); + }, 4_200); const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); From 5a1896cb13955ac75b46eb5cbb95588aff55950f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 2 Oct 2023 17:27:38 +0200 Subject: [PATCH 11/11] Fix test --- .github/workflows/build.yml | 1 - src/main/integrations/sentry-minidump/minidump-loader.ts | 5 +++-- test/unit/minidump-loader.test.ts | 9 +-------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aaf6df2d..f870740c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,6 @@ on: env: ELECTRON_CACHE_DIR: ${{ github.workspace }} FAILURE_LOG: true - CI: true jobs: build: diff --git a/src/main/integrations/sentry-minidump/minidump-loader.ts b/src/main/integrations/sentry-minidump/minidump-loader.ts index 32a47c47..e46b9d80 100644 --- a/src/main/integrations/sentry-minidump/minidump-loader.ts +++ b/src/main/integrations/sentry-minidump/minidump-loader.ts @@ -8,6 +8,7 @@ import { readDirAsync, readFileAsync, statAsync, unlinkAsync } from '../../fs'; /** Maximum number of days to keep a minidump before deleting it. */ const MAX_AGE_DAYS = 30; +const MS_PER_DAY = 24 * 3_600 * 1_000; /** Minimum number of milliseconds a minidump should not be modified for before we assume writing is complete */ const NOT_MODIFIED_MS = 1_000; const MAX_RETRY_MS = 5_000; @@ -53,9 +54,9 @@ export function createMinidumpLoader( let stats = await statAsync(path); - const thirtyDaysAgo = new Date().getTime() - MAX_AGE_DAYS * 24 * 3_600 * 1_000; + const thirtyDaysAgo = new Date().getTime() - MAX_AGE_DAYS * MS_PER_DAY; - if (stats.birthtimeMs < thirtyDaysAgo) { + if (stats.mtimeMs < thirtyDaysAgo) { logger.log(`Ignoring minidump as it is over ${MAX_AGE_DAYS} days old`); continue; } diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts index 3378befd..cac2f525 100644 --- a/test/unit/minidump-loader.test.ts +++ b/test/unit/minidump-loader.test.ts @@ -69,16 +69,9 @@ describe('createMinidumpLoader', () => { }); it("doesn't send minidumps that are over 30 days old", (done) => { - // Updating the file times does not appear to work in GitHub Actions on Windows and Linux - if (process.env.CI) { - done(); - return; - } - const dumpPath = join(tempDir.name, dumpFileName()); writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); - const now = new Date().getTime() / 1000; - const thirtyOneDaysAgo = now - 31 * 24 * 3_600; + const thirtyOneDaysAgo = new Date(new Date().getTime() - 31 * 24 * 3_600 * 1_000); utimesSync(dumpPath, thirtyOneDaysAgo, thirtyOneDaysAgo); const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));