UNPKG

20.1 kBJavaScriptView Raw
1/******************************************************************************
2 * Lemonade JS
3 * https://lemonadejs.com
4 *
5 * ----
6 *
7 * KERNEL
8 *
9 * Holds links to adaptors and daemons, main methods for including modules,
10 * logging (stdout and stderr), emits and listens for various events.
11 *
12 * Has 3 run levels : boot, run, shutdown.
13 *
14 * When constructed, it requires:
15 *
16 * - configuration object that will hold constants available
17 * everywhere via "this.kernel.config.*"
18 *
19 * - an object for each runlevel containing what "links" are created
20 * in each respective runlevel. All links will be available in the
21 * same manner as the configuration object via "this.linkname"
22 *
23 * @param {object} config
24 * @param {object} boot
25 * @param {object} run
26 * @param {object} shutdown
27 * @param {string} loglevel
28 *
29 *****************************************************************************/
30
31function Kernel (config, boot, run, shutdown, loglevel) {
32 /**
33 * Set the loglevel
34 */
35 if (loglevel !== this.LOGLEVEL_DEBUG &&
36 loglevel !== this.LOGLEVEL_NORMAL &&
37 loglevel !== this.LOGLEVEL_QUIET) {
38 loglevel = this.LOGLEVEL_NORMAL;
39 }
40 this.loglevel = loglevel;
41 /**
42 * Get the appdir
43 */
44 this.appdir = process.cwd() + '/';
45 /**
46 * Attach utility methods
47 */
48 this.utility = this.include(this.kerneldir + 'utility').singleton();
49 /**
50 * Merge config properties
51 */
52 this.config = this.include(this.kerneldir + 'config').singleton();
53 if (config) {
54 for (var key in config) {
55 this.config[key] = config[key];
56 }
57 }
58 this.debug(
59 'merged config : \n' + util.inspect(this.config, { depth : 0 })
60 , this._debugfile
61 );
62 /**
63 * Say hello
64 */
65 if (this.loglevel !== this.LOGLEVEL_QUIET) {
66 console.log();
67 console.log(this.utility.ansi('yellow', this.config.project));
68 console.log();
69 }
70 /**
71 * Fetch the link scripts
72 */
73 this._fetchlinks();
74 /**
75 * Attach listeners
76 */
77 this._listen();
78 /**
79 * Say hello 2
80 */
81 this.log('Lemonade ' + this.version + ' (' + this.name + ')');
82 this.log('app path : ' + this.appdir);
83 /**
84 * Check logpath
85 */
86 if (!fs.existsSync(this.config.logpath)) {
87 try {
88 fs.mkdirSync(this.config.logpath);
89 this.log(
90 'creating log path : ' + this.config.logpath + ' - ' +
91 this.utility.ansi('green', 'ok')
92 );
93 } catch (e) {
94 this.error(
95 new Error('Failed to create log path : ' + e)
96 );
97 }
98 } else {
99 this.log('log path : ' + this.config.logpath);
100 }
101 /**
102 * Attach the links to the kernel and prepare their loaders
103 */
104 var links = { boot : boot, run : run, shutdown : shutdown };
105 this.debug(
106 'preparing to attach links : \n' + util.inspect(links)
107 , this._debugfile
108 );
109 for (var level in links) {
110 for (var link in links[level]) {
111 this._attach(
112 link, links[level][link], level
113 );
114 }
115 }
116 this.debug(
117 'kernellinks : \n' + util.inspect(this._kernellinks, { depth : 3 })
118 , this._debugfile
119 );
120 /**
121 * Loops and switches runlevels when needed
122 */
123 this._heartbeat();
124}
125
126/**
127 * Module dependencies
128 */
129var
130 events = require('events')
131 , util = require('util')
132 , fs = require('fs')
133 , os = require('os')
134 , include = require('./include.js');
135
136/**
137 * Add emitter methods to the Kernel
138 */
139util.inherits(Kernel, events.EventEmitter);
140
141/**
142 * Kernel version
143 * @type {string}
144 */
145Kernel.prototype.version = require('../package.json').version;
146
147/**
148 * Kernel major version name
149 * @type {string}
150 */
151Kernel.prototype.name = 'Chrysanths';
152
153/**
154 * Event that will be emitted on each heartbeat loop. Should be useful
155 * for apps that use lemonade to check that it's still running
156 * @type {string}
157 */
158Kernel.prototype.HEARTBEAT = 'hearbeat';
159
160/**
161 * Event that will be emitted once the kernel goes into runlevel run and
162 * all links are ready
163 * @type {string}
164 */
165Kernel.prototype.ONLINE = 'kernel-online';
166
167/**
168 * Event that will be emitted once the kernel has gracefully shut down
169 * @type {string}
170 */
171Kernel.prototype.OFFLINE = 'kernel-offline';
172
173/**
174 * The kernel will listen for log or error events and call the appropriate
175 * methods : kernel.log / .error / .fatal. Used for asyncing the log/error
176 * operations. For sync calls the kernel methods can be accessed directly.
177 * @type {string}
178 */
179Kernel.prototype.LOG = 'log';
180Kernel.prototype.ERROR = 'error';
181Kernel.prototype.FATAL = 'fatal-error';
182
183/**
184 * Event emitted when a link successfully loads or unloads
185 * @type {string}
186 * @private
187 */
188Kernel.prototype._LINK_READY = 'link-ready';
189
190/**
191 * Kernel runlevels
192 * @private
193 */
194Kernel.prototype._RUNLEVEL_0 = 0;
195Kernel.prototype._RUNLEVEL_BOOT = 'boot';
196Kernel.prototype._RUNLEVEL_RUN = 'run';
197Kernel.prototype._RUNLEVEL_SHUTDOWN = 'shutdown';
198Kernel.prototype._currentrunlevel = 0;
199
200/**
201 * Kernel dir
202 * @type {string}
203 */
204Kernel.prototype.kerneldir = __dirname + '/';
205
206/**
207 * Application dir
208 * @type {string}
209 */
210Kernel.prototype.appdir = '';
211
212/**
213 * Configuration link
214 * @type {}
215 */
216Kernel.prototype.config = {};
217
218/**
219 * Utility link
220 * @type {}
221 */
222Kernel.prototype.utility = {};
223
224/**
225 * Loglevels
226 * @type {string}
227 */
228Kernel.prototype.LOGLEVEL_DEBUG = 'loglevel-debug';
229Kernel.prototype.LOGLEVEL_NORMAL = 'loglevel-normal';
230Kernel.prototype.LOGLEVEL_QUIET = 'loglevel-quiet';
231Kernel.prototype.loglevel = '';
232
233/**
234 * Kernel debug file
235 * @type {string}
236 * @private
237 */
238Kernel.prototype._debugfile = 'kernel.debug';
239
240/**
241 * Available link scripts (contents of appdir/links)
242 * @private
243 */
244Kernel.prototype._availablelinks = {};
245
246/**
247 * Kernel links stuctured to be loaded based on runlevel
248 * @private
249 */
250Kernel.prototype._kernellinks = {
251 /**
252 * Each kernellink is loaded via a loader script. Each loader script
253 * object is included and stored under the kernel link key in its
254 * corresponding runlevel
255 */
256 0 : { } // pre-boot
257 , boot : { }
258 , run : { }
259 , shutdown : { }
260};
261
262/**
263 * Links queued to be loaded for current runlevel
264 * @private
265 */
266Kernel.prototype._queuedfornow = {};
267
268/**
269 * Heartbeat interval id
270 * @private
271 */
272Kernel.prototype._heartbeatintv;
273
274/**
275 * Include a class file, overload it to contain instantiating
276 * methods, bind the kernel and instance object if available
277 *
278 * Depending on path's content type (extension based for now),
279 * a separate includer will be used. See ./include.js and ./include/*
280 *
281 * @param {string} path
282 * @return {function}
283 */
284Kernel.prototype.include = function(path) {
285 return include(path, this, this);
286};
287
288/**
289 * Gather up statuses from the kernel and all loaded modules
290 * @return {}
291 */
292Kernel.prototype.status = function() {
293
294 var memory = process.memoryUsage()
295 ,status = {};
296
297 status.kernel = {
298
299 hostname : os.hostname
300 ,memory : {
301 heapUsed : memory.heapUsed
302 ,heapTotal : memory.heapTotal
303 }
304 ,load : os.loadavg()
305 ,node : process.version
306 ,lemonade : {
307 version : this.version
308 ,name : this.name
309 }
310
311 };
312
313 for (var level in this._kernellinks) {
314 for (var link in this._kernellinks[level]) {
315 var linkstatus = this._kernellinks[level][link].obj.status();
316 if (linkstatus) {
317 status[link] = linkstatus;
318 } else {
319 status[link] = '';
320 }
321 }
322 }
323
324 return status;
325};
326
327/**
328 * Initiate graceful shutdown
329 * @param {string} message
330 */
331Kernel.prototype.shutdown = function(message) {
332 this._runlevel(this._RUNLEVEL_SHUTDOWN, message);
333};
334
335/**
336 * Generic replacement for console log with date
337 * @param {string|Error} message
338 * @param {string} file
339 * @param {Date} date
340 */
341Kernel.prototype.log = function(message, file, date) {
342 /**
343 * Log the error stack if we got an Error object
344 */
345 if (message instanceof Error) {
346 msg = this.utility.ansi('red', message.message);
347 msg += '\n\n\n' + message.stack + '\n\n';
348 message = msg;
349 } else if (this.loglevel === this.LOGLEVEL_QUIET) {
350 return;
351 }
352 if (typeof file !== 'undefined') {
353 var date = this.utility.date('yyyy-mm-dd HH:ii:ss', date)
354 ,logfile = this.config.logpath + file + '.log';
355 fs.appendFile(
356 logfile
357 ,date + ' ' + message + '\n'
358 ,function() {}
359 );
360 } else {
361 util.log(message);
362 }
363};
364
365/**
366 * Log an error message
367 * @param {string|Error} error
368 */
369Kernel.prototype.error = function(error) {
370 if (!(error instanceof Error)) {
371 error = new Error('Unprefixed error : ' + error);
372 }
373 this.log(error);
374};
375
376/**
377 * Log an error message and initiate shutdown
378 * @param {string|Error} message
379 */
380Kernel.prototype.fatal = function(message) {
381 this.error(message);
382 this.shutdown('fatal error');
383};
384
385/**
386 * Log a debug message (only if loglevel is set on debug)
387 * If param is function executes it only if loglevel debug
388 * @param {string|function} message
389 * @param {string} optional logfile
390 */
391Kernel.prototype.debug = function(message, logfile) {
392 if (this.loglevel === this.LOGLEVEL_DEBUG) {
393 if (typeof message === 'function') {
394 message();
395 } else {
396 this.log('DEBUG : ' + message, logfile);
397 }
398 }
399};
400
401/**
402 * Fetch the link scripts (contents of appdir/links)
403 * @private
404 */
405Kernel.prototype._fetchlinks = function() {
406 var fetch = function(dir) {
407 var arr = fs.readdirSync(dir);
408 for (var i = 0, max = arr.length; i < max; i++) {
409 var fname = arr[i].substring(0, arr[i].length-3);
410 this._availablelinks[fname] = dir + '/' + fname;
411 }
412 }.bind(this);
413 fetch(this.kerneldir + this.config._KERNEL_DIR_LINKS);
414 this.debug(
415 'base links : \n' + util.inspect(this._availablelinks)
416 , this._debugfile
417 );
418 if (fs.existsSync(this.appdir + this.config._KERNEL_DIR_LINKS)) {
419 fetch(this.appdir + this.config._KERNEL_DIR_LINKS);
420 this.debug(
421 'found custom links dir, read it and merged : \n'
422 + util.inspect(this._availablelinks)
423 , this._debugfile
424 );
425 }
426};
427
428/**
429 * Switch or return the current runlevel
430 * @private
431 * @param {string} level
432 * @param {string} message
433 * @return {string}
434 */
435Kernel.prototype._runlevel = function(level, message) {
436 /**
437 * Just checking, return the current runlevel
438 */
439 if (!level || (level === this._currentrunlevel)) {
440 return this._currentrunlevel;
441 }
442 /**
443 * Setting a new runlevel
444 */
445 this._currentrunlevel = level;
446 this.log(this.utility.ansi('blue',
447 '[ '
448 + this._currentrunlevel
449 + ((message) ? ' - ' + message : '')
450 +
451 ' ]'
452 ));
453 /**
454 * Callback to load/unload a link
455 */
456 var trigger = function(action, linkloader, linkname, level) {
457 process.nextTick(function() {
458 this._queuedfornow[linkname] = true;
459 if (typeof linkloader.obj[action] === 'function') {
460 try {
461 if (action === 'load') {
462 this.debug(
463 linkname + '.' + action + '(' + util.inspect(linkloader.loadparams) + ')'
464 );
465 /**
466 * Add a callback to the end of the params
467 * for the load method
468 */
469 linkloader.loadparams.push(function(err, msg) {
470 if (err) {
471 this.fatal(
472 new Error('[ ' + level + ' ][1] Link error - ' + linkname + ' : ' + err)
473 );
474 } else {
475 /**
476 * Module is ready, signal it
477 */
478 delete this._queuedfornow[linkname];
479 this.emit(this._LINK_READY, linkname, msg);
480 }
481 }.bind(this));
482 linkloader.obj[action].apply(linkloader.obj, linkloader.loadparams);
483 } else {
484 this.debug(
485 linkname + '.' + action + '()'
486 );
487 linkloader.obj[action](function(err, msg) {
488 if (err) {
489 this.fatal(
490 new Error('[ ' + level + ' ][3] Link error - ' + linkname + ' : ' + err)
491 );
492 } else {
493 delete this._queuedfornow[linkname];
494 this.emit(this._LINK_READY, linkname, msg);
495 }
496 }.bind(this));
497 }
498 } catch (e) {
499 this.fatal(
500 new Error('[ ' + level + ' ][2] Link error - ' + linkname + ' : ' + e)
501 );
502 }
503 } else {
504 /**
505 * Linkloader has no function defined for action, skip
506 */
507 delete this._queuedfornow[linkname];
508 this.emit(this._LINK_READY, linkname, 'skipping');
509 }
510 }.bind(this));
511 }.bind(this);
512 /**
513 * On shutdown, loop through all existing unload methods
514 */
515 if (this._currentrunlevel === this._RUNLEVEL_SHUTDOWN) {
516 for (var level in this._kernellinks) {
517 if (level !== this._RUNLEVEL_SHUTDOWN) {
518 var currentlinks = this._kernellinks[level];
519 for (var link in currentlinks) {
520 trigger(
521 'unload'
522 ,currentlinks[link]
523 ,link
524 ,level
525 );
526 }
527 }
528 }
529 } else {
530 /**
531 * Loop through the queued links and try to load them
532 */
533 var currentlinks = this._kernellinks[this._currentrunlevel];
534 for (var link in currentlinks) {
535 trigger(
536 'load'
537 ,currentlinks[link]
538 ,link
539 ,this._currentrunlevel
540 );
541 }
542 }
543};
544
545/**
546 * Internal loop, will switch runlevels when needed
547 * @private
548 */
549Kernel.prototype._heartbeat = function () {
550 /**
551 * Start the interval if not running
552 */
553 if (!this._heartbeatintv) {
554 this._heartbeatintv = setInterval(function() {
555 this._heartbeat();
556 }.bind(this), 1000);
557 }
558 /**
559 * Check if there are any links needing load for the
560 * current runlevel
561 * @type {Array}
562 */
563 var left = Object.keys(this._queuedfornow);
564 if (left.length > 0) {
565 /**
566 * Output links still waiting
567 */
568 this.log(
569 'zZz waiting for links (' + this.utility.ansi('red', left.join(',')) + ')'
570 );
571 } else {
572 /**
573 * Current runlevel is completed, decide what to do next
574 */
575 switch (this._currentrunlevel) {
576 case this._RUNLEVEL_0 :
577 /**
578 * First heartbeat, should initiate boot
579 */
580 this._runlevel(this._RUNLEVEL_BOOT);
581 break;
582 case this._RUNLEVEL_BOOT :
583 /**
584 * Boot ready, move on
585 */
586 this._runlevel(this._RUNLEVEL_RUN);
587 break;
588 case this._RUNLEVEL_RUN :
589 /**
590 * We are running
591 */
592 this.emit(this.ONLINE);
593 break;
594 case this._RUNLEVEL_SHUTDOWN :
595 /**
596 * Shutdown complete, stop the heartbeat
597 */
598 this.emit(this.OFFLINE);
599 clearInterval(this._heartbeatintv);
600 break;
601 }
602 }
603 /**
604 * Send out that it's still ticking
605 */
606 this.emit(this.HEARTBEAT);
607};
608
609/**
610 * Listen for various events
611 * @private
612 */
613Kernel.prototype._listen = function() {
614 /**
615 * Logs a message
616 */
617 this.on(this.LOG, function(message) {
618 this.log(message);
619 }.bind(this));
620 /**
621 * Logs an error message
622 */
623 this.on(this.ERROR, function(message) {
624 this.error(message);
625 }.bind(this));
626 /**
627 * Logs an error message and shuts down
628 */
629 this.on(this.FATAL, function(message) {
630 this.fatal(message);
631 }.bind(this));
632 /**
633 * Each link emits when ready
634 */
635 this.on(this._LINK_READY, function(link, msg) {
636 this.log(
637 this.utility.ansi('green', 'ok') + ' - '
638 + link + (msg ? ' ' + msg : '')
639 );
640 }.bind(this));
641};
642
643/**
644 * Attach a kernel link
645 * @private
646 * @param {string} key
647 * @param {string|Array|object} value
648 * @param {string|number} level
649 */
650Kernel.prototype._attach = function(key, value, level) {
651 var linkloader = {}, module;
652 /**
653 * Direct declaration
654 */
655 if (typeof value === 'function') {
656 this.debug(
657 'attaching function ' + key + ' : \n' + value
658 , this._debugfile
659 );
660 linkloader = value;
661 }
662 /**
663 * Is the value one of the available link scripts?
664 */
665 else if (this._availablelinks[value] ||
666 (value instanceof Array && this._availablelinks[value[0]])) {
667
668 module = this.include(
669 this._availablelinks[((value instanceof Array) ? value[0] : value)]
670 );
671 this.debug(
672 'attaching availablelink ' + key +': \n' + util.inspect(module, { depth : 0 })
673 , this._debugfile
674 );
675 if (module && typeof module.new === 'function') {
676 linkloader = module.new();
677 }
678 }
679 /**
680 * Value is not one of the available modules, treat it
681 * as a string and try to load the path
682 */
683 else if (fs.existsSync(this.appdir + value + '.js')) {
684 try {
685 module = this.include(this.appdir + value);
686 this.debug(
687 'attaching included module ' + key +': \n' + util.inspect(module, { depth : 0 })
688 , this._debugfile
689 );
690 if (module && typeof module.new === 'function') {
691 linkloader = module.new();
692 }
693 } catch (e) {
694 this.error(
695 new Error('Couldn\'t load ' + key + ' from ' + this.appdir + value + ' : ' + e)
696 );
697 }
698 }
699 /**
700 * Check the linkloader for the most important method
701 */
702 if (typeof linkloader.link === 'function') {
703 var params = [];
704 if (value instanceof Array) {
705 for (var i = 1, max = value.length; i < max; i++) {
706 params.push(value[i]);
707 }
708 }
709 /**
710 * Reference the object and load params
711 */
712 this._kernellinks[level][key] = {
713 obj : linkloader, loadparams : params, ready : {}
714 };
715 /**
716 * Bind the link method to the kernel
717 */
718 linkloader.label = key;
719 this[key] = linkloader.link.bind(linkloader);
720 } else {
721 /**
722 * Bad link loader, panic!
723 */
724 this.fatal(
725 new Error('Invalid link : ' + key)
726 );
727 }
728};
729
730/**
731 * Exports
732 */
733exports = module.exports = Kernel;
\No newline at end of file