1 | #!/usr/bin/env node
|
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 | var _ = require('underscore');
|
28 | var fs = require('fs');
|
29 | var os = require('os');
|
30 | var util = require('util');
|
31 | var events = require('events');
|
32 |
|
33 | var http = require('http');
|
34 | var https = require('https');
|
35 | var connect = require('connect');
|
36 | var express = require('express');
|
37 | var socketio = require("socket.io");
|
38 | var path = require('path');
|
39 | var UUID = require('node-uuid');
|
40 |
|
41 | var zutils = require('zetta-utils');
|
42 | var zstats = require('zetta-stats');
|
43 | var zrpc = require('zetta-rpc');
|
44 | var zlogin = require('zetta-login');
|
45 | var exec = require('child_process').exec;
|
46 | var getmac = require('getmac');
|
47 | var nodemailer = require('nodemailer');
|
48 | var mongo = require('mongodb');
|
49 | var os = require('os');
|
50 | var child_process = require('child_process');
|
51 | var Translator = require('zetta-translator');
|
52 |
|
53 | var _cl = console.log;
|
54 | console.log = function() {
|
55 | var args = Array.prototype.slice.call(arguments, 0);
|
56 | args.unshift(zutils.tsString()+' ');
|
57 | return _cl.apply(console, args);
|
58 | }
|
59 |
|
60 | function getConfig(name) {
|
61 |
|
62 | var filename = name+'.conf';
|
63 | var host_filename = name+'.'+os.hostname()+'.conf';
|
64 | var local_filename = name+'.local.conf';
|
65 |
|
66 | var data = [ ];
|
67 |
|
68 | fs.existsSync(filename) && data.push(fs.readFileSync(filename) || null);
|
69 | fs.existsSync(host_filename) && data.push(fs.readFileSync(host_filename) || null);
|
70 | fs.existsSync(local_filename) && data.push(fs.readFileSync(local_filename) || null);
|
71 |
|
72 | if(!data[0] && !data[1])
|
73 | throw new Error("Unable to read config file:"+(filename+'').magenta.bold)
|
74 | function merge(dst, src) {
|
75 | _.each(src, function(v, k) {
|
76 | if(_.isArray(v)) { dst[k] = [ ]; merge(dst[k], v); }
|
77 |
|
78 | else if(_.isObject(v)) { if(!dst[k]) dst[k] = { }; merge(dst[k], v); }
|
79 | else { if(_.isArray(src)) dst.push(v); else dst[k] = v; }
|
80 | })
|
81 | }
|
82 |
|
83 | var o = { }
|
84 | _.each(data, function(conf) {
|
85 | if(!conf || !conf.toString('utf-8').length)
|
86 | return;
|
87 | var layer = eval('('+conf.toString('utf-8')+')');
|
88 | merge(o, layer);
|
89 | })
|
90 |
|
91 | return o;
|
92 | }
|
93 |
|
94 | function readJSON(filename) {
|
95 | if(!fs.existsSync(filename))
|
96 | return undefined;
|
97 | var text = fs.readFileSync(filename, { encoding : 'utf-8' });
|
98 | if(!text)
|
99 | return undefined;
|
100 | try {
|
101 | return JSON.parse(text);
|
102 | } catch(ex) {
|
103 | console.log("Error parsing file:",filename);
|
104 | console.log(ex);
|
105 | console.log('Offending content follows:',text);
|
106 | }
|
107 | return undefined;
|
108 | }
|
109 |
|
110 | function writeJSON(filename, data) {
|
111 | fs.writeFileSync(filename, JSON.stringify(data, null, '\t'));
|
112 | }
|
113 |
|
114 | function Application(appFolder, appConfig) {
|
115 | var self = this;
|
116 | events.EventEmitter.call(this);
|
117 |
|
118 | self.appFolder = appFolder;
|
119 |
|
120 | self.pkg = readJSON(path.join(appFolder,'package.json'));
|
121 | if(!self.pkg)
|
122 | throw new Error("Application Unable to read package.json");
|
123 |
|
124 | if(!self.pkg.name)
|
125 | throw new Error("package.json must contain module 'name' field");
|
126 |
|
127 | self.getConfig = function(name) { return getConfig(path.join(appFolder,'config', name)) }
|
128 | self.readJSON = readJSON;
|
129 | self.writeJSON = writeJSON;
|
130 |
|
131 | self.config = self.getConfig(self.pkg.name);
|
132 |
|
133 | self.settings = { }
|
134 |
|
135 | http.globalAgent.maxSockets = self.config.maxHttpSockets || 1024;
|
136 | https.globalAgent.maxSockets = self.config.maxHttpSockets || 1024;
|
137 | if(process.platform != 'win32' && self.config.maxSockets) {
|
138 | if(fs.existsSync('node_modules/posix')) {
|
139 | try { require('posix').setrlimit('nofile', self.config.maxSockets); } catch(ex) {
|
140 | console.error(ex.stack);
|
141 | }
|
142 | }
|
143 | else
|
144 | console.log("WARNING - Please install POSIX module (npm install posix)".red.bold);
|
145 | }
|
146 |
|
147 | self.pingDataObject = { }
|
148 |
|
149 |
|
150 |
|
151 | self.restoreDefaultSettings = function(name, force) {
|
152 | var filename = path.join(self.appFolder,'config', name+'.settings');
|
153 | if(!fs.existsSync(filename)) {
|
154 | self.settings = { }
|
155 | return;
|
156 | }
|
157 | var data = fs.readFileSync(filename);
|
158 | self.settings = eval('('+data.toString('utf-8')+')');
|
159 | }
|
160 |
|
161 |
|
162 | self.restoreSettings = function(name) {
|
163 | self.restoreDefaultSettings(name);
|
164 |
|
165 | var host_filename = path.join(self.appFolder,'config', name+'.'+os.hostname().toLowerCase()+'.settings');
|
166 | if(!fs.existsSync(host_filename))
|
167 | return;
|
168 | var data = fs.readFileSync(host_filename);
|
169 | var settings = eval('('+data.toString('utf-8')+')');
|
170 | _.each(settings, function(o, key) {
|
171 | if(self.settings[key])
|
172 | self.settings[key].value = o.value;
|
173 | })
|
174 | }
|
175 |
|
176 | self.storeSettings = function(name) {
|
177 | var host_filename = path.join(self.appFolder,'config', name+'.'+os.hostname().toLowerCase()+'.settings');
|
178 | fs.writeFileSync(host_filename, JSON.stringify(self.settings, null, '\t'));
|
179 | }
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | self.initTranslator = function(callback) {
|
185 | if(self.config.translator) {
|
186 | var options = {
|
187 | storagePath: path.join(appFolder,'config'),
|
188 | rootFolderPath: appFolder
|
189 | };
|
190 | options = _.extend(self.config.translator, options);
|
191 |
|
192 | self.translator = new Translator(options, function() {
|
193 | self.translator.separateEditor();
|
194 | });
|
195 | }
|
196 |
|
197 | callback();
|
198 | }
|
199 |
|
200 | self.initCertificates = function(callback) {
|
201 | if(self.verbose)
|
202 | console.log('zetta-app: loading certificates from ',appFolder+'/'+self.config.certificates);
|
203 | if(self.certificates)
|
204 | callback && callback();
|
205 |
|
206 | self.certificates = {
|
207 | key: fs.readFileSync(path.join(appFolder,self.config.certificates)+'.key').toString(),
|
208 | cert: fs.readFileSync(path.join(appFolder,self.config.certificates)+'.crt').toString(),
|
209 | ca: [ ]
|
210 | }
|
211 |
|
212 | |
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | callback && callback();
|
224 | }
|
225 |
|
226 |
|
227 | self.initMonitoringInterfaces = function(callback) {
|
228 | self.stats = new zstats.StatsD(self.config.statsd, self.uuid, self.pkg.name);
|
229 |
|
230 | self.profiler = new zstats.Profiler(self.stats);
|
231 | self.monitor = new zstats.Monitor(self.stats, self.config.monitor);
|
232 |
|
233 | callback();
|
234 | }
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 | self.initMailer = function(callback) {
|
248 |
|
249 | var pickupFolder = path.join(self.appFolder,"mailer");
|
250 |
|
251 | if(self.config.mailer.pickup) {
|
252 | self.mailer = nodemailer.createTransport("PICKUP", {
|
253 | directory: pickupFolder
|
254 | })
|
255 | }
|
256 | else
|
257 | {
|
258 | self.mailer = nodemailer.createTransport("SMTP", {
|
259 | service: self.config.mailer.service,
|
260 |
|
261 | auth: {
|
262 | user: core.config.mailer.auth.user,
|
263 | pass: core.config.mailer.auth.password
|
264 | },
|
265 | debug: core.config.mailer.debug || false
|
266 | })
|
267 | }
|
268 |
|
269 | callback();
|
270 | }
|
271 |
|
272 | self.initDatabaseConfig = function(callback) {
|
273 |
|
274 | var dbconf = self.databaseConfig;
|
275 | console.log("Connecting to database...".bold);
|
276 |
|
277 | self.db = { }
|
278 | self.databases = { }
|
279 |
|
280 | initDatabaseConnection();
|
281 |
|
282 | function initDatabaseConnection() {
|
283 | var config = dbconf.shift();
|
284 | if (!config)
|
285 | return callback();
|
286 |
|
287 | var name = config.config;
|
288 | var db = self.config.mongodb[name];
|
289 |
|
290 | if(!db)
|
291 | throw new Error("Config missing database configuration for '"+name+"'");
|
292 |
|
293 | mongo.Db.connect(db, function (err, database) {
|
294 | if (err)
|
295 | return callback(err);
|
296 |
|
297 | self.databases[name] = database;
|
298 |
|
299 | console.log("DB '" + (name) + "' connected", self.config.mongodb[name].bold);
|
300 | zutils.bind_database_config(database, config.collections, function (err, db) {
|
301 | if (err)
|
302 | return callback(err);
|
303 | _.extend(self.db, db);
|
304 | initDatabaseConnection();
|
305 | })
|
306 | })
|
307 | }
|
308 | }
|
309 |
|
310 | self.initDatabaseCollections = function(callback) {
|
311 | console.log("Connecting to database...".bold);
|
312 |
|
313 | self.db = { }
|
314 | self.databases = { }
|
315 |
|
316 | mongo.Db.connect(self.config.mongodb, function (err, database) {
|
317 | if (err)
|
318 | return callback(err);
|
319 |
|
320 | self.database = database;
|
321 |
|
322 | console.log("Database connected", self.config.mongodb);
|
323 | zutils.bind_database_config(database, self.databaseCollections, function (err, db) {
|
324 | if (err)
|
325 | return callback(err);
|
326 | _.extend(self.db, db);
|
327 | callback();
|
328 | })
|
329 | })
|
330 | }
|
331 |
|
332 |
|
333 | self.initExpressConfig = function(callback) {
|
334 | var ExpressSession = require('express-session');
|
335 |
|
336 | self.app = express();
|
337 |
|
338 | self.app.set('views', path.join(appFolder,'views'));
|
339 | self.app.set('view engine', 'ejs');
|
340 | self.app.engine('html', require('ejs').renderFile);
|
341 | self.app.use(require('body-parser')());
|
342 | self.app.use(require('method-override')());
|
343 | self.app.use(require('cookie-parser')(self.config.http.session.secret));
|
344 |
|
345 | if(self.config.mongodb) {
|
346 | var MongoStore = require('connect-mongo')(ExpressSession);
|
347 | self.app.sessionStore = new MongoStore({url: self.config.mongodb.main || self.config.mongodb}, function() {
|
348 | self.app.use(ExpressSession({
|
349 | secret: self.config.http.session.secret,
|
350 | key: self.config.http.session.key,
|
351 | cookie: self.config.http.session.cookie,
|
352 | store: self.app.sessionStore
|
353 | }));
|
354 |
|
355 | return callback();
|
356 | });
|
357 | }
|
358 | else
|
359 | if(self.config.http && self.config.http.session) {
|
360 | self.app.sessionSecret = self.config.http.session.secret;
|
361 | var CookieSession = require('cookie-session');
|
362 | self.app.use(CookieSession({
|
363 | secret: self.config.http.session.secret,
|
364 | key: self.config.http.session.key,
|
365 | }));
|
366 |
|
367 | return callback();
|
368 | }
|
369 | }
|
370 |
|
371 |
|
372 | self.initExpressHandlers = function(callback) {
|
373 | var ErrorHandler = require('errorhandler')();
|
374 |
|
375 | var isErrorView = fs.existsSync(path.join(self.appFolder,'views','error.ejs'));
|
376 | self.handleHttpError = function(response, req, res, next) {
|
377 | if(req.xhr) {
|
378 | res.json({errors: _.isArray(response.errors) ? response.errors : [response.errors]});
|
379 | return;
|
380 | }
|
381 | else
|
382 | if(isErrorView) {
|
383 | res.render('error', { error : error });
|
384 | return;
|
385 | }
|
386 | else {
|
387 | res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
388 | res.end("Server Error");
|
389 | return;
|
390 | }
|
391 | }
|
392 |
|
393 |
|
394 | if(self.config.translator)
|
395 | self.app.use(self.translator.useSession);
|
396 |
|
397 |
|
398 | |
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 | function HttpError(response) {
|
406 | res.status(response.status);
|
407 | self.handleHttpError(respo)
|
408 | }
|
409 |
|
410 | self.app.use(function(req, res, next) {
|
411 | res.sendHttpError = function (response) {
|
412 | self.handleHttpError(response, req, res, next);
|
413 | }
|
414 |
|
415 | next();
|
416 | })
|
417 |
|
418 | var loginConfig = self.config.http.login;
|
419 | if(loginConfig && loginConfig.authenticator) {
|
420 | switch(loginConfig.authenticator.type) {
|
421 | case 'basic' : {
|
422 | console.log("Enabling basic authenticator".bold);
|
423 | self.authenticator = new zlogin.BasicAuthenticator(self, loginConfig.authenticator);
|
424 | self.login = new zlogin.Login(self, self.authenticator, loginConfig);
|
425 | self.login.init(self.app);
|
426 | } break;
|
427 | }
|
428 | }
|
429 |
|
430 | if(self.router)
|
431 | self.router.init(self.app);
|
432 |
|
433 | self.emit('init::express', self.app);
|
434 |
|
435 | if(self.config.http.static) {
|
436 | var ServeStatic = require('serve-static');
|
437 | _.each(self.config.http.static, function(dst, src) {
|
438 | console.log('HTTP serving '+src.cyan.bold+' -> '+dst.cyan.bold);
|
439 | self.app.use(src, ServeStatic(path.join(appFolder, dst)));
|
440 | })
|
441 | }
|
442 |
|
443 | |
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 | self.app.use(function (err, req, res, next) {
|
454 | if (typeof err == 'number') {
|
455 | err = {
|
456 | status: err,
|
457 | errors: http.STATUS_CODES[err] || "Error"
|
458 | };
|
459 | }
|
460 | else
|
461 | if (typeof err == 'string') {
|
462 | console.error(err);
|
463 | err = {
|
464 | status: 500,
|
465 | errors: 'Internal Server Error'
|
466 | };
|
467 | }
|
468 | else
|
469 | if (err instanceof Error) {
|
470 | if (self.config.development) {
|
471 | err.status = 500;
|
472 | return ErrorHandler(err, req, res, next);
|
473 | }
|
474 | else
|
475 | {
|
476 | console.error(err.stack);
|
477 | err = {
|
478 | status: 500,
|
479 | errors: 'Internal Server Error'
|
480 | };
|
481 | }
|
482 | }
|
483 |
|
484 | res.sendHttpError(err);
|
485 | });
|
486 |
|
487 | finish();
|
488 |
|
489 | function finish() {
|
490 | callback();
|
491 | }
|
492 |
|
493 |
|
494 | };
|
495 |
|
496 | self.initHttpServer = function(callback) {
|
497 |
|
498 | var CERTIFICATES = (self.config.http.ssl && self.config.certificates) ? self.certificates : null;
|
499 |
|
500 | var https_server = CERTIFICATES ? https.createServer(CERTIFICATES, self.app) : http.createServer(self.app);
|
501 | self.io = socketio.listen(https_server, { 'log level': 0, 'secure': CERTIFICATES ? true : false });
|
502 | if(self.router && self.router.initWebSocket)
|
503 | self.router.initWebSocket(self.io);
|
504 | self.config.websocket && self.initWebsocket(function(){});
|
505 | self.emit('init::websockets');
|
506 | self.emit('init::http::done');
|
507 | https_server.listen(self.config.http.port, function (err) {
|
508 | if (err) {
|
509 | console.error("Unable to start HTTP(S) server on port" + self.config.http.port);
|
510 | return callback(err);
|
511 | }
|
512 |
|
513 | console.log('HTTP server listening on port ' + (self.config.http.port+'').bold);
|
514 |
|
515 | if (!CERTIFICATES)
|
516 | console.log(("WARNING - SSL is currently disabled").magenta.bold);
|
517 |
|
518 | if (self.config.secure_under_username) {
|
519 | console.log("Securing run-time to user '" + self.config.secure_under_username + "'");
|
520 | zutils.secure_under_username(self.config.secure_under_username);
|
521 | }
|
522 |
|
523 | self.emit('init::http-server')
|
524 | callback();
|
525 | });
|
526 | };
|
527 |
|
528 | self.initSupervisors = function(callback) {
|
529 |
|
530 | if(!self.certificates)
|
531 | throw new Error("Application supervisor requires configured certificates");
|
532 | console.log("Connecting to supervisor(s)...".bold, self.config.supervisor.address);
|
533 | self.supervisor = new zrpc.Client({
|
534 | address: self.config.supervisor.address,
|
535 | auth: self.config.supervisor.auth,
|
536 | certificates: self.certificates,
|
537 | node: self.mac,
|
538 | mac: self.mac,
|
539 | uuid : self.uuid,
|
540 | designation: self.pkg.name,
|
541 | pingDataObject : self.pingDataObject
|
542 | });
|
543 | self.supervisor.registerListener(self);
|
544 | callback();
|
545 |
|
546 | self.on('package::info::get', function(msg) {
|
547 | console.log(msg.op.yellow.bold);
|
548 | self.supervisor.dispatch({ op : 'package::info::data', uuid : self.uuid, pkg : self.pkg })
|
549 | })
|
550 | }
|
551 |
|
552 | self.initWebsocket = function(callback) {
|
553 | self.webSocketMap = [ ]
|
554 | self.webSockets = self.io.of(self.config.websocket.path).on('connection', function(socket) {
|
555 | console.log("websocket "+socket.id+" connected");
|
556 | self.emit('websocket::connect', socket);
|
557 | self.webSocketMap[socket.id] = socket;
|
558 | socket.on('disconnect', function() {
|
559 | self.emit('websocket::disconnect', socket);
|
560 | delete self.webSocketMap[socket.id];
|
561 | console.log("websocket "+socket.id+" disconnected");
|
562 | })
|
563 | socket.on('rpc::request', function(msg) {
|
564 | try {
|
565 | var listeners = self.listeners(msg.req.op);
|
566 | if(listeners.length == 1) {
|
567 | listeners[0].call(socket, msg.req, function(err, resp) {
|
568 | socket.emit('rpc::response', {
|
569 | _resp : msg._req,
|
570 | err : err,
|
571 | resp : resp,
|
572 | });
|
573 | })
|
574 | }
|
575 | else
|
576 | if(listeners.length)
|
577 | {
|
578 | socket.emit('rpc::response', {
|
579 | _resp : msg._req,
|
580 | err : { error : "Too many handlers for '"+msg.req.op+"'" }
|
581 | });
|
582 | }
|
583 | else
|
584 | {
|
585 | socket.emit('rpc::response', {
|
586 | _resp : msg._req,
|
587 | err : { error : "No such handler '"+msg.req.op+"'" }
|
588 | });
|
589 | }
|
590 | }
|
591 | catch(ex) { console.error(ex.stack); }
|
592 | });
|
593 |
|
594 | socket.on('message', function(msg) {
|
595 | try {
|
596 | self.emit(msg.op, msg, socket);
|
597 | }
|
598 | catch(ex) { console.error(ex.stack); }
|
599 | });
|
600 | });
|
601 |
|
602 | callback();
|
603 | }
|
604 |
|
605 |
|
606 |
|
607 | function updateServerStats() {
|
608 |
|
609 | self.pingDataObject.loadAvg = self.monitor.stats.loadAvg;
|
610 | self.pingDataObject.memory = self.monitor.stats.memory;
|
611 |
|
612 | dpc(5 * 1000, updateServerStats)
|
613 | }
|
614 |
|
615 |
|
616 |
|
617 | var initStepsBeforeHttp_ = [ ]
|
618 | var initSteps_ = [ ]
|
619 |
|
620 | self.initBeforeHttp = function(fn) {
|
621 | initStepsBeforeHttp_.push(fn);
|
622 | }
|
623 |
|
624 | self.init = function(fn) {
|
625 | initSteps_.push(fn);
|
626 | }
|
627 |
|
628 | self.run = function(callback) {
|
629 |
|
630 | var steps = new zutils.Steps();
|
631 |
|
632 |
|
633 | self.config.translator && steps.push(self.initTranslator);
|
634 | self.config.certificates && steps.push(self.initCertificates);
|
635 | self.config.statsd && steps.push(self.initMonitoringInterfaces);
|
636 | self.config.mongodb && self.databaseConfig && steps.push(self.initDatabaseConfig);
|
637 | self.config.mongodb && self.databaseCollections && steps.push(self.initDatabaseCollections);
|
638 | self.emit('init::database', steps);
|
639 | _.each(initStepsBeforeHttp_, function(fn) {
|
640 | steps.push(fn);
|
641 | })
|
642 | if(self.config.http) {
|
643 | steps.push(self.initExpressConfig);
|
644 | steps.push(self.initExpressHandlers);
|
645 | steps.push(self.initHttpServer);
|
646 | }
|
647 | self.config.mailer && steps.push(initMailer);
|
648 | self.config.supervisor && self.config.supervisor.address && steps.push(self.initSupervisors);
|
649 |
|
650 | getmac.getMac(function (err, mac) {
|
651 | if (err) return callback(err);
|
652 | self.mac = mac.split(process.platform == 'win32' ? '-' : ':').join('').toLowerCase();
|
653 | self.macBytes = _.map(self.mac.match(/.{1,2}/g), function(v) { return parseInt(v, 16); })
|
654 |
|
655 | var uuid = self.appFolder.replace(/\\/g,'/').split('/').pop();
|
656 | if(!uuid || uuid.length != 36) {
|
657 | var local = readJSON('uuid');
|
658 | if(local && local.uuid)
|
659 | uuid = local.uuid;
|
660 | else {
|
661 | uuid = UUID.v1({ node : self.macBytes });
|
662 | Application.writeJSON("uuid", { uuid : uuid });
|
663 | }
|
664 | }
|
665 | self.uuid = uuid;
|
666 |
|
667 | console.log("App UUID:".bold,self.uuid.bold);
|
668 |
|
669 | _.each(initSteps_, function(fn) {
|
670 | steps.push(fn);
|
671 | })
|
672 |
|
673 | self.emit('init::build', steps);
|
674 |
|
675 | steps.run(function (err) {
|
676 | if (err)
|
677 | throw err;
|
678 |
|
679 | self.config.statsd && updateServerStats();
|
680 | console.log("init OK".bold);
|
681 | self.emit('init::done');
|
682 | callback && callback();
|
683 | })
|
684 |
|
685 | })
|
686 |
|
687 | return self;
|
688 | }
|
689 |
|
690 | self.caption = self.pkg.name;
|
691 | dpc(function() {
|
692 | if(self.caption) {
|
693 | zutils.render(self.caption.replace('-',' '), null, function(err, caption) {
|
694 | console.log('\n'+caption);
|
695 | dpc(function() {
|
696 | self.run();
|
697 | })
|
698 | })
|
699 | }
|
700 | else {
|
701 | self.run();
|
702 | }
|
703 | })
|
704 |
|
705 | }
|
706 |
|
707 | util.inherits(Application, events.EventEmitter);
|
708 |
|
709 | Application.getConfig = getConfig;
|
710 | Application.readJSON = readJSON;
|
711 | Application.writeJSON = writeJSON;
|
712 |
|
713 | module.exports = {
|
714 | Application : Application,
|
715 | getConfig : getConfig
|
716 | }
|