const tslib = require('tslib');
const utils = require('@sentry/utils');
const path = require('path');
const mutex = require('../../../common/mutex.js');
const electronNormalize = require('../../electron-normalize.js');
const fs = require('../../fs.js');

/** Maximum number of days to keep a minidump before deleting it. */
const MAX_AGE_DAYS = 30;
const MS_PER_DAY = 24 * 3600 * 1000;
/** Minimum number of milliseconds a minidump should not be modified for before we assume writing is complete */
const NOT_MODIFIED_MS = 1000;
const MAX_RETRY_MS = 5000;
const RETRY_DELAY_MS = 500;
const MAX_RETRIES = MAX_RETRY_MS / RETRY_DELAY_MS;
const MINIDUMP_HEADER = 'MDMP';
function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * 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
 */
function createMinidumpLoader(getMinidumpPaths, preProcessFile = (file) => file) {
    // The mutex protects against a whole host of reentrancy issues and race conditions.
    const mutex$1 = new mutex.Mutex();
    return (deleteAll, callback) => tslib.__awaiter(this, void 0, void 0, function* () {
        // any calls to this function will be queued and run exclusively
        yield mutex$1.runExclusive(() => tslib.__awaiter(this, void 0, void 0, function* () {
            for (const path of yield getMinidumpPaths()) {
                try {
                    if (deleteAll) {
                        continue;
                    }
                    utils.logger.log('Found minidump', path);
                    let stats = yield fs.statAsync(path);
                    const thirtyDaysAgo = new Date().getTime() - MAX_AGE_DAYS * MS_PER_DAY;
                    if (stats.mtimeMs < thirtyDaysAgo) {
                        utils.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() - NOT_MODIFIED_MS;
                        if (stats.mtimeMs < twoSecondsAgo) {
                            const file = yield fs.readFileAsync(path);
                            const data = preProcessFile(file);
                            if (data.length < 10000 || data.subarray(0, 4).toString() !== MINIDUMP_HEADER) {
                                utils.logger.warn('Dropping minidump as it appears invalid.');
                                break;
                            }
                            utils.logger.log('Sending minidump');
                            callback({
                                attachmentType: 'event.minidump',
                                filename: utils.basename(path),
                                data,
                            });
                            break;
                        }
                        utils.logger.log(`Waiting. Minidump has been modified in the last ${NOT_MODIFIED_MS} milliseconds.`);
                        retries += 1;
                        yield delay(RETRY_DELAY_MS);
                        // update the stats
                        stats = yield fs.statAsync(path);
                    }
                    if (retries >= MAX_RETRIES) {
                        utils.logger.warn('Timed out waiting for minidump to stop being modified');
                    }
                }
                catch (e) {
                    utils.logger.error('Failed to load minidump', e);
                }
                finally {
                    // We always attempt to delete the minidump
                    try {
                        yield fs.unlinkAsync(path);
                    }
                    catch (e) {
                        utils.logger.warn('Could not delete minidump', path);
                    }
                }
            }
        }));
    });
}
/** Attempts to remove the metadata file so Crashpad doesn't output `failed to stat report` errors to the console */
function deleteCrashpadMetadataFile(crashesDirectory, waitMs = 100) {
    return tslib.__awaiter(this, void 0, void 0, function* () {
        if (waitMs > 2000) {
            return;
        }
        const metadataPath = path.join(crashesDirectory, 'metadata');
        try {
            yield fs.unlinkAsync(metadataPath);
            utils.logger.log('Deleted Crashpad metadata file', metadataPath);
        }
        catch (e) {
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
            if (e.code && e.code == 'EBUSY') {
                // Since Crashpad probably still has the metadata file open, we make a few attempts to delete it, backing
                // off and waiting longer each time.
                setTimeout(() => tslib.__awaiter(this, void 0, void 0, function* () {
                    yield deleteCrashpadMetadataFile(crashesDirectory, waitMs * 2);
                }), waitMs);
            }
        }
    });
}
function readDirsAsync(paths) {
    return tslib.__awaiter(this, void 0, void 0, function* () {
        const found = [];
        for (const path$1 of paths) {
            try {
                const files = yield fs.readDirAsync(path$1);
                found.push(...files.map((file) => path.join(path$1, file)));
            }
            catch (_) {
                //
            }
        }
        return found;
    });
}
function crashpadMinidumpLoader() {
    const crashesDirectory = electronNormalize.getCrashesDirectory();
    const crashpadSubDirectory = process.platform === 'win32' ? 'reports' : 'completed';
    const dumpDirectories = [path.join(crashesDirectory, crashpadSubDirectory)];
    if (process.platform === 'darwin') {
        dumpDirectories.push(path.join(crashesDirectory, 'pending'));
    }
    return createMinidumpLoader(() => tslib.__awaiter(this, void 0, void 0, function* () {
        yield deleteCrashpadMetadataFile(crashesDirectory).catch((error) => utils.logger.error(error));
        const files = yield readDirsAsync(dumpDirectories);
        return files.filter((file) => file.endsWith('.dmp'));
    }));
}
/** Crudely parses the minidump from the Breakpad multipart file */
function minidumpFromBreakpadMultipart(file) {
    const binaryStart = file.lastIndexOf('Content-Type: application/octet-stream');
    if (binaryStart > 0) {
        const dumpStart = file.indexOf(MINIDUMP_HEADER, binaryStart);
        const dumpEnd = file.lastIndexOf('----------------------------');
        if (dumpStart > 0 && dumpEnd > 0 && dumpEnd > dumpStart) {
            return file.subarray(dumpStart, dumpEnd);
        }
    }
    return file;
}
function removeBreakpadMetadata(crashesDirectory, paths) {
    // Remove all metadata files and forget about them.
    Promise.all(paths
        .filter((file) => file.endsWith('.txt') && !file.endsWith('log.txt'))
        .map((file) => tslib.__awaiter(this, void 0, void 0, function* () {
        const path$1 = path.join(crashesDirectory, file);
        try {
            yield fs.unlinkAsync(path$1);
        }
        catch (e) {
            utils.logger.warn('Could not delete', path$1);
        }
    }))).catch(() => {
        // ignore since we catch each unlink individually
    });
}
function breakpadMinidumpLoader() {
    const crashesDirectory = electronNormalize.getCrashesDirectory();
    return createMinidumpLoader(() => tslib.__awaiter(this, void 0, void 0, function* () {
        // Breakpad stores all minidump files along with a metadata file directly in
        // the crashes directory.
        const files = yield fs.readDirAsync(crashesDirectory);
        removeBreakpadMetadata(crashesDirectory, files);
        return files.filter((file) => file.endsWith('.dmp')).map((file) => path.join(crashesDirectory, file));
    }), minidumpFromBreakpadMultipart);
}
/**
 * Gets the minidump loader
 */
function getMinidumpLoader() {
    return electronNormalize.usesCrashpad() ? crashpadMinidumpLoader() : breakpadMinidumpLoader();
}

exports.createMinidumpLoader = createMinidumpLoader;
exports.getMinidumpLoader = getMinidumpLoader;
//# sourceMappingURL=minidump-loader.js.map
