import { __assign, __awaiter, __generator, __read, __spreadArray } from "tslib";
/* eslint-disable max-lines */
import { API } from '@sentry/core';
import { Status } from '@sentry/types';
import { forget, logger, timestampWithMs } from '@sentry/utils';
import { basename, join } from 'path';
import { supportsCrashpadOnWindows } from '../electron-version';
import { mkdirp, readDirAsync, readFileAsync, renameAsync, statAsync, unlinkAsync } from './fs';
import { Store } from './store';
/** Maximum number of days to keep a minidump before deleting it. */
var MAX_AGE = 30;
/** Maximum number of requests that we store/queue if something goes wrong. */
var MAX_REQUESTS_COUNT = 10;
/**
 * A service that discovers Minidump crash reports and uploads them to Sentry.
 */
var MinidumpUploader = /** @class */ (function () {
    /**
     * Creates a new uploader instance.
     *
     * @param dsn The Sentry DSN.
     * @param crashesDirectory The directory Electron stores crashes in.
     * @param cacheDirectory A persistent directory to cache minidumps.
     */
    function MinidumpUploader(dsn, _crashesDirectory, _cacheDirectory, _transport) {
        this._crashesDirectory = _crashesDirectory;
        this._cacheDirectory = _cacheDirectory;
        this._transport = _transport;
        var crashpadWindows = process.platform === 'win32' && supportsCrashpadOnWindows();
        this._type = process.platform === 'darwin' || crashpadWindows ? 'crashpad' : 'breakpad';
        this._crashpadSubDirectory = process.platform === 'darwin' ? 'completed' : 'reports';
        this._knownPaths = [];
        this._api = new API(dsn);
        this._queue = new Store(this._cacheDirectory, 'queue', []);
    }
    /**
     * Returns the minidump endpoint in Sentry
     * @param dsn Dsn
     */
    MinidumpUploader.minidumpUrlFromDsn = function (dsn) {
        var host = dsn.host, path = dsn.path, projectId = dsn.projectId, port = dsn.port, protocol = dsn.protocol, user = dsn.user;
        return protocol + "://" + host + (port !== '' ? ":" + port : '') + (path !== '' ? "/" + path : '') + "/api/" + projectId + "/minidump/?sentry_key=" + user;
    };
    /**
     * Uploads a minidump file to Sentry.
     *
     * @param path Absolute path to the minidump file.
     * @param event Event data to attach to the minidump.
     * @returns A promise that resolves when the upload is complete.
     */
    MinidumpUploader.prototype.uploadMinidump = function (request) {
        return __awaiter(this, void 0, void 0, function () {
            var transport, response, requestForTransport, e_1, err_1, error;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        if (typeof this._transport.sendRequest !== 'function') {
                            logger.warn("Your transport doesn't implement sendRequest");
                            logger.warn('Skipping sending minidump');
                            return [2 /*return*/];
                        }
                        logger.log('Sending minidump', request.path);
                        transport = this._transport;
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 10, , 13]);
                        response = void 0;
                        if (!!transport.isRateLimited('event')) return [3 /*break*/, 4];
                        return [4 /*yield*/, this._toMinidumpRequest(transport, request.event, request.path)];
                    case 2:
                        requestForTransport = _a.sent();
                        return [4 /*yield*/, transport.sendRequest(requestForTransport)];
                    case 3:
                        response = _a.sent();
                        _a.label = 4;
                    case 4:
                        _a.trys.push([4, 6, , 7]);
                        return [4 /*yield*/, unlinkAsync(request.path)];
                    case 5:
                        _a.sent();
                        return [3 /*break*/, 7];
                    case 6:
                        e_1 = _a.sent();
                        logger.warn('Could not delete', request.path);
                        return [3 /*break*/, 7];
                    case 7:
                        // Forget this minidump in all caches
                        // tslint:disable-next-line: strict-comparisons
                        this._queue.update(function (queued) { return queued.filter(function (stored) { return stored !== request; }); });
                        this._knownPaths.splice(this._knownPaths.indexOf(request.path), 1);
                        if (!(response && response.status === Status.Success)) return [3 /*break*/, 9];
                        return [4 /*yield*/, this.flushQueue()];
                    case 8:
                        _a.sent();
                        _a.label = 9;
                    case 9: return [3 /*break*/, 13];
                    case 10:
                        err_1 = _a.sent();
                        // TODO: Test this
                        logger.warn('Failed to upload minidump', err_1);
                        error = err_1 ? err_1 : { code: '' };
                        if (!(error.code === 'ENOTFOUND')) return [3 /*break*/, 12];
                        return [4 /*yield*/, this._queueMinidump(request)];
                    case 11:
                        _a.sent();
                        _a.label = 12;
                    case 12: return [3 /*break*/, 13];
                    case 13: return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Searches for new, unknown minidump files in the crash directory.
     * @returns A promise that resolves to absolute paths of those dumps.
     */
    MinidumpUploader.prototype.getNewMinidumps = function () {
        return __awaiter(this, void 0, void 0, function () {
            var minidumps, _a, oldestMs;
            var _this = this;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (!(this._type === 'crashpad')) return [3 /*break*/, 2];
                        return [4 /*yield*/, this._scanCrashpadFolder()];
                    case 1:
                        _a = _b.sent();
                        return [3 /*break*/, 4];
                    case 2: return [4 /*yield*/, this._scanBreakpadFolder()];
                    case 3:
                        _a = _b.sent();
                        _b.label = 4;
                    case 4:
                        minidumps = _a;
                        logger.log("Found " + minidumps.length + " minidumps");
                        oldestMs = new Date().getTime() - MAX_AGE * 24 * 3600 * 1000;
                        return [2 /*return*/, this._filterAsync(minidumps, function (path) { return __awaiter(_this, void 0, void 0, function () {
                                var stats, e_2;
                                return __generator(this, function (_a) {
                                    switch (_a.label) {
                                        case 0:
                                            // Skip files that we have seen before
                                            if (this._knownPaths.indexOf(path) >= 0) {
                                                return [2 /*return*/, false];
                                            }
                                            // Lock this minidump until we have uploaded it or an error occurs and we
                                            // remove it from the file system.
                                            this._knownPaths.push(path);
                                            return [4 /*yield*/, statAsync(path)];
                                        case 1:
                                            stats = _a.sent();
                                            if (!(stats.birthtimeMs < oldestMs)) return [3 /*break*/, 6];
                                            _a.label = 2;
                                        case 2:
                                            _a.trys.push([2, 4, , 5]);
                                            return [4 /*yield*/, unlinkAsync(path)];
                                        case 3:
                                            _a.sent();
                                            return [3 /*break*/, 5];
                                        case 4:
                                            e_2 = _a.sent();
                                            logger.warn('Could not delete', path);
                                            return [3 /*break*/, 5];
                                        case 5:
                                            this._knownPaths.splice(this._knownPaths.indexOf(path), 1);
                                            return [2 /*return*/, false];
                                        case 6: return [2 /*return*/, true];
                                    }
                                });
                            }); })];
                }
            });
        });
    };
    /** Flushes locally cached minidumps from the queue. */
    MinidumpUploader.prototype.flushQueue = function () {
        return __awaiter(this, void 0, void 0, function () {
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, Promise.all(this._queue.get().map(function (request) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
                            return [2 /*return*/, this.uploadMinidump(request)];
                        }); }); }))];
                    case 1:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Helper to filter an array with asynchronous callbacks.
     *
     * @param array An array containing items to filter.
     * @param predicate An async predicate evaluated on every item.
     * @param thisArg Optional value passed as "this" into the callback.
     * @returns An array containing only values where the callback returned true.
     */
    MinidumpUploader.prototype._filterAsync = function (array, predicate, thisArg) {
        return __awaiter(this, void 0, void 0, function () {
            var verdicts;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, Promise.all(array.map(predicate, thisArg))];
                    case 1:
                        verdicts = _a.sent();
                        return [2 /*return*/, array.filter(function (_, index) { return verdicts[index]; })];
                }
            });
        });
    };
    /** Scans the Crashpad directory structure for minidump files. */
    MinidumpUploader.prototype._scanCrashpadFolder = function () {
        return __awaiter(this, void 0, void 0, function () {
            var dumpDirectory, files;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        forget(this._deleteCrashpadMetadataFile());
                        dumpDirectory = join(this._crashesDirectory, this._crashpadSubDirectory);
                        return [4 /*yield*/, readDirAsync(dumpDirectory)];
                    case 1:
                        files = _a.sent();
                        return [2 /*return*/, files.filter(function (file) { return file.endsWith('.dmp'); }).map(function (file) { return join(dumpDirectory, file); })];
                }
            });
        });
    };
    /** Attempts to remove the metadata file so Crashpad doesn't output `failed to stat report` errors to the console */
    MinidumpUploader.prototype._deleteCrashpadMetadataFile = function (waitMs) {
        if (waitMs === void 0) { waitMs = 100; }
        return __awaiter(this, void 0, void 0, function () {
            var metadataPath, e_3;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (waitMs > 2000) {
                            return [2 /*return*/];
                        }
                        metadataPath = join(this._crashesDirectory, 'metadata');
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        return [4 /*yield*/, unlinkAsync(metadataPath)];
                    case 2:
                        _a.sent();
                        logger.log('Deleted Crashpad metadata file', metadataPath);
                        return [3 /*break*/, 4];
                    case 3:
                        e_3 = _a.sent();
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
                        if (e_3.code && e_3.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(function () { return __awaiter(_this, void 0, void 0, function () {
                                return __generator(this, function (_a) {
                                    switch (_a.label) {
                                        case 0: return [4 /*yield*/, this._deleteCrashpadMetadataFile(waitMs * 2)];
                                        case 1:
                                            _a.sent();
                                            return [2 /*return*/];
                                    }
                                });
                            }); }, waitMs);
                        }
                        return [3 /*break*/, 4];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    /** Scans the Breakpad directory structure for minidump files. */
    MinidumpUploader.prototype._scanBreakpadFolder = function () {
        return __awaiter(this, void 0, void 0, function () {
            var files;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, readDirAsync(this._crashesDirectory)];
                    case 1:
                        files = _a.sent();
                        // Remove all metadata files and forget about them.
                        // eslint-disable-next-line @typescript-eslint/no-floating-promises
                        Promise.all(files
                            .filter(function (file) { return file.endsWith('.txt') && !file.endsWith('log.txt'); })
                            .map(function (file) { return __awaiter(_this, void 0, void 0, function () {
                            var path, e_4;
                            return __generator(this, function (_a) {
                                switch (_a.label) {
                                    case 0:
                                        path = join(this._crashesDirectory, file);
                                        _a.label = 1;
                                    case 1:
                                        _a.trys.push([1, 3, , 4]);
                                        return [4 /*yield*/, unlinkAsync(path)];
                                    case 2:
                                        _a.sent();
                                        return [3 /*break*/, 4];
                                    case 3:
                                        e_4 = _a.sent();
                                        logger.warn('Could not delete', path);
                                        return [3 /*break*/, 4];
                                    case 4: return [2 /*return*/];
                                }
                            });
                        }); }));
                        return [2 /*return*/, files.filter(function (file) { return file.endsWith('.dmp'); }).map(function (file) { return join(_this._crashesDirectory, file); })];
                }
            });
        });
    };
    /** Crudely parses the dump file from the Breakpad multipart file */
    MinidumpUploader.prototype._parseBreakpadMultipartFile = function (file) {
        return __awaiter(this, void 0, void 0, function () {
            var binaryStart, dumpStart, dumpEnd;
            return __generator(this, function (_a) {
                binaryStart = file.lastIndexOf('Content-Type: application/octet-stream');
                if (binaryStart > 0) {
                    dumpStart = file.indexOf('MDMP', binaryStart);
                    dumpEnd = file.lastIndexOf('----------------------------');
                    if (dumpStart > 0 && dumpEnd > 0 && dumpEnd > dumpStart) {
                        return [2 /*return*/, file.slice(dumpStart, dumpEnd)];
                    }
                }
                return [2 /*return*/, undefined];
            });
        });
    };
    /**
     * Enqueues a minidump with event information for later upload.
     * @param request The request containing a minidump and event info.
     */
    MinidumpUploader.prototype._queueMinidump = function (request) {
        return __awaiter(this, void 0, void 0, function () {
            var filename, queuePath, requests, stale;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        filename = basename(request.path);
                        // Only enqueue if this minidump hasn't been enqueued before. Compare the
                        // filename instead of the full path, because we will move the file to a
                        // temporary location later on.
                        if (this._queue.get().some(function (req) { return basename(req.path) === filename; })) {
                            return [2 /*return*/];
                        }
                        queuePath = join(this._cacheDirectory, filename);
                        return [4 /*yield*/, mkdirp(this._cacheDirectory)];
                    case 1:
                        _a.sent();
                        return [4 /*yield*/, renameAsync(request.path, queuePath)];
                    case 2:
                        _a.sent();
                        requests = __spreadArray(__spreadArray([], __read(this._queue.get())), [__assign(__assign({}, request), { path: queuePath })]);
                        stale = requests.splice(-MAX_REQUESTS_COUNT);
                        this._queue.set(requests);
                        return [4 /*yield*/, Promise.all(stale.map(function (req) { return __awaiter(_this, void 0, void 0, function () {
                                var e_5;
                                return __generator(this, function (_a) {
                                    switch (_a.label) {
                                        case 0:
                                            _a.trys.push([0, 2, , 3]);
                                            return [4 /*yield*/, unlinkAsync(req.path)];
                                        case 1:
                                            _a.sent();
                                            return [3 /*break*/, 3];
                                        case 2:
                                            e_5 = _a.sent();
                                            logger.warn('Could not delete', req.path);
                                            return [3 /*break*/, 3];
                                        case 3: return [2 /*return*/];
                                    }
                                });
                            }); }))];
                    case 3:
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Create minidump request to dispatch to the transpoirt
     */
    MinidumpUploader.prototype._toMinidumpRequest = function (transport, event, minidumpPath) {
        return __awaiter(this, void 0, void 0, function () {
            var envelopeHeaders, itemHeaders, eventPayload, bodyBuffer, minidumpContent, dump, minidumpHeader;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        envelopeHeaders = JSON.stringify({
                            event_id: event.event_id,
                            // Internal helper that uses `perf_hooks` to get clock reading
                            sent_at: new Date(timestampWithMs() * 1000).toISOString(),
                        });
                        // If attachments are ratelimited we add this hint so users know
                        if (transport.isRateLimited('attachment')) {
                            event.message = 'Ratelimited - Minidump Event';
                        }
                        itemHeaders = JSON.stringify({
                            content_type: 'application/json',
                            type: 'event',
                        });
                        eventPayload = JSON.stringify(event);
                        bodyBuffer = Buffer.from(envelopeHeaders + "\n" + itemHeaders + "\n" + eventPayload + "\n");
                        if (!!transport.isRateLimited('attachment')) return [3 /*break*/, 4];
                        return [4 /*yield*/, readFileAsync(minidumpPath)];
                    case 1:
                        minidumpContent = (_a.sent());
                        if (!(this._type !== 'crashpad')) return [3 /*break*/, 3];
                        return [4 /*yield*/, this._parseBreakpadMultipartFile(minidumpContent)];
                    case 2:
                        dump = _a.sent();
                        if (dump) {
                            minidumpContent = dump;
                        }
                        _a.label = 3;
                    case 3:
                        minidumpHeader = JSON.stringify({
                            attachment_type: 'event.minidump',
                            length: minidumpContent.length,
                            type: 'attachment',
                            filename: basename(minidumpPath),
                        });
                        bodyBuffer = Buffer.concat([bodyBuffer, Buffer.from(minidumpHeader + "\n"), minidumpContent, Buffer.from('\n')]);
                        return [3 /*break*/, 5];
                    case 4:
                        logger.warn('Will not add minidump to request since they are rate limited.');
                        _a.label = 5;
                    case 5: return [2 /*return*/, {
                            body: bodyBuffer,
                            url: this._api.getEnvelopeEndpointWithUrlEncodedAuth(),
                            type: 'event',
                        }];
                }
            });
        });
    };
    return MinidumpUploader;
}());
export { MinidumpUploader };
//# sourceMappingURL=uploader.js.map