"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ManagerAdvanced = exports.ManagerBase = void 0;
exports.errorWithCode = errorWithCode;
exports.generateDbName = generateDbName;
exports.getHost = getHost;
exports.uriTemplate = uriTemplate;
exports.isNullOrUndefined = isNullOrUndefined;
exports.assertion = assertion;
exports.killProcess = killProcess;
exports.isAlive = isAlive;
exports.ensureAsync = ensureAsync;
exports.authDefault = authDefault;
exports.statPath = statPath;
exports.pathExists = pathExists;
exports.checkBinaryPermissions = checkBinaryPermissions;
exports.mkdir = mkdir;
exports.createTmpDir = createTmpDir;
exports.removeDir = removeDir;
exports.uuidv4 = uuidv4;
exports.md5 = md5;
exports.md5FromFile = md5FromFile;
exports.lockfilePath = lockfilePath;
exports.getStorageEngine = getStorageEngine;
const tslib_1 = require("tslib");
const debug_1 = tslib_1.__importDefault(require("debug"));
const fs_1 = require("fs");
const errors_1 = require("./errors");
const os_1 = require("os");
const path = tslib_1.__importStar(require("path"));
const crypto_1 = require("crypto");
const semver = tslib_1.__importStar(require("semver"));
const log = (0, debug_1.default)('MongoMS:utils');
/**
 * This is here, because NodeJS does not have a FSError type
 * @param err Value to check agains
 * @returns `true` if it is a error with code, `false` if not
 */
function errorWithCode(err) {
    return err instanceof Error && 'code' in err;
}
/**
 * Return input or default database
 * @param {string} dbName
 */
function generateDbName(dbName) {
    // this is ""(empty) to make it compatible with mongodb's uri format and mongoose's uri format
    // (in mongodb its the auth database, in mongoose its the default database for models)
    return dbName || '';
}
/**
 * Extracts the host and port information from a mongodb URI string.
 * @param {string} uri mongodb URI
 */
function getHost(uri) {
    // this will turn "mongodb://user:pass@localhost:port/authdb?queryoptions=1" to "localhost:port"
    return uri.replace(/(?:^mongodb:\/{2})|(?:\/.*$)|(?:.*@)/gim, '');
}
/**
 * Basic MongoDB Connection string
 * @param host the host ip or an list of hosts
 * @param port the host port or undefined if "host" is an list of hosts
 * @param dbName the database to add to the uri (in mongodb its the auth database, in mongoose its the default database for models)
 * @param query extra uri-query options (joined with "&")
 */
function uriTemplate(host, port, dbName, query) {
    const hosts = !isNullOrUndefined(port) ? `${host}:${port}` : host;
    return `mongodb://${hosts}/${dbName}` + (!isNullOrUndefined(query) ? `?${query.join('&')}` : '');
}
/**
 * Because since node 4.0.0 the internal util.is* functions got deprecated
 * @param val Any value to test if null or undefined
 */
function isNullOrUndefined(val) {
    return val === null || val === undefined;
}
/**
 * Assert an condition, if "false" throw error
 * Note: it is not named "assert" to differentiate between node and jest types
 * @param cond The Condition to throw
 * @param error An Custom Error to throw
 */
function assertion(cond, error) {
    if (!cond) {
        throw error ?? new errors_1.AssertionFallbackError();
    }
}
/**
 * Kill an ChildProcess
 * @param childprocess The Process to kill
 * @param name the name used in the logs
 * @param mongodPort the port for the mongod process (for easier logging)
 */
async function killProcess(childprocess, name, mongodPort) {
    function ilog(msg) {
        log(`Mongo[${mongodPort || 'unknown'}] killProcess: ${msg}`);
    }
    // this case can somehow happen, see https://github.com/typegoose/mongodb-memory-server/issues/666
    if (isNullOrUndefined(childprocess)) {
        ilog('childprocess was somehow undefined');
        return;
    }
    // check if the childProcess (via PID) is still alive (found thanks to https://github.com/typegoose/mongodb-memory-server/issues/411)
    if (!isAlive(childprocess.pid)) {
        ilog("given childProcess's PID was not alive anymore");
        return;
    }
    /**
     * Timeout before using SIGKILL
     */
    const timeoutTime = 1000 * 10;
    await new Promise((res, rej) => {
        let timeout = setTimeout(() => {
            ilog('timeout triggered, trying SIGKILL');
            if (!debug_1.default.enabled('MongoMS:utils')) {
                console.warn('An Process didnt exit with signal "SIGINT" within 10 seconds, using "SIGKILL"!\n' +
                    'Enable debug logs for more information');
            }
            childprocess.kill('SIGKILL');
            timeout = setTimeout(() => {
                ilog('timeout triggered again, rejecting');
                rej(new Error(`Process "${name}" didnt exit, enable debug for more information.`));
            }, timeoutTime);
        }, timeoutTime);
        childprocess.once(`exit`, (code, signal) => {
            ilog(`${name}: got exit signal, Code: ${code}, Signal: ${signal}`);
            clearTimeout(timeout);
            res();
        });
        ilog(`${name}: sending "SIGINT"`);
        childprocess.kill('SIGINT');
    });
}
/**
 * Check if the given Process is still alive
 * @param {number} pid The Process PID
 */
function isAlive(pid) {
    // This test (and allow to be undefined) is here because somewhere between nodejs 12 and 16 the types for "childprocess.pid" changed to include "| undefined"
    if (isNullOrUndefined(pid)) {
        return false;
    }
    try {
        process.kill(pid, 0); // code "0" dosnt actually kill anything (on all supported systems)
        return true;
    }
    catch (err) {
        return false;
    }
}
/**
 * Call "process.nextTick" to ensure an function is exectued directly after all code surrounding it
 * look at the following link to get to know on why this needed: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#process-nexttick (read full documentation)
 */
