mongodb-memory-server-core
Version:
MongoDB Server for testing (core package, without autodownload). The server will allow you to connect your favourite ODM or client library to the MongoDB Server and run parallel integration tests isolated from each other.
391 lines • 21.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoInstance = exports.MongoInstanceEvents = void 0;
const tslib_1 = require("tslib");
const child_process_1 = require("child_process");
const path = (0, tslib_1.__importStar)(require("path"));
const MongoBinary_1 = require("./MongoBinary");
const debug_1 = (0, tslib_1.__importDefault)(require("debug"));
const utils_1 = require("./utils");
const semver_1 = require("semver");
const events_1 = require("events");
const mongodb_1 = require("mongodb");
const errors_1 = require("./errors");
// ignore the nodejs warning for coverage
/* istanbul ignore next */
if ((0, semver_1.lt)(process.version, '12.22.0')) {
console.warn('Using NodeJS below 12.22.0');
}
const log = (0, debug_1.default)('MongoMS:MongoInstance');
var MongoInstanceEvents;
(function (MongoInstanceEvents) {
MongoInstanceEvents["instanceReplState"] = "instanceReplState";
MongoInstanceEvents["instancePrimary"] = "instancePrimary";
MongoInstanceEvents["instanceReady"] = "instanceReady";
MongoInstanceEvents["instanceSTDOUT"] = "instanceSTDOUT";
MongoInstanceEvents["instanceSTDERR"] = "instanceSTDERR";
MongoInstanceEvents["instanceClosed"] = "instanceClosed";
/** Only Raw Error (emitted by mongodProcess) */
MongoInstanceEvents["instanceRawError"] = "instanceRawError";
/** Raw Errors and Custom Errors */
MongoInstanceEvents["instanceError"] = "instanceError";
MongoInstanceEvents["killerLaunched"] = "killerLaunched";
MongoInstanceEvents["instanceLaunched"] = "instanceLaunched";
MongoInstanceEvents["instanceStarted"] = "instanceStarted";
})(MongoInstanceEvents = exports.MongoInstanceEvents || (exports.MongoInstanceEvents = {}));
/**
* MongoDB Instance Handler Class
* This Class starts & stops the "mongod" process directly and handles stdout, sterr and close events
*/
class MongoInstance extends events_1.EventEmitter {
constructor(opts) {
super();
/**
* This boolean is "true" if the instance is elected to be PRIMARY
*/
this.isInstancePrimary = false;
/**
* This boolean is "true" if the instance is successfully started
*/
this.isInstanceReady = false;
/**
* This boolean is "true" if the instance is part of an replset
*/
this.isReplSet = false;
this.instanceOpts = Object.assign({}, opts.instance);
this.binaryOpts = Object.assign({}, opts.binary);
this.spawnOpts = Object.assign({}, opts.spawn);
this.on(MongoInstanceEvents.instanceReady, () => {
this.isInstanceReady = true;
this.debug('constructor: Instance is ready!');
});
this.on(MongoInstanceEvents.instanceError, (err) => (0, tslib_1.__awaiter)(this, void 0, void 0, function* () {
this.debug(`constructor: Instance has thrown an Error: ${err.toString()}`);
this.isInstanceReady = false;
this.isInstancePrimary = false;
yield this.stop();
}));
}
/**
* Debug-log with template applied
* @param msg The Message to log
*/
debug(msg, ...extra) {
var _a;
const port = (_a = this.instanceOpts.port) !== null && _a !== void 0 ? _a : 'unknown';
log(`Mongo[${port}]: ${msg}`, ...extra);
}
/**
* Create an new instance an call method "start"
* @param opts Options passed to the new instance
*/
static create(opts) {
return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () {
log('create: Called .create() method');
const instance = new this(opts);
yield instance.start();
return instance;
});
}
/**
* Create an array of arguments for the mongod instance
*/
prepareCommandArgs() {
var _a;
this.debug('prepareCommandArgs');
(0, utils_1.assertion)(!(0, utils_1.isNullOrUndefined)(this.instanceOpts.port), new Error('"instanceOpts.port" is required to be set!'));
(0, utils_1.assertion)(!(0, utils_1.isNullOrUndefined)(this.instanceOpts.dbPath), new Error('"instanceOpts.dbPath" is required to be set!'));
const result = [];
result.push('--port', this.instanceOpts.port.toString());
result.push('--dbpath', this.instanceOpts.dbPath);
// "!!" converts the value to an boolean (double-invert) so that no "falsy" values are added
if (!!this.instanceOpts.replSet) {
this.isReplSet = true;
result.push('--replSet', this.instanceOpts.replSet);
}
if (!!this.instanceOpts.storageEngine) {
result.push('--storageEngine', this.instanceOpts.storageEngine);
}
if (!!this.instanceOpts.ip) {
result.push('--bind_ip', this.instanceOpts.ip);
}
if (this.instanceOpts.auth) {
result.push('--auth');
if (this.isReplSet) {
(0, utils_1.assertion)(!(0, utils_1.isNullOrUndefined)(this.instanceOpts.keyfileLocation), new errors_1.KeyFileMissingError());
result.push('--keyFile', this.instanceOpts.keyfileLocation);
}
}
else {
result.push('--noauth');
}
const final = result.concat((_a = this.instanceOpts.args) !== null && _a !== void 0 ? _a : []);
this.debug('prepareCommandArgs: final argument array:' + JSON.stringify(final));
return final;
}
/**
* Create the mongod process
* @fires MongoInstance#instanceStarted
*/
start() {
return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () {
this.debug('start');
this.isInstancePrimary = false;
this.isInstanceReady = false;
this.isReplSet = false;
let timeout;
const launch = new Promise((res, rej) => {
this.once(MongoInstanceEvents.instanceReady, res);
this.once(MongoInstanceEvents.instanceError, rej);
this.once(MongoInstanceEvents.instanceClosed, function launchInstanceClosed() {
rej(new Error('Instance Exited before being ready and without throwing an error!'));
});
// extra conditions just to be sure that the custom defined timeout is valid
const timeoutTime = !!this.instanceOpts.launchTimeout && this.instanceOpts.launchTimeout >= 1000
? this.instanceOpts.launchTimeout
: 1000 * 10; // default 10 seconds
timeout = setTimeout(() => {
const err = new errors_1.GenericMMSError(`Instance failed to start within ${timeoutTime}ms`);
this.emit(MongoInstanceEvents.instanceError, err);
rej(err);
}, timeoutTime);
}).finally(() => {
// always clear the timeout after the promise somehow resolves
clearTimeout(timeout);
});
const mongoBin = yield MongoBinary_1.MongoBinary.getPath(this.binaryOpts);
yield (0, utils_1.checkBinaryPermissions)(mongoBin);
this.debug('start: Starting Processes');
this.mongodProcess = this._launchMongod(mongoBin);
// This assertion is here because somewhere between nodejs 12 and 16 the types for "childprocess.pid" changed to include "| undefined"
// it is tested and a error is thrown in "this_launchMongod", but typescript somehow does not see this yet as of 4.3.5
(0, utils_1.assertion)(!(0, utils_1.isNullOrUndefined)(this.mongodProcess.pid), new Error('MongoD Process failed to spawn'));
this.killerProcess = this._launchKiller(process.pid, this.mongodProcess.pid);
yield launch;
this.emit(MongoInstanceEvents.instanceStarted);
this.debug('start: Processes Started');
});
}
/**
* Shutdown all related processes (Mongod Instance & Killer Process)
*/
stop() {
return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () {
this.debug('stop');
if (!this.mongodProcess && !this.killerProcess) {
this.debug('stop: nothing to shutdown, returning');
return false;
}
if (!(0, utils_1.isNullOrUndefined)(this.mongodProcess)) {
// try to run "shutdown" before running "killProcess" (gracefull "SIGINT")
// using this, otherwise on windows nodejs will handle "SIGINT" & "SIGTERM" & "SIGKILL" the same (instant exit)
if (this.isReplSet) {
let con;
try {
this.debug('stop: trying shutdownServer');
const port = this.instanceOpts.port;
const ip = this.instanceOpts.ip;
(0, utils_1.assertion)(!(0, utils_1.isNullOrUndefined)(port), new Error('Cannot shutdown replset gracefully, no "port" is provided'));
(0, utils_1.assertion)(!(0, utils_1.isNullOrUndefined)(ip), new Error('Cannot shutdown replset gracefully, no "ip" is provided'));
con = yield mongodb_1.MongoClient.connect((0, utils_1.uriTemplate)(ip, port, 'admin'), Object.assign(Object.assign({}, this.extraConnectionOptions), { directConnection: true }));
const admin = con.db('admin'); // just to ensure it is actually the "admin" database
// "timeoutSecs" is set to "1" otherwise it will take at least "10" seconds to stop (very long tests)
yield admin.command({ shutdown: 1, force: true, timeoutSecs: 1 });
this.debug('stop: after admin shutdown command');
}
catch (err) {
// Quote from MongoDB Documentation (https://docs.mongodb.com/manual/reference/command/replSetStepDown/#client-connections):
// > Starting in MongoDB 4.2, replSetStepDown command no longer closes all client connections.
// > In MongoDB 4.0 and earlier, replSetStepDown command closes all client connections during the step down.
// so error "MongoNetworkError: connection 1 to 127.0.0.1:41485 closed" will get thrown below 4.2
if (!(err instanceof mongodb_1.MongoNetworkError &&
/^connection \d+ to [\d.]+:\d+ closed$/i.test(err.message))) {
console.warn(err);
}
}
finally {
if (!(0, utils_1.isNullOrUndefined)(con)) {
// even if it errors out, somehow the connection stays open
yield con.close();
}
}
}
yield (0, utils_1.killProcess)(this.mongodProcess, 'mongodProcess', this.instanceOpts.port);
this.mongodProcess = undefined; // reset reference to the childProcess for "mongod"
}
else {
this.debug('stop: mongodProcess: nothing to shutdown, skipping');
}
if (!(0, utils_1.isNullOrUndefined)(this.killerProcess)) {
yield (0, utils_1.killProcess)(this.killerProcess, 'killerProcess', this.instanceOpts.port);
this.killerProcess = undefined; // reset reference to the childProcess for "mongo_killer"
}
else {
this.debug('stop: killerProcess: nothing to shutdown, skipping');
}
this.debug('stop: Instance Finished Shutdown');
return true;
});
}
/**
* Actually launch mongod
* @param mongoBin The binary to run
* @fires MongoInstance#instanceLaunched
*/
_launchMongod(mongoBin) {
var _a, _b;
this.debug('_launchMongod: Launching Mongod Process');
const childProcess = (0, child_process_1.spawn)(path.resolve(mongoBin), this.prepareCommandArgs(), Object.assign(Object.assign({}, this.spawnOpts), { stdio: 'pipe' }));
(_a = childProcess.stderr) === null || _a === void 0 ? void 0 : _a.on('data', this.stderrHandler.bind(this));
(_b = childProcess.stdout) === null || _b === void 0 ? void 0 : _b.on('data', this.stdoutHandler.bind(this));
childProcess.on('close', this.closeHandler.bind(this));
childProcess.on('error', this.errorHandler.bind(this));
if ((0, utils_1.isNullOrUndefined)(childProcess.pid)) {
throw new errors_1.StartBinaryFailedError(path.resolve(mongoBin));
}
this.emit(MongoInstanceEvents.instanceLaunched);
return childProcess;
}
/**
* Spawn an seperate process to kill the parent and the mongod instance to ensure "mongod" gets stopped in any case
* @param parentPid Parent nodejs process
* @param childPid Mongod process to kill
* @fires MongoInstance#killerLaunched
*/
_launchKiller(parentPid, childPid) {
this.debug(`_launchKiller: Launching Killer Process (parent: ${parentPid}, child: ${childPid})`);
// spawn process which kills itself and mongo process if current process is dead
const killer = (0, child_process_1.fork)(path.resolve(__dirname, '../../scripts/mongo_killer.js'), [parentPid.toString(), childPid.toString()], {
detached: true,
stdio: 'ignore', // stdio cannot be done with an detached process cross-systems and without killing the fork on parent termination
});
killer.unref(); // dont force an exit on the fork when parent is exiting
this.emit(MongoInstanceEvents.killerLaunched);
return killer;
}
/**
* Event "error" handler
* @param err The Error to handle
* @fires MongoInstance#instanceRawError
* @fires MongoInstance#instanceError
*/
errorHandler(err) {
this.emit(MongoInstanceEvents.instanceRawError, err);
this.emit(MongoInstanceEvents.instanceError, err);
}
/**
* Write the CLOSE event to the debug function
* @param code The Exit code to handle
* @param signal The Signal to handle
* @fires MongoInstance#instanceClosed
*/
closeHandler(code, signal) {
// check if the platform is windows, if yes check if the code is not "12" or "0" otherwise just check code is not "0"
// because for mongodb any event on windows (like SIGINT / SIGTERM) will result in an code 12
// https://docs.mongodb.com/manual/reference/exit-codes/#12
if ((process.platform === 'win32' && code != 12 && code != 0) || code != 0) {
this.debug('closeHandler: Mongod instance closed with an non-0 (or non 12 on windows) code!');
// Note: this also emits when a signal is present, which is expected because signals are not expected here
this.emit(MongoInstanceEvents.instanceError, new errors_1.UnexpectedCloseError(code, signal));
}
this.debug(`closeHandler: code: "${code}", signal: "${signal}"`);
this.emit(MongoInstanceEvents.instanceClosed, code, signal);
}
/**
* Write STDERR to debug function
* @param message The STDERR line to write
* @fires MongoInstance#instanceSTDERR
*/
stderrHandler(message) {
const line = message.toString().trim();
this.debug(`stderrHandler: ""${line}""`); // denoting the STDERR string with double quotes, because the stdout might also use quotes
this.emit(MongoInstanceEvents.instanceSTDERR, line);
this.checkErrorInLine(line);
}
/**
* Write STDOUT to debug function and process some special messages
* @param message The STDOUT line to write/parse
* @fires MongoInstance#instanceSTDOUT
* @fires MongoInstance#instanceReady
* @fires MongoInstance#instanceError
* @fires MongoInstance#instancePrimary
* @fires MongoInstance#instanceReplState
*/
stdoutHandler(message) {
var _a, _b;
const line = message.toString().trim(); // trimming to remove extra new lines and spaces around the message
this.debug(`stdoutHandler: ""${line}""`); // denoting the STDOUT string with double quotes, because the stdout might also use quotes
this.emit(MongoInstanceEvents.instanceSTDOUT, line);
// dont use "else if", because input can be multiple lines and match multiple things
if (/waiting for connections/i.test(line)) {
this.emit(MongoInstanceEvents.instanceReady);
}
this.checkErrorInLine(line);
// this case needs to be infront of "transition to primary complete", otherwise it might reset "isInstancePrimary" to "false"
if (/transition to \w+ from \w+/i.test(line)) {
const state = (_b = (_a = /transition to (\w+) from \w+/i.exec(line)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 'UNKNOWN';
this.emit(MongoInstanceEvents.instanceReplState, state);
if (state !== 'PRIMARY') {
this.isInstancePrimary = false;
}
}
if (/transition to primary complete; database writes are now permitted/i.test(line)) {
this.isInstancePrimary = true;
this.debug('stdoutHandler: emitting "instancePrimary"');
this.emit(MongoInstanceEvents.instancePrimary);
}
}
/**
* Run Checks on the line if the lines contain any thrown errors
* @param line The Line to check
*/
checkErrorInLine(line) {
var _a, _b, _c, _d, _e;
if (/address already in use/i.test(line)) {
this.emit(MongoInstanceEvents.instanceError, new errors_1.StdoutInstanceError(`Port "${this.instanceOpts.port}" already in use`));
}
{
const execptionMatch = /\bexception in initAndListen: (\w+): /i.exec(line);
if (!(0, utils_1.isNullOrUndefined)(execptionMatch)) {
// in pre-4.0 mongodb this exception may have been "permission denied" and "Data directory /path not found"
this.emit(MongoInstanceEvents.instanceError, new errors_1.StdoutInstanceError(`Instance Failed to start with "${(_a = execptionMatch[1]) !== null && _a !== void 0 ? _a : 'unknown'}". Original Error:\n` +
line
.substring(execptionMatch.index + execptionMatch[0].length)
.replace(/, terminating$/gi, '')));
}
// special handling for when mongodb outputs this error as json
const execptionMatchJson = /\bDBException in initAndListen,/i.test(line);
if (execptionMatchJson) {
const loadedJSON = (_b = JSON.parse(line)) !== null && _b !== void 0 ? _b : {};
this.emit(MongoInstanceEvents.instanceError, new errors_1.StdoutInstanceError((_d = `Instance Failed to start with "DBException in initAndListen". Original Error:\n` +
((_c = loadedJSON === null || loadedJSON === void 0 ? void 0 : loadedJSON.attr) === null || _c === void 0 ? void 0 : _c.error)) !== null && _d !== void 0 ? _d : line // try to use the parsed json, but as fallback use the entire line
));
}
}
if (/CURL_OPENSSL_3['\s]+not found/i.test(line)) {
this.emit(MongoInstanceEvents.instanceError, new errors_1.StdoutInstanceError('libcurl3 is not available on your system. Mongod requires it and cannot be started without it.\n' +
'You should manually install libcurl3 or try to use an newer version of MongoDB'));
}
if (/CURL_OPENSSL_4['\s]+not found/i.test(line)) {
this.emit(MongoInstanceEvents.instanceError, new errors_1.StdoutInstanceError('libcurl4 is not available on your system. Mongod requires it and cannot be started without it.\n' +
'You need to manually install libcurl4'));
}
{
/*
The following regex matches something like "libsomething.so.1: cannot open shared object"
and is optimized to only start matching at a word boundary ("\b") and using atomic-group replacement "(?=inner)\1"
*/
const liberrormatch = line.match(/\b(?=(lib[^:]+))\1: cannot open shared object/i);
if (!(0, utils_1.isNullOrUndefined)(liberrormatch)) {
const lib = (_e = liberrormatch[1].toLocaleLowerCase()) !== null && _e !== void 0 ? _e : 'unknown';
this.emit(MongoInstanceEvents.instanceError, new errors_1.StdoutInstanceError(`Instance failed to start because a library is missing or cannot be opened: "${lib}"`));
}
}
if (/\*\*\*aborting after/i.test(line)) {
this.emit(MongoInstanceEvents.instanceError, new errors_1.StdoutInstanceError('Mongod internal error'));
}
}
}
exports.MongoInstance = MongoInstance;
exports.default = MongoInstance;
//# sourceMappingURL=MongoInstance.js.map