UNPKG

20.5 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 * Clear appdir require cache
403 */
404Kernel.prototype.clearcache = function() {
405 for (var key in require.cache) {
406 this.debug('found in require.cache : ' + key);
407 if (key.indexOf(this.appdir) !== -1) {
408 this.debug('cleared : ' + key);
409 delete require.cache[key];
410 }
411 }
412 this.log(
413 this.utility.ansi('green', 'ok') + ' - '
414 + 'cleared application cache'
415 );
416};
417
418/**
419 * Fetch the link scripts (contents of appdir/links)
420 * @private
421 */
422Kernel.prototype._fetchlinks = function() {
423 var fetch = function(dir) {
424 var arr = fs.readdirSync(dir);
425 for (var i = 0, max = arr.length; i < max; i++) {
426 var fname = arr[i].substring(0, arr[i].length-3);
427 this._availablelinks[fname] = dir + '/' + fname;
428 }
429 }.bind(this);
430 fetch(this.kerneldir + this.config._KERNEL_DIR_LINKS);
431 this.debug(
432 'base links : \n' + util.inspect(this._availablelinks)
433 , this._debugfile
434 );
435 if (fs.existsSync(this.appdir + this.config._KERNEL_DIR_LINKS)) {
436 fetch(this.appdir + this.config._KERNEL_DIR_LINKS);
437 this.debug(
438 'found custom links dir, read it and merged : \n'
439 + util.inspect(this._availablelinks)
440 , this._debugfile
441 );
442 }
443};
444
445/**
446 * Switch or return the current runlevel
447 * @private
448 * @param {string} level
449 * @param {string} message
450 * @return {string}
451 */
452Kernel.prototype._runlevel = function(level, message) {
453 /**
454 * Just checking, return the current runlevel
455 */
456 if (!level || (level === this._currentrunlevel)) {
457 return this._currentrunlevel;
458 }
459 /**
460 * Setting a new runlevel
461 */
462 this._currentrunlevel = level;
463 this.log(this.utility.ansi('blue',
464 '[ '
465 + this._currentrunlevel
466 + ((message) ? ' - ' + message : '')
467 +
468 ' ]'
469 ));
470 /**
471 * Callback to load/unload a link
472 */
473 var trigger = function(action, linkloader, linkname, level) {
474 process.nextTick(function() {
475 this._queuedfornow[linkname] = true;
476 if (typeof linkloader.obj[action] === 'function') {
477 try {
478 if (action === 'load') {
479 this.debug(
480 linkname + '.' + action + '(' + util.inspect(linkloader.loadparams) + ')'
481 );
482 /**
483 * Add a callback to the end of the params
484 * for the load method
485 */
486 linkloader.loadparams.push(function(err, msg) {
487 if (err) {
488 this.fatal(
489 new Error('[ ' + level + ' ][1] Link error - ' + linkname + ' : ' + err)
490 );
491 } else {
492 /**
493 * Module is ready, signal it
494 */
495 delete this._queuedfornow[linkname];
496 this.emit(this._LINK_READY, linkname, msg);
497 }
498 }.bind(this));
499 linkloader.obj[action].apply(linkloader.obj, linkloader.loadparams);
500 } else {
501 this.debug(
502 linkname + '.' + action + '()'
503 );
504 linkloader.obj[action](function(err, msg) {
505 if (err) {
506 this.fatal(
507 new Error('[ ' + level + ' ][3] Link error - ' + linkname + ' : ' + err)
508 );
509 } else {
510 delete this._queuedfornow[linkname];
511 this.emit(this._LINK_READY, linkname, msg);
512 }
513 }.bind(this));
514 }
515 } catch (e) {
516 this.fatal(
517 new Error('[ ' + level + ' ][2] Link error - ' + linkname + ' : ' + e)
518 );
519 }
520 } else {
521 /**
522 * Linkloader has no function defined for action, skip
523 */
524 delete this._queuedfornow[linkname];
525 this.emit(this._LINK_READY, linkname, 'skipping');
526 }
527 }.bind(this));
528 }.bind(this);
529 /**
530 * On shutdown, loop through all existing unload methods
531 */
532 if (this._currentrunlevel === this._RUNLEVEL_SHUTDOWN) {
533 for (var level in this._kernellinks) {
534 if (level !== this._RUNLEVEL_SHUTDOWN) {
535 var currentlinks = this._kernellinks[level];
536 for (var link in currentlinks) {
537 trigger(
538 'unload'
539 ,currentlinks[link]
540 ,link
541 ,level
542 );
543 }
544 }
545 }
546 } else {
547 /**
548 * Loop through the queued links and try to load them
549 */
550 var currentlinks = this._kernellinks[this._currentrunlevel];
551 for (var link in currentlinks) {
552 trigger(
553 'load'
554 ,currentlinks[link]
555 ,link
556 ,this._currentrunlevel
557 );
558 }
559 }
560};
561
562/**
563 * Internal loop, will switch runlevels when needed
564 * @private
565 */
566Kernel.prototype._heartbeat = function () {
567 /**
568 * Start the interval if not running
569 */
570 if (!this._heartbeatintv) {
571 this._heartbeatintv = setInterval(function() {
572 this._heartbeat();
573 }.bind(this), 1000);
574 }
575 /**
576 * Check if there are any links needing load for the
577 * current runlevel
578 * @type {Array}
579 */
580 var left = Object.keys(this._queuedfornow);
581 if (left.length > 0) {
582 /**
583 * Output links still waiting
584 */
585 this.log(
586 'zZz waiting for links (' + this.utility.ansi('red', left.join(',')) + ')'
587 );
588 } else {
589 /**
590 * Current runlevel is completed, decide what to do next
591 */
592 switch (this._currentrunlevel) {
593 case this._RUNLEVEL_0 :
594 /**
595 * First heartbeat, should initiate boot
596 */
597 this._runlevel(this._RUNLEVEL_BOOT);
598 break;
599 case this._RUNLEVEL_BOOT :
600 /**
601 * Boot ready, move on
602 */
603 this._runlevel(this._RUNLEVEL_RUN);
604 break;
605 case this._RUNLEVEL_RUN :
606 /**
607 * We are running
608 */
609 this.emit(this.ONLINE);
610 break;
611 case this._RUNLEVEL_SHUTDOWN :
612 /**
613 * Shutdown complete, stop the heartbeat
614 */
615 this.clearcache();
616 clearInterval(this._heartbeatintv);
617 this.emit(this.OFFLINE);
618 break;
619 }
620 }
621 /**
622 * Send out that it's still ticking
623 */
624 this.emit(this.HEARTBEAT);
625};
626
627/**
628 * Listen for various events
629 * @private
630 */
631Kernel.prototype._listen = function() {
632 /**
633 * Logs a message
634 */
635 this.on(this.LOG, function(message) {
636 this.log(message);
637 }.bind(this));
638 /**
639 * Logs an error message
640 */
641 this.on(this.ERROR, function(message) {
642 this.error(message);
643 }.bind(this));
644 /**
645 * Logs an error message and shuts down
646 */
647 this.on(this.FATAL, function(message) {
648 this.fatal(message);
649 }.bind(this));
650 /**
651 * Each link emits when ready
652 */
653 this.on(this._LINK_READY, function(link, msg) {
654 this.log(
655 this.utility.ansi('green', 'ok') + ' - '
656 + link + (msg ? ' ' + msg : '')
657 );
658 }.bind(this));
659};
660
661/**
662 * Attach a kernel link
663 * @private
664 * @param {string} key
665 * @param {string|Array|object} value
666 * @param {string|number} level
667 */
668Kernel.prototype._attach = function(key, value, level) {
669 var linkloader = {}, module;
670 /**
671 * Direct declaration
672 */
673 if (typeof value === 'function') {
674 this.debug(
675 'attaching function ' + key + ' : \n' + value
676 , this._debugfile
677 );
678 linkloader = value;
679 }
680 /**
681 * Is the value one of the available link scripts?
682 */
683 else if (this._availablelinks[value] ||
684 (value instanceof Array && this._availablelinks[value[0]])) {
685
686 module = this.include(
687 this._availablelinks[((value instanceof Array) ? value[0] : value)]
688 );
689 this.debug(
690 'attaching availablelink ' + key +': \n' + util.inspect(module, { depth : 0 })
691 , this._debugfile
692 );
693 if (module && typeof module.new === 'function') {
694 linkloader = module.new();
695 }
696 }
697 /**
698 * Value is not one of the available modules, treat it
699 * as a string and try to load the path
700 */
701 else if (fs.existsSync(this.appdir + value + '.js')) {
702 try {
703 module = this.include(this.appdir + value);
704 this.debug(
705 'attaching included module ' + key +': \n' + util.inspect(module, { depth : 0 })
706 , this._debugfile
707 );
708 if (module && typeof module.new === 'function') {
709 linkloader = module.new();
710 }
711 } catch (e) {
712 this.error(
713 new Error('Couldn\'t load ' + key + ' from ' + this.appdir + value + ' : ' + e)
714 );
715 }
716 }
717 /**
718 * Check the linkloader for the most important method
719 */
720 if (typeof linkloader.link === 'function') {
721 var params = [];
722 if (value instanceof Array) {
723 for (var i = 1, max = value.length; i < max; i++) {
724 params.push(value[i]);
725 }
726 }
727 /**
728 * Reference the object and load params
729 */
730 this._kernellinks[level][key] = {
731 obj : linkloader, loadparams : params, ready : {}
732 };
733 /**
734 * Bind the link method to the kernel
735 */
736 linkloader.label = key;
737 this[key] = linkloader.link.bind(linkloader);
738 } else {
739 /**
740 * Bad link loader, panic!
741 */
742 this.fatal(
743 new Error('Invalid link : ' + key)
744 );
745 }
746};
747
748/**
749 * Exports
750 */
751exports = module.exports = Kernel;
\No newline at end of file