async function ensureAsync() {
    return new Promise((res) => process.nextTick(res));
}
/**
 * Convert Partitial input into full-defaulted output
 * @param opts Partitial input options
 */
function authDefault(opts) {
    return {
        force: false,
        enable: true,
        customRootName: 'mongodb-memory-server-root',
        customRootPwd: 'rootuser',
        extraUsers: [],
        keyfileContent: '0123456789',
        ...opts,
    };
}
/**
 * Run "fs.promises.stat", but return "undefined" if error is "ENOENT" or "EACCES"
 * follows symlinks
 * @param path The Path to Stat
 * @throws if the error is not "ENOENT" or "EACCES"
 */
async function statPath(path) {
    return fs_1.promises.stat(path).catch((err) => {
        // catch the error if the directory doesn't exist or permission is denied, without throwing an error
        if (['ENOENT', 'EACCES'].includes(err.code)) {
            return undefined;
        }
        throw err;
    });
}
/**
 * Like "fs.existsSync" but async
 * uses "utils.statPath"
 * follows symlinks
 * @param path The Path to check for
 */
async function pathExists(path) {
    return !isNullOrUndefined(await statPath(path));
}
/**
 * This Class is used to have unified types for base-manager functions
 */
class ManagerBase {
}
exports.ManagerBase = ManagerBase;
/**
 * This Class is used to have unified types for advanced-manager functions
 */
class ManagerAdvanced extends ManagerBase {
}
exports.ManagerAdvanced = ManagerAdvanced;
/**
 * Check that the Binary has sufficient Permissions to be executed
 * @param path The Path to check
 */
async function checkBinaryPermissions(path) {
    try {
        await fs_1.promises.access(path, fs_1.constants.X_OK); // check if the provided path exists and has the execute bit for current user
    }
    catch (err) {
        if (errorWithCode(err)) {
            if (err.code === 'EACCES') {
                throw new errors_1.InsufficientPermissionsError(path);
            }
            if (err.code === 'ENOENT') {
                throw new errors_1.BinaryNotFoundError(path);
            }
        }
        throw err;
    }
}
/**
 * Make Directory, wrapper for native mkdir with recursive true
 * @param path The Path to create
 * @returns Nothing
 */
async function mkdir(path) {
    await fs_1.promises.mkdir(path, { recursive: true });
}
/**
 * Create a Temporary directory with prefix, and optionally at "atPath"
 * @param prefix The prefix to use to create the tmpdir
 * @param atPath Optionally set a custom path other than "os.tmpdir"
 * @returns The created Path
 */
async function createTmpDir(prefix, atPath) {
    const tmpPath = atPath ?? (0, os_1.tmpdir)();
    return fs_1.promises.mkdtemp(path.join(tmpPath, prefix));
}
/**
 * Removes the given "path", if it is a directory, and does not throw a error if not existing
 * @param dirPath The Directory Path to delete
 * @returns "true" if deleted, otherwise "false"
 */
async function removeDir(dirPath) {
    const stat = await statPath(dirPath);
    if (isNullOrUndefined(stat)) {
        return;
    }
    if (!stat.isDirectory()) {
        throw new Error(`Given Path is not a directory! (Path: "${dirPath}")`);
    }
    await fs_1.promises.rm(dirPath, { force: true, recursive: true });
}
/**
 * Helper function to have uuidv4 generation and definition in one place
 * @returns a uuid-v4
 */
function uuidv4() {
    return (0, crypto_1.randomUUID)();
}
/**
 * Helper function to have md5 generation and definition in one place
 * @param content the content to checksum
 * @returns a md5 of the input
 */
function md5(content) {
    return (0, crypto_1.createHash)('md5').update(content).digest('hex');
}
/**
 * Helper function to have md5 generation and definition in one place for a file
 * @param file the location of a file to read for a hash
 * @returns a md5 of the input file
 */
async function md5FromFile(file) {
    return md5(await fs_1.promises.readFile(file));
}
/**
 * Helper function to get the lockfile for the provided `version` in `downloadDir`
 * @param downloadDir The Download directory of the binary
 * @param version The version to be downlaoded
 * @returns The lockfile path
 */
function lockfilePath(downloadDir, version) {
    return path.resolve(downloadDir, `${version}.lock`);
}
/**
 * Get the storage engine for the given given binary version, and issue a warning if it needs to be changed
 * @param storageEngine The engine that is configured
 * @param coercedVersion The binary version as semver
 * @returns The engine that actually will run in the given binary version
 */
function getStorageEngine(storageEngine, coercedVersion) {
    // warn when storage engine "ephemeralForTest" is explicitly used and switch to "wiredTiger"
    if (storageEngine === 'ephemeralForTest' && semver.gte(coercedVersion, '7.0.0')) {
        console.warn('Storage Engine "ephemeralForTest" is removed since mongodb 7.0.0, automatically using "wiredTiger"!\n' +
            'This warning is because the mentioned storage engine is explicitly used and mongodb version is 7.0.0 or higher');
        return 'wiredTiger';
    }
    if (isNullOrUndefined(storageEngine)) {
        if (semver.gte(coercedVersion, '7.0.0')) {
            return 'wiredTiger';
        }
        return 'ephemeralForTest';
    }
    return storageEngine;
}
//# sourceMappingURL=utils.js.map