mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
1118 lines
35 KiB
JavaScript
1118 lines
35 KiB
JavaScript
import { MongoExitCodes } from '../utils/mongo-exit-codes';
|
|
var files = require('../fs/files');
|
|
var utils = require('../utils/utils.js');
|
|
var fiberHelpers = require('../utils/fiber-helpers.js');
|
|
var runLog = require('./run-log.js');
|
|
var child_process = require('child_process');
|
|
var _ = require('underscore');
|
|
import { loadIsopackage } from '../tool-env/isopackets.js';
|
|
var Console = require('../console/console.js').Console;
|
|
|
|
// Given a Mongo URL, open an interactive Mongo shell on this terminal
|
|
// on that database.
|
|
var runMongoShell = function(url) {
|
|
var mongoPath = files.pathJoin(
|
|
files.getDevBundle(),
|
|
'mongodb',
|
|
'bin',
|
|
'mongo'
|
|
);
|
|
// XXX mongo URLs are not real URLs (notably, the comma-separation for
|
|
// multiple hosts). We've had a little better luck using the mongodb-uri npm
|
|
// package.
|
|
var mongoUrl = require('url').parse(url);
|
|
var auth = mongoUrl.auth && mongoUrl.auth.split(':');
|
|
var ssl = require('querystring').parse(mongoUrl.query).ssl === 'true';
|
|
|
|
var args = [];
|
|
if (ssl) {
|
|
args.push('--ssl');
|
|
}
|
|
if (auth) {
|
|
args.push('-u', auth[0]);
|
|
}
|
|
if (auth) {
|
|
args.push('-p', auth[1]);
|
|
}
|
|
args.push(mongoUrl.hostname + ':' + mongoUrl.port + mongoUrl.pathname);
|
|
|
|
// run with rosetta on mac m1
|
|
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
args = ['-x86_64', mongoPath, ...args];
|
|
mongoPath = 'arch';
|
|
}
|
|
|
|
child_process.spawn(files.convertToOSPath(mongoPath), args, {
|
|
stdio: 'inherit',
|
|
});
|
|
};
|
|
|
|
// Start mongod with a dummy replSet and wait for it to listen.
|
|
function spawnMongod(mongodPath, port, dbPath, replSetName) {
|
|
const child_process = require('child_process');
|
|
|
|
mongodPath = files.convertToOSPath(mongodPath);
|
|
dbPath = files.convertToOSPath(dbPath);
|
|
|
|
let args = [
|
|
// nb: cli-test.sh and findMongoPids make strong assumptions about the
|
|
// order of the arguments! Check them before changing any arguments.
|
|
'--bind_ip',
|
|
process.env.METEOR_MONGO_BIND_IP || '127.0.0.1',
|
|
'--port',
|
|
port,
|
|
'--dbpath',
|
|
dbPath,
|
|
// Use an 8MB oplog rather than 256MB. Uses less space on disk and
|
|
// initializes faster. (Not recommended for production!)
|
|
'--oplogSize',
|
|
'8',
|
|
'--replSet',
|
|
replSetName,
|
|
'--noauth'
|
|
];
|
|
|
|
// Use mmapv1 on 32bit platforms, as our binary doesn't support WT
|
|
if (process.arch === 'ia32') {
|
|
args.push('--storageEngine', 'mmapv1', '--smallfiles');
|
|
} else if (process.platform !== 'linux') {
|
|
// MongoDB 4, which we use on 64-bit systems, displays a banner in the
|
|
// Mongo shell about a free monitoring service, which can be disabled
|
|
// with this flag. However, the custom Linux build (see MONGO_BASE_URL
|
|
// in scripts/generate-dev-bundle.sh) neither displays the banner nor
|
|
// supports the flag, so it's safe/important to avoid passing the flag
|
|
// to mongod on 64-bit linux.
|
|
args.push('--enableFreeMonitoring', 'off');
|
|
}
|
|
|
|
// run with rosetta on mac m1
|
|
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
args = ['-x86_64', mongodPath, ...args];
|
|
mongodPath = 'arch';
|
|
}
|
|
return child_process.spawn(mongodPath, args, {
|
|
// Apparently in some contexts, Mongo crashes if your locale isn't set up
|
|
// right. I wasn't able to reproduce it, but many people on #4019
|
|
// were. Let's default a couple environment variables to English UTF-8 if
|
|
// they aren't set already. If these few aren't good enough, we'll at least
|
|
// detect the locale error and print a link to #4019 (look for
|
|
// `detectedErrors.badLocale` below).
|
|
env: Object.assign(
|
|
{
|
|
LANG: 'en_US.UTF-8',
|
|
LC_ALL: 'en_US.UTF-8',
|
|
},
|
|
process.env
|
|
),
|
|
});
|
|
}
|
|
|
|
// Find all running Mongo processes that were started by this program
|
|
// (even by other simultaneous runs of this program). If passed,
|
|
// dbDir and port act as filters on the list of running mongos.
|
|
//
|
|
// Yields. Returns an array of objects with keys pid, port, dbDir.
|
|
var findMongoPids;
|
|
if (process.platform === 'win32') {
|
|
// Windows doesn't have a ps equivalent that (reliably) includes the command
|
|
// line, so approximate using the combined output of tasklist and netstat.
|
|
findMongoPids = async function(dbDir_unused, port) {
|
|
var promise = fiberHelpers.makeFulfillablePromise();
|
|
|
|
child_process.exec('tasklist /fi "IMAGENAME eq mongod.exe"', function(
|
|
error,
|
|
stdout,
|
|
stderr
|
|
) {
|
|
if (error) {
|
|
var additionalInfo = JSON.stringify(error);
|
|
if (error.code === 'ENOENT') {
|
|
additionalInfo =
|
|
"tasklist wasn't found on your system, it usually can be found at C:\\Windows\\System32\\.";
|
|
}
|
|
promise.reject(
|
|
new Error("Couldn't run tasklist.exe: " + additionalInfo)
|
|
);
|
|
return;
|
|
} else {
|
|
// Find the pids of all mongod processes
|
|
var mongo_pids = [];
|
|
stdout.split('\n').forEach(function(line) {
|
|
var m = line.match(/^mongod.exe\s+(\d+) /);
|
|
if (m) {
|
|
mongo_pids[m[1]] = true;
|
|
}
|
|
});
|
|
|
|
// Now get the corresponding port numbers
|
|
child_process.exec(
|
|
'netstat -ano',
|
|
{ maxBuffer: 1024 * 1024 * 10 },
|
|
function(error, stdout, stderr) {
|
|
if (error) {
|
|
promise.reject(
|
|
new Error("Couldn't run netstat -ano: " + JSON.stringify(error))
|
|
);
|
|
return;
|
|
} else {
|
|
var pids = [];
|
|
stdout.split('\n').forEach(function(line) {
|
|
var m = line.match(
|
|
/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/
|
|
);
|
|
if (m) {
|
|
var found_pid = parseInt(m[2], 10);
|
|
var found_port = parseInt(m[1], 10);
|
|
|
|
// We can't check the path app_dir so assume it always matches
|
|
if (mongo_pids[found_pid] && (!port || port === found_port)) {
|
|
// Note that if the mongo rest interface is enabled the
|
|
// initial port + 1000 is also likely to be open.
|
|
// So remove the pid so we only match it once.
|
|
delete mongo_pids[found_pid];
|
|
pids.push({
|
|
pid: found_pid,
|
|
port: found_port,
|
|
app_dir: null,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
promise.resolve(pids);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
return await promise;
|
|
};
|
|
} else {
|
|
findMongoPids = async function(dbDir, port) {
|
|
var promise = fiberHelpers.makeFulfillablePromise();
|
|
|
|
// 'ps ax' should be standard across all MacOS and Linux.
|
|
// However, ps on OS X corrupts some non-ASCII characters in arguments,
|
|
// such as т (CYRILLIC SMALL LETTER TE), leading to this function
|
|
// failing to properly match pathnames with those characters. #3999
|
|
//
|
|
// pgrep appears to do a better job (and has output that is roughly
|
|
// similar; it lacks a few fields that we don't care about). Plus,
|
|
// it can do some of the grepping for us.
|
|
//
|
|
// However, 'pgrep' only started shipping with OS X 10.8 (and may be less
|
|
// common on Linux too), so we check to see if it exists and fall back to
|
|
// 'ps' if we can't find it.
|
|
//
|
|
// We avoid using pgrep on Linux, because some versions of Linux pgrep
|
|
// require you to pass -a/--list-full to include the arguments in the
|
|
// output, and other versions fail if you pass that option. We have not
|
|
// observed the Unicode corruption on Linux, so using ps ax there is fine.
|
|
var psScript = 'ps ax';
|
|
if (process.platform === 'darwin') {
|
|
psScript =
|
|
'if type pgrep >/dev/null 2>&1; then ' +
|
|
// -lf means to display and match against full argument lists.
|
|
// pgrep exits 1 if no processes match the argument; we're OK
|
|
// considering this as a success, but we don't want other errors
|
|
// to be ignored. Note that this is sh not bash, so we can't use
|
|
// [[.
|
|
'pgrep -lf mongod; test "$?" -eq 0 -o "$?" -eq 1;' +
|
|
'else ps ax; fi';
|
|
}
|
|
|
|
// If the child process output includes unicode, make sure it's
|
|
// handled properly.
|
|
const {
|
|
LANG = 'en_US.UTF-8',
|
|
LC_ALL = LANG,
|
|
LANGUAGE = LANG,
|
|
// Remainder of process.env without above properties.
|
|
...env
|
|
} = process.env;
|
|
|
|
// Make sure all three properties are set to the same value, which
|
|
// defaults to "en_US.UTF-8" or whatever LANG was already set to.
|
|
Object.assign(env, { LANG, LC_ALL, LANGUAGE });
|
|
|
|
child_process.exec(
|
|
psScript,
|
|
{
|
|
env,
|
|
// we don't want this to randomly fail just because you're running
|
|
// lots of processes. 10MB should be more than ps ax will ever
|
|
// spit out; the default is 200K, which at least one person hit
|
|
// (#2158).
|
|
maxBuffer: 1024 * 1024 * 10,
|
|
},
|
|
function(error, stdout, stderr) {
|
|
if (error) {
|
|
promise.reject(
|
|
new Error(
|
|
"Couldn't run ps ax: " +
|
|
JSON.stringify(error) +
|
|
'; ' +
|
|
error.message
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
var ret = [];
|
|
stdout.split('\n').forEach(function(line) {
|
|
// Matches mongos we start. Note that this matches
|
|
// 'fake-mongod' (our mongod stub for automated tests) as well
|
|
// as 'mongod'.
|
|
var m = line.match(
|
|
/^\s*(\d+).+mongod .+--port (\d+) --dbpath (.+(?:\/|\\)db)/
|
|
);
|
|
if (m && m.length === 4) {
|
|
var foundPid = parseInt(m[1], 10);
|
|
var foundPort = parseInt(m[2], 10);
|
|
var foundPath = m[3];
|
|
|
|
if (
|
|
(!port || port === foundPort) &&
|
|
(!dbDir || dbDir === foundPath)
|
|
) {
|
|
ret.push({
|
|
pid: foundPid,
|
|
port: foundPort,
|
|
dbDir: foundPath,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
promise.resolve(ret);
|
|
}
|
|
);
|
|
|
|
return await promise;
|
|
};
|
|
}
|
|
|
|
// See if mongo is running already. Yields. Returns the port that
|
|
// mongo is running on or null if mongo is not running.
|
|
var findMongoPort = async function(dbDir) {
|
|
var pids = await findMongoPids(dbDir);
|
|
|
|
if (pids.length !== 1) {
|
|
return null;
|
|
}
|
|
|
|
var pid = pids[0].pid;
|
|
try {
|
|
process.kill(pid, 0); // make sure it is still alive
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
|
|
return pids[0].port;
|
|
};
|
|
|
|
// XXX actually -- the code below is probably more correct than the code we
|
|
// have above for non-Windows platforms (since that code relies on
|
|
// `findMongoPids`). But changing this a few days before the 1.1 release
|
|
// seemed too bold. But if you're changing code around here, consider using
|
|
// the implementation below on non-Windows platforms as well.
|
|
if (process.platform === 'win32') {
|
|
// On Windows, finding the Mongo pid, checking it and extracting the port
|
|
// is often unreliable (XXX reliable in what specific way?). There is an
|
|
// easier way to find the port of running Mongo: look it up in a METEOR-
|
|
// PORT file that we generate when running. This may result into problems
|
|
// where we try to connect to a mongod that is not running, or a wrong
|
|
// mongod if our current app is not running but there is a left-over file
|
|
// lying around. This still can be better than always failing to connect.
|
|
findMongoPort = function(dbPath) {
|
|
var mongoPort = null;
|
|
|
|
var portFile = files.pathJoin(dbPath, 'METEOR-PORT');
|
|
if (files.exists(portFile)) {
|
|
mongoPort = files.readFile(portFile, 'utf8').replace(/\s/g, '');
|
|
}
|
|
|
|
// Now, check if there really is a Mongo server running on this port.
|
|
// (The METEOR-PORT file may point to an old Mongo server that's now
|
|
// stopped)
|
|
var net = require('net');
|
|
|
|
return new Promise(resolve => {
|
|
var client = net.connect(
|
|
{
|
|
port: mongoPort,
|
|
},
|
|
() => {
|
|
// The server is running.
|
|
client.end();
|
|
resolve(mongoPort);
|
|
}
|
|
);
|
|
client.on('error', () => resolve(null));
|
|
})
|
|
.catch(() => null);
|
|
};
|
|
}
|
|
|
|
// Kill any mongos running on 'port'. Yields, and returns once they
|
|
// are all dead. Throws an exception on failure.
|
|
//
|
|
// This is a big hammer for dealing with still running mongos, but
|
|
// smaller hammers have failed before and it is getting tiresome.
|
|
var findMongoAndKillItDead = async function(port, dbPath) {
|
|
var pids = await findMongoPids(null, port);
|
|
|
|
// Go through the list serially. There really should only ever be
|
|
// at most one but we're not taking any chances.
|
|
pidsLoop:
|
|
for (const processInfo of pids) {
|
|
var pid = processInfo.pid;
|
|
|
|
// Send kill attempts and wait. First a SIGINT, then if it isn't
|
|
// dead within 2 sec, SIGKILL. Check every 100ms to see if it's
|
|
// dead.
|
|
for (var attempts = 1; attempts <= 40; attempts++) {
|
|
var signal = 0;
|
|
if (attempts === 1) {
|
|
signal = 'SIGINT';
|
|
} else if (attempts === 20 || attempts === 30) {
|
|
signal = 'SIGKILL';
|
|
}
|
|
|
|
try {
|
|
process.kill(pid, signal);
|
|
} catch (e) {
|
|
// it's dead. on to the next one
|
|
break pidsLoop;
|
|
}
|
|
|
|
await utils.sleepMs(100);
|
|
}
|
|
|
|
// give up after 4 seconds.
|
|
// XXX should actually catch this higher up and print a nice
|
|
// error. foreseeable conditions should never result in exceptions
|
|
// for the user.
|
|
throw new Error("Can't kill running mongo (pid " + pid + ').');
|
|
}
|
|
|
|
// If we had to kill mongod with SIGKILL, or on Windows where all calls to
|
|
// `process.kill` work like SIGKILL, mongod will not have the opportunity to
|
|
// close gracefully. Delete a lock file that may have been left over.
|
|
var mongodLockFile = files.pathJoin(dbPath, 'mongod.lock');
|
|
if (files.exists(mongodLockFile)) {
|
|
files.unlink(mongodLockFile);
|
|
}
|
|
};
|
|
|
|
var StoppedDuringLaunch = function() {};
|
|
|
|
// Starts a single instance of mongod, and configures it properly as a singleton
|
|
// replica set. Yields. Returns once the mongod is successfully listening (or
|
|
// the process exited).
|
|
//
|
|
// Takes an onExit handler, which will be invoked when the process exits (which
|
|
// may be before or after this function returns depending on whether or not it
|
|
// ever successfully started).
|
|
//
|
|
// If the 'multiple' option is set, it actually sets up three mongod instances
|
|
// (launching the second and third on the next two ports after the specified
|
|
// port). In this case, if any of the three instances exit for any reason, all
|
|
// are killed (and onExit is then invoked). Also, the entirety of all three
|
|
// databases is deleted before starting up. This is mode intended for testing
|
|
// mongo failover, not for normal development or production use.
|
|
var launchMongo = async function(options) {
|
|
var onExit = options.onExit || function() {};
|
|
|
|
var noOplog = false;
|
|
var mongod_path = files.pathJoin(
|
|
files.getDevBundle(),
|
|
'mongodb',
|
|
'bin',
|
|
'mongod'
|
|
);
|
|
var replSetName = 'meteor';
|
|
|
|
// Automated testing: If this is set, instead of starting mongod, we
|
|
// start our stub (fake-mongod) which can then be remote-controlled
|
|
// by the test.
|
|
if (process.env.METEOR_TEST_FAKE_MONGOD_CONTROL_PORT) {
|
|
if (options.multiple) {
|
|
throw Error("Can't specify multiple with fake mongod");
|
|
}
|
|
|
|
var fakeMongodCommand =
|
|
process.platform === 'win32' ? 'fake-mongod.bat' : 'fake-mongod';
|
|
mongod_path = files.pathJoin(
|
|
files.getCurrentToolsDir(),
|
|
'tools',
|
|
'tests',
|
|
'fake-mongod',
|
|
fakeMongodCommand
|
|
);
|
|
|
|
// oplog support requires sending admin commands to mongod, so
|
|
// it'd be hard to make fake-mongod support it.
|
|
noOplog = true;
|
|
}
|
|
|
|
var subHandles = [];
|
|
var stopped = false;
|
|
var handle = {};
|
|
var stopPromise = new Promise((resolve, reject) => {
|
|
handle.stop = function() {
|
|
if (stopped) {
|
|
return;
|
|
}
|
|
stopped = true;
|
|
_.each(subHandles, function(handle) {
|
|
handle.stop();
|
|
});
|
|
|
|
if (options.onStopped) {
|
|
options.onStopped();
|
|
}
|
|
|
|
reject(new StoppedDuringLaunch());
|
|
};
|
|
});
|
|
|
|
var yieldingMethod = async function(object, methodName, ...args) {
|
|
return await Promise.race([
|
|
stopPromise,
|
|
new Promise((resolve, reject) => {
|
|
object[methodName](...args, (err, res) => {
|
|
err ? reject(err) : resolve(res);
|
|
});
|
|
}),
|
|
]);
|
|
};
|
|
|
|
var launchOneMongoAndWaitForReadyForInitiate = async function(
|
|
dbPath,
|
|
port,
|
|
portFile
|
|
) {
|
|
files.mkdir_p(dbPath, 0o755);
|
|
|
|
var proc = null;
|
|
|
|
if (options.allowKilling) {
|
|
await findMongoAndKillItDead(port, dbPath);
|
|
}
|
|
|
|
if (options.multiple) {
|
|
// This is only for testing, so we're OK with incurring the replset
|
|
// setup on each startup.
|
|
await files.rm_recursive(dbPath);
|
|
files.mkdir_p(dbPath, 0o755);
|
|
} else if (portFile) {
|
|
var portFileExists = false;
|
|
var matchingPortFileExists = false;
|
|
try {
|
|
matchingPortFileExists = +files.readFile(portFile) === port;
|
|
portFileExists = true;
|
|
} catch (e) {
|
|
if (!e || e.code !== 'ENOENT') {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// If this is the first time we're using this DB, or we changed port since
|
|
// the last time, then we want to destroy any existing replSet
|
|
// configuration and create a new one. First we delete the "local"
|
|
// database if it exists. (It's a pain and slow to change the port in an
|
|
// existing replSet configuration. It's also a little slow to initiate a
|
|
// new replSet, thus the attempt to not do it unless the port changes.)
|
|
//
|
|
// In the "multiple" case, we just wipe out the entire database and incur
|
|
// the cost, because this won't affect normal users running meteor.
|
|
if (!matchingPortFileExists) {
|
|
// Delete the port file if it exists, so we don't mistakenly believe
|
|
// that the DB is still configured.
|
|
if (portFileExists) {
|
|
files.unlink(portFile);
|
|
}
|
|
|
|
try {
|
|
var dbFiles = files.readdir(dbPath);
|
|
} catch (e) {
|
|
if (!e || e.code !== 'ENOENT') {
|
|
throw e;
|
|
}
|
|
}
|
|
_.each(dbFiles, function(dbFile) {
|
|
if (/^local\./.test(dbFile)) {
|
|
files.unlink(files.pathJoin(dbPath, dbFile));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Let's not actually start a process if we yielded (eg during
|
|
// findMongoAndKillItDead) and we decided to stop in the middle (eg, because
|
|
// we're in multiple mode and another process exited).
|
|
if (stopped) {
|
|
return;
|
|
}
|
|
|
|
proc = spawnMongod(mongod_path, port, dbPath, replSetName);
|
|
|
|
function stop() {
|
|
if (proc) {
|
|
proc.removeListener('exit', procExitHandler);
|
|
proc.kill('SIGINT');
|
|
proc = null;
|
|
}
|
|
}
|
|
require('../tool-env/cleanup.js').onExit(stop);
|
|
subHandles.push({ stop });
|
|
|
|
var procExitHandler = await fiberHelpers.bindEnvironment(async function(code, signal) {
|
|
// Defang subHandle.stop().
|
|
proc = null;
|
|
|
|
// Kill any other processes too. This will also remove
|
|
// procExitHandler from the other processes, so onExit will only be called
|
|
// once.
|
|
await handle.stop();
|
|
|
|
// Invoke the outer onExit callback.
|
|
await onExit(code, signal, stderrOutput, detectedErrors);
|
|
});
|
|
proc.on('exit', procExitHandler);
|
|
|
|
var listening = false;
|
|
var replSetReadyToBeInitiated = false;
|
|
var replSetReady = false;
|
|
|
|
var maybeReadyToTalk;
|
|
var readyToTalkPromise = new Promise(function(resolve) {
|
|
maybeReadyToTalk = function() {
|
|
if (
|
|
resolve &&
|
|
listening &&
|
|
(noOplog || replSetReadyToBeInitiated || replSetReady)
|
|
) {
|
|
proc.stdout.removeListener('data', stdoutOnData);
|
|
resolve();
|
|
resolve = null;
|
|
}
|
|
};
|
|
});
|
|
|
|
var stopOrReadyPromise = Promise.race([stopPromise, readyToTalkPromise]);
|
|
|
|
var detectedErrors = {};
|
|
var stdoutOnData = fiberHelpers.bindEnvironment(function(data) {
|
|
// note: don't use "else ifs" in this, because 'data' can have multiple
|
|
// lines
|
|
if (
|
|
/replica set config in use/.test(data) ||
|
|
/Did not find local replica set configuration document at startup/.test(
|
|
data
|
|
) ||
|
|
/\[.*\] Locally stored replica set configuration does not have a valid entry for the current node/.test(
|
|
data
|
|
)
|
|
) {
|
|
replSetReadyToBeInitiated = true;
|
|
maybeReadyToTalk();
|
|
}
|
|
|
|
if (
|
|
/Waiting for connections/.test(data) ||
|
|
/ \[.*\] waiting for connections on port/.test(data)
|
|
) {
|
|
listening = true;
|
|
maybeReadyToTalk();
|
|
}
|
|
|
|
if (/ \[rsSync-0\] transition to primary complete/.test(data)) {
|
|
replSetReady = true;
|
|
maybeReadyToTalk();
|
|
}
|
|
|
|
if (/Insufficient free space/.test(data)) {
|
|
detectedErrors.freeSpace = true;
|
|
}
|
|
|
|
// Running against a old mmapv1 engine, probably from pre-mongo-3.2 Meteor
|
|
if (
|
|
/created by the 'mmapv1' storage engine, so setting the active storage engine to 'mmapv1'/.test(
|
|
data
|
|
)
|
|
) {
|
|
Console.warn();
|
|
Console.warn(
|
|
'Your development database is using mmapv1, ' +
|
|
'the old, pre-MongoDB 3.0 database engine. ' +
|
|
'You should consider upgrading to Wired Tiger, the new engine. ' +
|
|
'The easiest way to do so in development is to run ' +
|
|
Console.command('meteor reset') +
|
|
'. ' +
|
|
"If you'd like to migrate your database, please consult " +
|
|
Console.url(
|
|
'https://docs.mongodb.org/v3.0/release-notes/3.0-upgrade/'
|
|
)
|
|
);
|
|
Console.warn();
|
|
}
|
|
|
|
if (/Invalid or no user locale set/.test(data)) {
|
|
detectedErrors.badLocale = true;
|
|
}
|
|
});
|
|
proc.stdout.setEncoding('utf8');
|
|
proc.stdout.on('data', stdoutOnData);
|
|
|
|
var stderrOutput = '';
|
|
proc.stderr.setEncoding('utf8');
|
|
proc.stderr.on('data', function(data) {
|
|
stderrOutput += data;
|
|
});
|
|
|
|
await stopOrReadyPromise;
|
|
};
|
|
|
|
var initiateReplSetAndWaitForReady = async function () {
|
|
try {
|
|
// Load mongo so we'll be able to talk to it.
|
|
const {MongoClient} = (await loadIsopackage(
|
|
'npm-mongo'
|
|
)).NpmModuleMongodb;
|
|
|
|
// Connect to the intended primary and start a replset.
|
|
const client = new MongoClient(
|
|
`mongodb://127.0.0.1:${options.port}`, {
|
|
minPoolSize: 1,
|
|
maxPoolSize: 1,
|
|
socketTimeoutMS: 60000,
|
|
directConnection: true
|
|
}
|
|
);
|
|
|
|
const db = client.db('meteor');
|
|
|
|
if (stopped) {
|
|
return;
|
|
}
|
|
|
|
var configuration = {
|
|
_id: replSetName,
|
|
version: 1,
|
|
protocolVersion: 1,
|
|
members: [{_id: 0, host: '127.0.0.1:' + options.port, priority: 100}],
|
|
};
|
|
|
|
try {
|
|
const config = (await yieldingMethod(db.admin(), 'command', {
|
|
replSetGetConfig: 1,
|
|
})).config;
|
|
|
|
// If a replication set configuration already exists, it's
|
|
// important that the new version number is greater than the old.
|
|
if (config && _.has(config, 'version')) {
|
|
configuration.version = config.version + 1;
|
|
}
|
|
} catch (e) {}
|
|
|
|
if (options.multiple) {
|
|
// Add two more members: one of which should start as secondary but
|
|
// could in theory become primary, and one of which can never be
|
|
// primary.
|
|
configuration.members.push({
|
|
_id: 1,
|
|
host: '127.0.0.1:' + (options.port + 1),
|
|
priority: 5,
|
|
});
|
|
configuration.members.push({
|
|
_id: 2,
|
|
host: '127.0.0.1:' + (options.port + 2),
|
|
priority: 0,
|
|
});
|
|
}
|
|
|
|
try {
|
|
await yieldingMethod(db.admin(), 'command', {
|
|
replSetInitiate: configuration,
|
|
});
|
|
} catch (e) {
|
|
if (e.message === 'already initialized') {
|
|
await yieldingMethod(db.admin(), 'command', {
|
|
replSetReconfig: configuration,
|
|
force: true,
|
|
});
|
|
} else {
|
|
throw Error('rs.initiate error: ' + e.message);
|
|
}
|
|
}
|
|
|
|
if (stopped) {
|
|
return;
|
|
}
|
|
|
|
let writableTimestamp = Date.now();
|
|
|
|
// Wait until the primary is writable. If it isn't writable after one
|
|
// minute, throw an error and report the replica set status.
|
|
while (!stopped) {
|
|
const {ismaster} = await yieldingMethod(db.admin(), 'command', {
|
|
isMaster: 1,
|
|
});
|
|
|
|
if (ismaster) {
|
|
// From mongoDB 5.0, w: majority is the default write concern for most MongoDB configurations
|
|
// this causes writes to be acknowledged after the timeout on M1 macs
|
|
// We are explicitly setting it to 1 when there is only 1 node, as we do simulate replica sets with only 1 node
|
|
// when running locally or in test environments.
|
|
// ref: https://docs.mongodb.com/manual/reference/write-concern/#mongodb-writeconcern-writeconcern.-majority-
|
|
await yieldingMethod(db.admin(), 'command', {
|
|
setDefaultRWConcern: 1,
|
|
...( options.multiple ? {} : {defaultWriteConcern: {w: 1}})
|
|
});
|
|
break;
|
|
} else if (Date.now() - writableTimestamp > 60000) {
|
|
const status = await yieldingMethod(db.admin(), 'command', {
|
|
replSetGetStatus: 1,
|
|
});
|
|
|
|
throw new Error(
|
|
'Primary not writable after one minute. Last replica set status: ' +
|
|
JSON.stringify(status)
|
|
);
|
|
}
|
|
|
|
await utils.sleepMs(50);
|
|
}
|
|
|
|
client.close(true /* means "the app is closing the connection" */);
|
|
} catch (e) {
|
|
// If the process has exited, we're doing another form of error
|
|
// handling. No need to throw random low-level errors farther.
|
|
if (!stopped || e instanceof StoppedDuringLaunch) {
|
|
throw e;
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
if (options.multiple) {
|
|
var dbBasePath = files.pathJoin(options.projectLocalDir, 'dbs');
|
|
let i = 2;
|
|
while (i >= 0) {
|
|
// Did we get stopped (eg, by one of the processes exiting) by now? Then
|
|
// don't start anything new.
|
|
if (stopped) {
|
|
return;
|
|
}
|
|
var dbPath = files.pathJoin(options.projectLocalDir, 'dbs', '' + i);
|
|
await launchOneMongoAndWaitForReadyForInitiate(dbPath, options.port + i);
|
|
i--;
|
|
}
|
|
|
|
if (!stopped) {
|
|
await initiateReplSetAndWaitForReady();
|
|
}
|
|
} else {
|
|
var dbPath = files.pathJoin(options.projectLocalDir, 'db');
|
|
var portFile = !noOplog && files.pathJoin(dbPath, 'METEOR-PORT');
|
|
await launchOneMongoAndWaitForReadyForInitiate(dbPath, options.port, portFile);
|
|
if (!stopped && !noOplog) {
|
|
await initiateReplSetAndWaitForReady();
|
|
if (!stopped) {
|
|
// Write down that we configured the database properly.
|
|
files.writeFile(portFile, '' + options.port);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (!(e instanceof StoppedDuringLaunch)) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
if (stopped) {
|
|
return null;
|
|
}
|
|
|
|
return handle;
|
|
};
|
|
|
|
// This runs a Mongo process and restarts it whenever it fails. If it
|
|
// restarts too often, we give up on restarting it, diagnostics are
|
|
// logged, and onFailure is called.
|
|
//
|
|
// options: projectLocalDir, port, onFailure, multiple
|
|
var MongoRunner = function(options) {
|
|
var self = this;
|
|
self.projectLocalDir = options.projectLocalDir;
|
|
self.port = options.port;
|
|
self.onFailure = options.onFailure;
|
|
self.multiple = options.multiple;
|
|
|
|
self.handle = null;
|
|
self.shuttingDown = false;
|
|
self.resolveStartupPromise = null;
|
|
|
|
self.errorCount = 0;
|
|
self.errorTimer = null;
|
|
self.restartTimer = null;
|
|
self.firstStart = true;
|
|
self.suppressExitMessage = false;
|
|
};
|
|
|
|
var MRp = MongoRunner.prototype;
|
|
|
|
Object.assign(MRp, {
|
|
// Blocks (yields) until the server has started for the first time and
|
|
// is accepting connections. (It might subsequently die and be
|
|
// restarted; we won't tell you about that.)
|
|
//
|
|
// If the server fails to start for the first time (after a few
|
|
// restarts), we'll print a message and give up.
|
|
start: async function() {
|
|
var self = this;
|
|
|
|
if (self.handle) {
|
|
throw new Error('already running?');
|
|
}
|
|
|
|
await self._startOrRestart();
|
|
|
|
// Did we properly start up? Great!
|
|
if (self.handle) {
|
|
return;
|
|
}
|
|
|
|
// Are we shutting down? OK.
|
|
if (self.shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
// Otherwise, wait for a successful _startOrRestart, or a failure.
|
|
if (!self.resolveStartupPromise) {
|
|
await new Promise(function(resolve) {
|
|
self.resolveStartupPromise = resolve;
|
|
});
|
|
}
|
|
},
|
|
|
|
// Tries to launch Mongo once. It returns when either (a) Mongo is listening
|
|
// or (b) mongod exited before it got to the point of listening.
|
|
//
|
|
// (To be specific: in non-multiple mode, this means that the single mongod is
|
|
// listening and the primary, or that the single mongod died. In multiple
|
|
// mode, it means that the first mongod is listening and is primary and the
|
|
// other mongods are listening and are secondary, or that any mongod died (and
|
|
// it tried to kill the others).)
|
|
//
|
|
// In case (a), self.handle will be the handle returned from launchMongo; in
|
|
// case (b) self.handle will be null.
|
|
_startOrRestart: async function() {
|
|
var self = this;
|
|
|
|
if (self.handle) {
|
|
throw new Error('already running?');
|
|
}
|
|
|
|
var allowKilling = self.multiple || self.firstStart;
|
|
self.firstStart = false;
|
|
if (!allowKilling) {
|
|
// If we're not going to try to kill an existing mongod first, then we
|
|
// shouldn't annoy the user by telling it that we couldn't start up.
|
|
self.suppressExitMessage = true;
|
|
}
|
|
self.handle = await launchMongo({
|
|
projectLocalDir: self.projectLocalDir,
|
|
port: self.port,
|
|
multiple: self.multiple,
|
|
allowKilling: allowKilling,
|
|
onExit: _.bind(self._exited, self),
|
|
onStopped() {
|
|
self.suppressExitMessage = false;
|
|
},
|
|
});
|
|
// It has successfully started up, so if it exits after this point, that
|
|
// actually is an interesting fact and we shouldn't suppress it.
|
|
self.suppressExitMessage = false;
|
|
|
|
if (self.handle) {
|
|
self._allowStartupToReturn();
|
|
}
|
|
},
|
|
|
|
_exited: async function(code, signal, stderr, detectedErrors) {
|
|
var self = this;
|
|
|
|
self.handle = null;
|
|
|
|
// If Mongo exited because (or rather, anytime after) we told it
|
|
// to exit, great, nothing to do. Otherwise, we'll print an error
|
|
// and try to restart.
|
|
if (self.shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
// Only print an error if we tried to kill Mongo and something went
|
|
// wrong. If we didn't try to kill Mongo, we'll do that on the next
|
|
// restart. Not killing it on the first try is important for speed,
|
|
// since findMongoAndKillItDead is a very slow operation.
|
|
if (!self.suppressExitMessage) {
|
|
// Print the last 20 lines of stderr.
|
|
runLog.log(
|
|
stderr
|
|
.split('\n')
|
|
.slice(-20)
|
|
.join('\n') +
|
|
'Unexpected mongo exit code ' +
|
|
code +
|
|
(self.multiple ? '.' : '. Restarting.')
|
|
);
|
|
}
|
|
|
|
// If we're in multiple mode, we never try to restart. That's to keep the
|
|
// test-only multiple code simple.
|
|
if (self.multiple) {
|
|
self._fail();
|
|
return;
|
|
}
|
|
|
|
// We'll restart it up to 3 times in a row. The counter is reset
|
|
// when 5 seconds goes without a restart. (Note that by using a
|
|
// timer instead of looking at the current date, we avoid getting
|
|
// confused by time changes.)
|
|
self.errorCount++;
|
|
if (self.errorTimer) {
|
|
clearTimeout(self.errorTimer);
|
|
}
|
|
self.errorTimer = setTimeout(function() {
|
|
self.errorTimer = null;
|
|
self.errorCount = 0;
|
|
}, 5000);
|
|
|
|
if (self.errorCount < 3) {
|
|
// Wait a second, then restart.
|
|
self.restartTimer = await setTimeout(
|
|
fiberHelpers.bindEnvironment(async function() {
|
|
self.restartTimer = null;
|
|
await self._startOrRestart();
|
|
}),
|
|
1000
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Too many restarts, too quicky. It's dead. Print friendly
|
|
// diagnostics and give up.
|
|
var explanation = MongoExitCodes[code];
|
|
var message = "Can't start Mongo server.";
|
|
|
|
if (
|
|
explanation &&
|
|
explanation.symbol === 'EXIT_UNCAUGHT' &&
|
|
detectedErrors.freeSpace
|
|
) {
|
|
message +=
|
|
'\n\n' +
|
|
'Looks like you are out of free disk space under .meteor/local.';
|
|
} else if (explanation) {
|
|
message += '\n' + explanation.longText;
|
|
} else if (process.platform === 'win32') {
|
|
message +=
|
|
'\n\n' +
|
|
'Check how to troubleshoot here ' +
|
|
'https://docs.meteor.com/windows.html#cant-start-mongo-server';
|
|
}
|
|
|
|
if (explanation && explanation.symbol === 'EXIT_NET_ERROR') {
|
|
message +=
|
|
'\n\n' +
|
|
'Check for other processes listening on port ' +
|
|
self.port +
|
|
'\n' +
|
|
'or other Meteor instances running in the same project.';
|
|
}
|
|
|
|
if (!explanation && /GLIBC/i.test(stderr)) {
|
|
message +=
|
|
'\n\n' +
|
|
'Looks like you are trying to run Meteor on an old Linux distribution.\n' +
|
|
'Meteor on Linux requires glibc version 2.9 or above. Try upgrading your\n' +
|
|
'distribution to the latest version.';
|
|
}
|
|
|
|
if (detectedErrors.badLocale) {
|
|
message +=
|
|
'\n\n' +
|
|
"Looks like MongoDB doesn't understand your locale settings. See\n" +
|
|
'https://github.com/meteor/meteor/issues/4019 for more details.';
|
|
}
|
|
|
|
runLog.log(message);
|
|
self._fail();
|
|
},
|
|
|
|
// Idempotent
|
|
stop: function() {
|
|
var self = this;
|
|
|
|
if (self.shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
self.shuttingDown = true;
|
|
|
|
self.errorTimer && clearTimeout(self.errorTimer);
|
|
self.restartTimer && clearTimeout(self.restartTimer);
|
|
|
|
if (self.handle) {
|
|
self.handle.stop();
|
|
self.handle = null;
|
|
}
|
|
},
|
|
|
|
_allowStartupToReturn: function() {
|
|
var self = this;
|
|
if (self.resolveStartupPromise) {
|
|
var resolve = self.resolveStartupPromise;
|
|
self.resolveStartupPromise = null;
|
|
resolve();
|
|
}
|
|
},
|
|
|
|
_fail: function() {
|
|
var self = this;
|
|
self.stop();
|
|
self.onFailure && self.onFailure();
|
|
self._allowStartupToReturn();
|
|
},
|
|
|
|
_mongoHosts: function() {
|
|
var self = this;
|
|
var ports = [self.port];
|
|
if (self.multiple) {
|
|
ports.push(self.port + 1, self.port + 2);
|
|
}
|
|
return _.map(ports, function(port) {
|
|
return '127.0.0.1:' + port;
|
|
}).join(',');
|
|
},
|
|
|
|
mongoUrl: function() {
|
|
var self = this;
|
|
return 'mongodb://' + self._mongoHosts() + '/meteor';
|
|
},
|
|
|
|
oplogUrl: function() {
|
|
var self = this;
|
|
return 'mongodb://' + self._mongoHosts() + '/local';
|
|
},
|
|
});
|
|
|
|
exports.runMongoShell = runMongoShell;
|
|
exports.findMongoPort = findMongoPort;
|
|
exports.MongoRunner = MongoRunner;
|
|
exports.findMongoAndKillItDead = findMongoAndKillItDead;
|