forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mongo_runner.js
314 lines (277 loc) · 9.83 KB
/
mongo_runner.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
var fs = require("fs");
var path = require("path");
var files = require('./files.js');
var _ = require('underscore');
var unipackage = require('./unipackage.js');
var Fiber = require('fibers');
/** Internal.
*
* If passed, app_dir and port act as filters on the list of running mongos.
*
* callback is called with (err, [{pid, port, app_dir}])
*/
var find_mongo_pids = function (app_dir, port, callback) {
// 'ps ax' should be standard across all MacOS and Linux.
var child_process = require('child_process');
child_process.exec('ps ax',
function (error, stdout, stderr) {
if (error) {
callback({reason: error});
} else {
var pids = [];
_.each(stdout.split('\n'), function (ps_line) {
// matches mongos we start.
var m = ps_line.match(/^\s*(\d ). mongod . --port (\d ) --dbpath (. )(?:\/|\\)\.meteor(?:\/|\\)local(?:\/|\\)db(?: |$)/);
if (m && m.length === 4) {
var found_pid = parseInt(m[1]);
var found_port = parseInt(m[2]);
var found_path = m[3];
if ( (!port || port === found_port) &&
(!app_dir || app_dir === found_path)) {
pids.push({
pid: found_pid, port: found_port, app_dir: found_path});
}
}
});
callback(null, pids);
}
});
};
// See if mongo is running already. Callback takes a single argument,
// 'port', which is the port mongo is running on or null if mongo is not
// running.
exports.find_mongo_port = function (app_dir, callback) {
find_mongo_pids(app_dir, null, function (err, pids) {
if (err || pids.length !== 1) {
callback(null);
return;
}
var pid = pids[0].pid;
try {
process.kill(pid, 0); // make sure it is still alive
} catch (e) {
callback(null);
return;
}
callback(pids[0].port);
});
}
// Try to kill any other mongos running on our port. Calls callback
// once they are all gone. Callback takes one arg: err (falsy means all
// good).
//
// This is a big hammer for dealing with still running mongos, but
// smaller hammers have failed before and it is getting tiresome.
var find_mongo_and_kill_it_dead = function (port, callback) {
find_mongo_pids(null, port, function (err, pids) {
if (err) {
callback(err);
return;
}
if (pids.length) {
// Send kill attempts and wait. First a SIGINT, then if it isn't
// dead within 2 sec, SIGKILL. This goes through the list
// serially, but thats OK because there really should only ever be
// one.
var attempts = 0;
var dead_yet = function () {
attempts = attempts 1;
var pid = pids[0].pid;
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. remove this pid from the list.
pids.shift();
// if no more in the list, we're done!
if (!pids.length) {
callback();
return;
}
}
if (attempts === 40) {
// give up after 4 seconds.
callback({
reason: "Can't kill running mongo (pid " pid ")."});
return;
}
// recurse
setTimeout(dead_yet, 100);
};
dead_yet();
} else {
// nothing to kill, fire OK callback
callback();
}
});
};
exports.launchMongo = function (options) {
var handle = {stop: function (callback) { callback(); } };
var onListen = options.onListen || function () {};
var onExit = options.onExit || function () {};
// If we are passed an external mongo, assume it is launched and never
// exits. Matches code in run.js:exports.run.
// Since it is externally managed, asking it to actually stop would be
// impolite, so our stoppable handle is a noop
if (process.env.MONGO_URL) {
onListen();
return handle;
}
var mongod_path = path.join(files.get_dev_bundle(),
'mongodb',
'bin',
'mongod');
// store data in app_dir
var dbPath = path.join(options.context.appDir, '.meteor', 'local', 'db');
files.mkdir_p(dbPath, 0755);
// add .gitignore if needed.
files.add_to_gitignore(path.join(options.context.appDir, '.meteor'), 'local');
find_mongo_and_kill_it_dead(options.port, function (err) {
Fiber(function (){
if (err) {
// XXX this was being passed to onListen and ignored before. should do
// something better.
throw {reason: "Can't kill running mongo: " err.reason};
}
var portFile = path.join(dbPath, 'METEOR-PORT');
var portFileExists = false;
var createReplSet = true;
try {
createReplSet = (fs.readFileSync(portFile)) !== options.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 destroying 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.)
if (createReplSet) {
// Delete the port file, so we don't mistakenly believe that the DB is
// still configured.
if (portFileExists)
fs.unlinkSync(portFile);
try {
var dbFiles = fs.readdirSync(dbPath);
} catch (e) {
if (!e || e.code !== 'ENOENT')
throw e;
}
_.each(dbFiles, function (dbFile) {
if (/^local\./.test(dbFile))
fs.unlinkSync(path.join(dbPath, dbFile));
});
// Load mongo-livedata so we'll be able to talk to it.
var mongoNpmModule = unipackage.load({
library: options.context.library,
packages: [ 'mongo-livedata' ],
release: options.context.releaseVersion
})['mongo-livedata'].MongoInternals.NpmModule;
}
// Start mongod with a dummy replSet and wait for it to listen.
var child_process = require('child_process');
var replSetName = 'meteor';
var proc = child_process.spawn(mongod_path, [
// nb: cli-test.sh and find_mongo_pids make strong assumptions about the
// order of the arguments! Check them before changing any arguments.
'--bind_ip', '127.0.0.1',
'--smallfiles',
'--nohttpinterface',
'--port', options.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
]);
var stderrOutput = '';
proc.stderr.setEncoding('utf8');
proc.stderr.on('data', function (data) {
stderrOutput = data;
});
var callOnExit = function (code, signal) {
onExit(code, signal, stderrOutput);
};
proc.on('exit', callOnExit);
handle.stop = function (callback) {
var tries = 0;
var exited = false;
proc.removeListener('exit', callOnExit);
proc.kill('SIGINT');
callback && callback(err);
};
proc.stdout.setEncoding('utf8');
var listening = false;
var replSetReady = false;
var replSetReadyToBeInitiated = false;
var alreadyInitiatedReplSet = false;
var alreadyCalledOnListen = false;
var maybeCallOnListen = function () {
if (listening && replSetReady && !alreadyCalledOnListen) {
if (createReplSet)
fs.writeFileSync(portFile, options.port);
alreadyCalledOnListen = true;
onListen();
}
};
var maybeInitiateReplset = function () {
// We need to want to create a replset, be confident that the server is
// listening, be confident that the server's replset implementation is
// ready to be initiated, and have not already done it.
if (!(createReplSet && listening && replSetReadyToBeInitiated
&& !alreadyInitiatedReplSet)) {
return;
}
alreadyInitiatedReplSet = true;
// Connect to it and start a replset.
var db = new mongoNpmModule.Db(
'meteor', new mongoNpmModule.Server('127.0.0.1', options.port),
{safe: true});
db.open(function(err, db) {
if (err)
throw err;
db.admin().command({
replSetInitiate: {
_id: replSetName,
members: [{_id : 0, host: '127.0.0.1:' options.port}]
}
}, function (err, result) {
if (err)
throw err;
// why this isn't in the error is unclear.
if (result && result.documents && result.documents[0]
&& result.documents[0].errmsg) {
throw result.document[0].errmsg;
}
db.close(true);
});
});
};
proc.stdout.on('data', function (data) {
// note: don't use "else ifs" in this, because 'data' can have multiple
// lines
if (/config from self or any seed \(EMPTYCONFIG\)/.test(data)) {
replSetReadyToBeInitiated = true;
maybeInitiateReplset();
}
if (/ \[initandlisten\] waiting for connections on port/.test(data)) {
listening = true;
maybeInitiateReplset();
maybeCallOnListen();
}
if (/ \[rsMgr\] replSet PRIMARY/.test(data)) {
replSetReady = true;
maybeCallOnListen();
}
});
}).run();
});
return handle;
};