root / HServer / 00.Server / 00.Program / node_modules / mongod / Mongod.js
이력 | 보기 | 이력해설 | 다운로드 (10.6 KB)
1 | 39 | HKM | 'use strict';
|
---|---|---|---|
2 | |||
3 | /**
|
||
4 | * Configuration options for {@link Mongod}.
|
||
5 | * @typedef {Object} Mongod~Config
|
||
6 | * @property {String} [bin=mongod]
|
||
7 | * @property {String} [config]
|
||
8 | * @property {(Number|String)} [port=27017]
|
||
9 | * @property {String} [dbpath]
|
||
10 | * @property {String} [storageEngine]
|
||
11 | * @property {Boolean} [nojournal=false]
|
||
12 | */
|
||
13 | |||
14 | /**
|
||
15 | * Invoked when an operation (i.e. {@link Mongod#open}) completes.
|
||
16 | * @callback Mongod~callback
|
||
17 | * @argument {Error} err
|
||
18 | */
|
||
19 | |||
20 | const childprocess = require('child_process');
|
||
21 | const events = require('events');
|
||
22 | const PromiseQueue = require('promise-queue');
|
||
23 | |||
24 | /**
|
||
25 | * A collection of regualar expressions used by {@link Mongod.parseData} to
|
||
26 | * parse stdout and stderr messages.
|
||
27 | * @see Mongod.parseData
|
||
28 | * @readonly
|
||
29 | * @private
|
||
30 | * @type {Object.<String,RegExp>}
|
||
31 | */
|
||
32 | const regExp = { |
||
33 | terminalMessage: /waiting\s+for\s+connections|already\s+in\s+use|denied|error|exception|badvalue/im, |
||
34 | whiteSpace: /\s/g, |
||
35 | newline: /\r?\n/ |
||
36 | }; |
||
37 | |||
38 | /**
|
||
39 | * Start and stop a local MongoDB server like a boss.
|
||
40 | * @class
|
||
41 | */
|
||
42 | class Mongod extends events.EventEmitter { |
||
43 | |||
44 | /**
|
||
45 | * Get a function that takes chunks of stdin data, aggregates it, and passes
|
||
46 | * it in complete lines, one by one, to a given {@link Mongod~callback}.
|
||
47 | * @argument {Mongod~callback} callback
|
||
48 | * @return {Function}
|
||
49 | */
|
||
50 | static getTextLineAggregator(callback) {
|
||
51 | let buffer = '';
|
||
52 | |||
53 | return (data) => {
|
||
54 | const fragments = data.toString().split(regExp.newline); |
||
55 | const lines = fragments.slice(0, fragments.length - 1); |
||
56 | |||
57 | // If there was an unended line in the previous dump, complete it by
|
||
58 | // the first section.
|
||
59 | lines[0] = buffer + lines[0]; |
||
60 | |||
61 | // If there is an unended line in this dump, store it to be completed by
|
||
62 | // the next. This assumes there will be a terminating newline character
|
||
63 | // at some point. Generally, this is a safe assumption.
|
||
64 | buffer = fragments[fragments.length - 1];
|
||
65 | |||
66 | for (let line of lines) {
|
||
67 | callback(line); |
||
68 | } |
||
69 | }; |
||
70 | } |
||
71 | |||
72 | /**
|
||
73 | * Populate a given {@link Mongod~Config} with values from a
|
||
74 | * given {@link Mongod~Config}.
|
||
75 | * @protected
|
||
76 | * @argument {Mongod~Config} source
|
||
77 | * @argument {Mongod~Config} target
|
||
78 | * @return {Mongod~Config}
|
||
79 | */
|
||
80 | static parseConfig(source, target) {
|
||
81 | if (target == null) { |
||
82 | target = Object.create(null);
|
||
83 | } |
||
84 | |||
85 | if (source == null) { |
||
86 | return target;
|
||
87 | } |
||
88 | |||
89 | if (typeof source === 'number' || typeof source === 'string') { |
||
90 | target.port = source; |
||
91 | |||
92 | return target;
|
||
93 | } |
||
94 | |||
95 | if (typeof source !== 'object') { |
||
96 | return target;
|
||
97 | } |
||
98 | |||
99 | if (source.bin != null) { |
||
100 | target.bin = source.bin; |
||
101 | } |
||
102 | |||
103 | if (source.conf != null) { |
||
104 | target.conf = source.conf; |
||
105 | |||
106 | return target;
|
||
107 | } |
||
108 | |||
109 | if (source.nojournal === true) { |
||
110 | target.nojournal = true;
|
||
111 | } |
||
112 | |||
113 | if (source.storageEngine != null) { |
||
114 | target.storageEngine = source.storageEngine; |
||
115 | } |
||
116 | |||
117 | if (source.dbpath != null) { |
||
118 | target.dbpath = source.dbpath; |
||
119 | } |
||
120 | |||
121 | if (source.port != null) { |
||
122 | target.port = source.port; |
||
123 | } |
||
124 | |||
125 | return target;
|
||
126 | } |
||
127 | |||
128 | /**
|
||
129 | * Parse process flags for MongoDB from a given {@link Mongod~Config}.
|
||
130 | * @protected
|
||
131 | * @argument {Mongod~Config} config
|
||
132 | * @return {Array.<String>}
|
||
133 | */
|
||
134 | static parseFlags(config) {
|
||
135 | if (config.conf != null) { |
||
136 | return ['--config', config.conf]; |
||
137 | } |
||
138 | |||
139 | const flags = []; |
||
140 | |||
141 | if (config.nojournal) {
|
||
142 | flags.push('--nojournal');
|
||
143 | } |
||
144 | |||
145 | if (config.storageEngine != null) { |
||
146 | flags.push('--storageEngine', config.storageEngine);
|
||
147 | } |
||
148 | |||
149 | if (config.dbpath != null) { |
||
150 | flags.push('--dbpath', config.dbpath);
|
||
151 | } |
||
152 | |||
153 | if (config.port != null) { |
||
154 | flags.push('--port', config.port);
|
||
155 | } |
||
156 | |||
157 | return flags;
|
||
158 | } |
||
159 | |||
160 | /**
|
||
161 | * Parse MongoDB server output for terminal messages.
|
||
162 | * @protected
|
||
163 | * @argument {String} string
|
||
164 | * @return {Object}
|
||
165 | */
|
||
166 | static parseData(string) {
|
||
167 | const matches = regExp.terminalMessage.exec(string); |
||
168 | |||
169 | if (matches === null) { |
||
170 | return null; |
||
171 | } |
||
172 | |||
173 | const result = { |
||
174 | err: null, |
||
175 | key: matches
|
||
176 | .pop() |
||
177 | .replace(regExp.whiteSpace, '')
|
||
178 | .toLowerCase() |
||
179 | }; |
||
180 | |||
181 | switch (result.key) {
|
||
182 | case 'waitingforconnections': |
||
183 | break;
|
||
184 | |||
185 | case 'alreadyinuse': |
||
186 | result.err = new Error('Address already in use'); |
||
187 | result.err.code = -1;
|
||
188 | |||
189 | break;
|
||
190 | |||
191 | case 'denied': |
||
192 | result.err = new Error('Permission denied'); |
||
193 | result.err.code = -2;
|
||
194 | |||
195 | break;
|
||
196 | |||
197 | case 'error': |
||
198 | case 'exception': |
||
199 | case 'badvalue': |
||
200 | result.err = new Error(string.trim());
|
||
201 | result.err.code = -3;
|
||
202 | |||
203 | break;
|
||
204 | } |
||
205 | |||
206 | return result;
|
||
207 | } |
||
208 | |||
209 | /**
|
||
210 | * Start a given {@link Mongod}.
|
||
211 | * @protected
|
||
212 | * @argument {Mongod} server
|
||
213 | * @return {Promise}
|
||
214 | */
|
||
215 | static open(server) {
|
||
216 | if (server.isOpening) {
|
||
217 | return server.openPromise;
|
||
218 | } |
||
219 | |||
220 | server.isOpening = true;
|
||
221 | server.isClosing = false;
|
||
222 | server.openPromise = server.promiseQueue.add(() => { |
||
223 | if (server.isClosing || server.isRunning) {
|
||
224 | server.isOpening = false;
|
||
225 | |||
226 | return Promise.resolve(null); |
||
227 | } |
||
228 | |||
229 | return new Promise((resolve, reject) => { |
||
230 | /**
|
||
231 | * A listener for the current server process' stdout/stderr that
|
||
232 | * resolves or rejects the current {@link Promise} when done.
|
||
233 | * @see Mongod.getTextLineAggregator
|
||
234 | * @see Mongod.parseData
|
||
235 | * @argument {Buffer} buffer
|
||
236 | * @return {undefined}
|
||
237 | */
|
||
238 | const dataListener = Mongod.getTextLineAggregator((value) => { |
||
239 | const result = Mongod.parseData(value); |
||
240 | |||
241 | if (result === null) { |
||
242 | return;
|
||
243 | } |
||
244 | |||
245 | server.process.stdout.removeListener('data', dataListener);
|
||
246 | |||
247 | server.isOpening = false;
|
||
248 | |||
249 | if (result.err === null) { |
||
250 | server.isRunning = true;
|
||
251 | |||
252 | server.emit('open');
|
||
253 | resolve(null);
|
||
254 | } |
||
255 | else {
|
||
256 | server.isClosing = true;
|
||
257 | |||
258 | server.emit('closing');
|
||
259 | server.process.once('close', () => reject(result.err));
|
||
260 | } |
||
261 | }); |
||
262 | |||
263 | /**
|
||
264 | * A listener to close the server when the current process exits.
|
||
265 | * @return {undefined}
|
||
266 | */
|
||
267 | const exitListener = () => { |
||
268 | // istanbul ignore next
|
||
269 | server.close(); |
||
270 | }; |
||
271 | |||
272 | /**
|
||
273 | * Get a text line aggregator that emits a given {@linkcode event}
|
||
274 | * for the current server.
|
||
275 | * @see Mongod.getTextLineAggregator
|
||
276 | * @argument {String} event
|
||
277 | * @return {Function}
|
||
278 | */
|
||
279 | const getDataPropagator = (event) => |
||
280 | Mongod.getTextLineAggregator((line) => server.emit(event, line)); |
||
281 | |||
282 | server.emit('opening');
|
||
283 | |||
284 | server.process = childprocess.spawn( |
||
285 | server.config.bin, |
||
286 | Mongod.parseFlags(server.config) |
||
287 | ); |
||
288 | |||
289 | server.process.stderr.on('data', dataListener);
|
||
290 | server.process.stderr.on('data', getDataPropagator('stdout')); |
||
291 | server.process.stdout.on('data', dataListener);
|
||
292 | server.process.stdout.on('data', getDataPropagator('stdout')); |
||
293 | server.process.on('close', () => {
|
||
294 | server.process = null;
|
||
295 | server.isRunning = false;
|
||
296 | server.isClosing = false;
|
||
297 | |||
298 | process.removeListener('exit', exitListener);
|
||
299 | server.emit('close');
|
||
300 | }); |
||
301 | process.on('exit', exitListener);
|
||
302 | }); |
||
303 | }); |
||
304 | |||
305 | return server.openPromise;
|
||
306 | } |
||
307 | |||
308 | /**
|
||
309 | * Stop a given {@link Mongod}.
|
||
310 | * @protected
|
||
311 | * @argument {Mongod} server
|
||
312 | * @return {Promise}
|
||
313 | */
|
||
314 | static close(server) {
|
||
315 | if (server.isClosing) {
|
||
316 | return server.closePromise;
|
||
317 | } |
||
318 | |||
319 | server.isClosing = true;
|
||
320 | server.isOpening = false;
|
||
321 | server.closePromise = server.promiseQueue.add(() => { |
||
322 | if (server.isOpening || !server.isRunning) {
|
||
323 | server.isClosing = false;
|
||
324 | |||
325 | return Promise.resolve(null); |
||
326 | } |
||
327 | |||
328 | return new Promise((resolve) => { |
||
329 | server.emit('closing');
|
||
330 | server.process.once('close', () => resolve(null)); |
||
331 | server.process.kill(); |
||
332 | }); |
||
333 | }); |
||
334 | |||
335 | return server.closePromise;
|
||
336 | } |
||
337 | |||
338 | /**
|
||
339 | * Construct a new {@link Mongod}.
|
||
340 | * @argument {(Number|String|Mongod~Config)} [configOrPort]
|
||
341 | * A number or string that is a port or an object for configuration.
|
||
342 | */
|
||
343 | constructor(configOrPort) { |
||
344 | super();
|
||
345 | |||
346 | /**
|
||
347 | * Configuration options.
|
||
348 | * @protected
|
||
349 | * @type {Mongod~Config}
|
||
350 | */
|
||
351 | this.config = Mongod.parseConfig(configOrPort, {
|
||
352 | bin: 'mongod', |
||
353 | conf: null, |
||
354 | port: 27017, |
||
355 | dbpath: null, |
||
356 | storageEngine: null, |
||
357 | nojournal: false |
||
358 | }); |
||
359 | |||
360 | /**
|
||
361 | * The current process.
|
||
362 | * @protected
|
||
363 | * @type {ChildProcess}
|
||
364 | */
|
||
365 | this.process = null; |
||
366 | |||
367 | /**
|
||
368 | * The last {@link Promise} returned by {@link Mongod#open}.
|
||
369 | * @protected
|
||
370 | * @type {Promise}
|
||
371 | */
|
||
372 | this.openPromise = Promise.resolve(null); |
||
373 | |||
374 | /**
|
||
375 | * The last {@link Promise} returned by {@link Mongod#close}.
|
||
376 | * @protected
|
||
377 | * @type {Promise}
|
||
378 | */
|
||
379 | this.closePromise = Promise.resolve(null); |
||
380 | |||
381 | /**
|
||
382 | * A serial queue of open and close promises.
|
||
383 | * @protected
|
||
384 | * @type {PromiseQueue}
|
||
385 | */
|
||
386 | this.promiseQueue = new PromiseQueue(1); |
||
387 | |||
388 | /**
|
||
389 | * Determine if the instance is closing a MongoDB server; {@linkcode true}
|
||
390 | * while a process is being, or about to be, killed until the
|
||
391 | * contained MongoDB server either closes or errs.
|
||
392 | * @readonly
|
||
393 | * @type {Boolean}
|
||
394 | */
|
||
395 | this.isClosing = false; |
||
396 | |||
397 | /**
|
||
398 | * Determine if the instance is starting a MongoDB server; {@linkcode true}
|
||
399 | * while a process is spawning, or about tobe spawned, until the
|
||
400 | * contained MongoDB server either starts or errs.
|
||
401 | * @readonly
|
||
402 | * @type {Boolean}
|
||
403 | */
|
||
404 | this.isRunning = false; |
||
405 | |||
406 | /**
|
||
407 | * Determine if the instance is running a MongoDB server; {@linkcode true}
|
||
408 | * once a process has spawned and the contained MongoDB server is ready
|
||
409 | * to service requests.
|
||
410 | * @readonly
|
||
411 | * @type {Boolean}
|
||
412 | */
|
||
413 | this.isOpening = false; |
||
414 | } |
||
415 | |||
416 | /**
|
||
417 | * Open the server.
|
||
418 | * @argument {Mongod~callback} [callback]
|
||
419 | * @return {Promise}
|
||
420 | */
|
||
421 | open(callback) { |
||
422 | const promise = Mongod.open(this);
|
||
423 | |||
424 | return typeof callback === 'function' |
||
425 | ? promise |
||
426 | .then((v) => callback(null, v))
|
||
427 | .catch((e) => callback(e, null)) |
||
428 | : promise; |
||
429 | } |
||
430 | |||
431 | /**
|
||
432 | * Close the server.
|
||
433 | * @argument {Mongod~callback} [callback]
|
||
434 | * @return {Promise}
|
||
435 | */
|
||
436 | close(callback) { |
||
437 | const promise = Mongod.close(this);
|
||
438 | |||
439 | return typeof callback === 'function' |
||
440 | ? promise |
||
441 | .then((v) => callback(null, v))
|
||
442 | .catch((e) => callback(e, null)) |
||
443 | : promise; |
||
444 | } |
||
445 | } |
||
446 | |||
447 | module.exports = exports = Mongod; |