Huge refactoring of architecture started

This commit is contained in:
Dominic Bosch 2014-02-17 23:27:26 +01:00
parent 92ab16ba74
commit 264ef6eeb6
25 changed files with 742 additions and 411 deletions

View file

@ -7,26 +7,26 @@ Configuration
###
# **Requires:**
# - [Logging](logging.html)
log = require './logging'
# - Node.js Modules: [fs](http://nodejs.org/api/fs.html) and
# [path](http://nodejs.org/api/path.html)
fs = require 'fs'
path = require 'path'
###
##Module call
Module call
-----------
Calling the module as a function will act as a constructor and load the config file.
It is possible to hand an args object with the properties nolog (true if no outputs shall
be generated) and configPath for a custom configuration file path.
Calling the module as a function will make it look for the `configPath` property in the
args object and then try to load a config file from that relative path.
@param {Object} args
###
exports = module.exports = ( args ) ->
exports = module.exports = ( args ) =>
args = args ? {}
log args
if typeof args.configPath is 'string'
if args.nolog
@nolog = true
if args.configPath
loadConfigFile args.configPath
else
loadConfigFile path.join 'config', 'config.json'
@ -41,20 +41,25 @@ Reads the config file synchronously from the file system and try to parse it.
###
loadConfigFile = ( configPath ) =>
@config = null
confProperties = [
'log'
'http-port'
'db-port'
'crypto-key'
]
#TODO Try to get rid of crypto key
try
@config = JSON.parse fs.readFileSync path.resolve __dirname, '..', configPath
if @config and @config.http_port and @config.db_port and
@config.crypto_key and @config.session_secret
log.print 'CF', 'config file loaded successfully'
else
log.error 'CF', new Error """Missing property in config file, requires:
- http_port
- db_port
- crypto_key
- session_secret"""
isReady = true
for prop in confProperties
if !@config[prop]
isReady = false
if not isReady and not @nolog
console.error """Missing property in config file, requires:
- #{ confProperties.join "\n -" }"""
catch e
e.addInfo = 'no config ready'
log.error 'CF', e
if not @nolog
console.error "Failed loading config file: #{ e.message }"
###
@ -77,25 +82,25 @@ exports.isReady = => @config?
@public getHttpPort()
###
exports.getHttpPort = -> fetchProp 'http_port'
exports.getHttpPort = -> fetchProp 'http-port'
###
***Returns*** the DB port*
@public getDBPort()
###
exports.getDBPort = -> fetchProp 'db_port'
exports.getDBPort = -> fetchProp 'db-port'
###
***Returns*** the log conf object
@public getLogConf()
###
exports.getLogConf = -> fetchProp 'log'
###
***Returns*** the crypto key
@public getCryptoKey()
###
exports.getCryptoKey = -> fetchProp 'crypto_key'
###
***Returns*** the session secret
@public getSessionSecret()
###
exports.getSessionSecret = -> fetchProp 'session_secret'
exports.getCryptoKey = -> fetchProp 'crypto-key'

View file

@ -15,6 +15,7 @@ log = require './logging'
# - [Config](config.html)
config = require './config'
# TODO remove config
# - [User Handler](user_handler.html)
requestHandler = require './request_handler'
@ -53,10 +54,14 @@ Adds the shutdown handler to the admin commands.
@public addHandlers( *fShutDown* )
###
exports.addHandlers = ( fShutDown ) ->
requestHandler.addHandlers fShutDown
requestHandler.addShutdownHandler fShutDown
# Add cookie support for session handling.
app.use express.cookieParser()
app.use express.session { secret: config.getSessionSecret() }
#TODO This needs to be fixed!
sess_sec = "149u*y8C:@kmN/520Gt\\v'+KFBnQ!\\r<>5X/xRI`sT<Iw"
app.use express.session { secret: sess_sec }
# app.use express.session { secret: config.getSessionSecret() }
#At the moment there's no redis session backbone (didn't work straight away)
log.print 'HL', 'no session backbone'

59
coffee/new_logging.coffee Normal file
View file

@ -0,0 +1,59 @@
# Logging
# =======
# A Helper to handle logging.
# **Requires:**
# - Node.js Module(s): [path](http://nodejs.org/api/path.html)
path = require 'path'
# - External Module(s): [bunyan](https://github.com/trentm/node-bunyan)
bunyan = require 'bunyan'
###
Module call
-----------
Calling the module as a function will act as a constructor and load the config file.
It is possible to hand an args object with the properties nolog (true if no outputs shall
be generated) and configPath for a custom configuration file path.
@param {Object} args
###
exports = module.exports = ( args ) =>
emptylog =
{
info: () ->
warn: () ->
error: () ->
}
args = args ? {}
if args.nolog
emptylog
else
try
opt =
name: "webapi-eca"
if args['mode'] is 'development'
opt.src = true
if args['file-path']
@logPath = path.resolve __dirname, '..', 'logs', args['file-path']
else
@logPath = path.resolve __dirname, '..', 'logs', 'server.log'
opt.streams = [
{
level: args['io-level']
stream: process.stdout
},
{
level: args['file-level']
type: 'rotating-file'
path: @logPath
period: '1d'
count: 3
}
]
bunyan.createLogger opt
catch e
console.error e
emptylog

View file

@ -55,7 +55,7 @@ exports = module.exports = ( args ) =>
@ai = new IndexedModules( 'action-invoker', @db )
else
log.error 'DB', 'Initialization failed because of missing config file!'
###
Checks whether the db is connected and passes either an error on failure after
ten attempts within five seconds, or nothing on success to the callback(err).
@ -845,4 +845,4 @@ Shuts down the db link.
@public shutDown()
###
exports.shutDown = => @db.quit()
exports.shutDown = () => @db.quit()

View file

@ -28,14 +28,6 @@ qs = require 'querystring'
# [crypto-js](https://github.com/evanvosberg/crypto-js)
mustache = require 'mustache'
crypto = require 'crypto-js'
# Prepare the admin command handlers which are invoked via HTTP requests.
objAdminCmds =
'loadrules': mm.loadRulesFromFS
'loadaction': mm.loadActionModuleFromFS
'loadactions': mm.loadActionModulesFromFS
'loadevent': mm.loadEventModuleFromFS
'loadevents': mm.loadEventModulesFromFS
# Prepare the user command handlers which are invoked via HTTP requests.
objUserCmds =
@ -57,15 +49,14 @@ exports = module.exports = ( args ) ->
module.exports
###
This allows the parent to add handlers. The event handler will receive
the events that were received. The shutdown function will be called if the
admin command shutdown is issued.
This allows the parent to add the shutdown handler.
The shutdown function will be called if the admin command shutdown is issued.
@public addHandlers( *fShutdown* )
@public addShutdownHandler( *fShutdown* )
@param {function} fShutdown
###
exports.addHandlers = ( fShutdown ) =>
objAdminCmds.shutdown = ( args, answerHandler ) ->
exports.addShutdownHandler = ( fShutdown ) =>
@objAdminCmds.shutdown = ( args, answerHandler ) ->
answerHandler.answerSuccess 'Shutting down... BYE!'
setTimeout fShutdown, 500
@ -284,13 +275,13 @@ objects.*
@public handleAdmin( *req, resp* )
###
exports.handleAdmin = ( req, resp ) ->
exports.handleAdmin = ( req, resp ) =>
if req.session and req.session.user
if req.session.user.isAdmin is "true"
q = req.query
log.print 'RH', 'Received admin request: ' + req.originalUrl
if q.cmd
objAdminCmds[q.cmd]? q, answerHandler req, resp, true
@objAdminCmds[q.cmd]? q, answerHandler req, resp, true
else
resp.send 404, 'Command unknown!'
else

View file

@ -5,26 +5,15 @@ Server
>This is the main module that is used to run the whole server:
>
> node server [log_type http_port]
> node server [opt]
>
>Valid `log_type`'s are:
>
>- `0`: standard I/O output (default)
>- `1`: log file (server.log)
>- `2`: silent
>
>`http_port` can be set to use another port, than defined in the
>[config](config.html) file, to listen to, e.g. used by the test suite.
>
>
TODO how about we allow spawning child processes with servers based on address?
> See below in the optimist CLI preparation for allowed optional parameters
###
# **Requires:**
# - [Logging](logging.html)
log = require './logging'
logger = require './new_logging'
# - [Configuration](config.html)
conf = require './config'
@ -38,9 +27,54 @@ engine = require './engine'
# - [HTTP Listener](http_listener.html)
http_listener = require './http_listener'
args = {}
# - Node.js Modules: [fs](http://nodejs.org/api/fs.html),
# [path](http://nodejs.org/api/path.html)
# and [child_process](http://nodejs.org/api/child_process.html)
fs = require 'fs'
path = require 'path'
cp = require 'child_process'
# - External Module: [optimist](https://github.com/substack/node-optimist)
optimist = require 'optimist'
procCmds = {}
###
Let's prepare the optimist CLI
###
usage = 'This runs your webapi-based ECA engine'
opt =
'h':
alias : 'help',
describe: 'Display this'
'c':
alias : 'config-path',
describe: 'Specify a path to a custom configuration file, other than "config/config.json"'
'w':
alias : 'http-port',
describe: 'Specify a HTTP port for the web server'
'd':
alias : 'db-port',
describe: 'Specify a port for the redis DB'
'm':
alias : 'log-mode',
describe: 'Specify a log mode: [development|productive]'
'i':
alias : 'log-io-level',
describe: 'Specify the log level for the I/O'
'f':
alias : 'log-file-level',
describe: 'Specify the log level for the log file'
'p':
alias : 'log-file-path',
describe: 'Specify the path to the log file within the "logs" folder'
'n':
alias : 'nolog',
describe: 'Set this if no output shall be generated'
argv = optimist.usage( usage ).options( opt ).argv
if argv.help
console.log optimist.help()
process.exit()
###
Error handling of the express port listener requires special attention,
@ -50,7 +84,7 @@ the port is already in use.
process.on 'uncaughtException', ( err ) ->
switch err.errno
when 'EADDRINUSE'
err.addInfo = 'http_port already in use, shutting down!'
err.addInfo = 'http-port already in use, shutting down!'
log.error 'RS', err
shutDown()
# else log.error 'RS', err
@ -61,50 +95,63 @@ This function is invoked right after the module is loaded and starts the server.
@private init()
###
init = ->
log.print 'RS', 'STARTING SERVER'
conf args
conf argv.c
# > Check whether the config file is ready, which is required to start the server.
if !conf.isReady()
log.error 'RS', 'Config file not ready!'
console.error 'FAIL: Config file not ready! Shutting down...'
process.exit()
# > Fetch the `log_type` argument and post a log about which log type is used.
if process.argv.length > 2
args.logType = parseInt(process.argv[2]) || 0
switch args.logType
when 0 then log.print 'RS', 'Log type set to standard I/O output'
when 1 then log.print 'RS', 'Log type set to file output'
when 2 then log.print 'RS', 'Log type set to silent'
else log.print 'RS', 'Unknown log type, using standard I/O'
log args
else
log.print 'RS', 'No log method argument provided, using standard I/O'
# > Fetch the `http_port` argument
if process.argv.length > 3 then args.http_port = parseInt process.argv[3]
else log.print 'RS', 'No HTTP port passed, using standard port from config file'
logconf = conf.getLogConf()
if argv.m
logconf[ 'mode' ] = argv.m
if argv.i
logconf[ 'io-level' ] = argv.i
if argv.f
logconf[ 'file-level' ] = argv.f
if argv.p
logconf[ 'file-path' ] = argv.p
if argv.n
logconf[ 'nolog' ] = argv.n
try
fs.unlinkSync path.resolve __dirname, '..', 'logs', logconf[ 'file-path' ]
log = logger logconf
log.info 'RS | STARTING SERVER'
args =
logger: log
logconf: logconf
# > Fetch the `http-port` argument
args[ 'http-port' ] = parseInt argv.w || conf.getHttpPort()
log.print 'RS', 'Initialzing DB'
log.info 'RS | Initialzing DB'
db args
# > We only proceed with the initialization if the DB is ready
db.isConnected ( err, result ) ->
if !err
# > Initialize all required modules with the args object.
log.print 'RS', 'Initialzing engine'
log.info 'RS | Initialzing engine'
engine args
log.print 'RS', 'Initialzing http listener'
log.info 'RS | Initialzing http listener'
http_listener args
# > Distribute handlers between modules to link the application.
log.print 'RS', 'Passing handlers to engine'
log.info 'RS | Passing handlers to engine'
engine.addPersistence db
log.print 'RS', 'Passing handlers to http listener'
log.info 'RS | Passing handlers to http listener'
#TODO engine pushEvent needs to go into redis queue
http_listener.addHandlers shutDown
#log.print 'RS', 'Passing handlers to module manager'
http_listener.addShutdownHandler shutDown
#TODO loadAction and addRule will be removed
#mm.addHandlers db, engine.loadActionModule, engine.addRule
log.info 'RS | For e child process for the event poller'
cliArgs = [
args.logconf['mode']
args.logconf['io-level']
args.logconf['file-level']
args.logconf['file-path']
args.logconf['nolog']
]
poller = cp.fork path.resolve( __dirname, 'event_poller' ), cliArgs
###
Shuts down the server.
@ -112,7 +159,7 @@ Shuts down the server.
@private shutDown()
###
shutDown = ->
log.print 'RS', 'Received shut down command!'
log.warn 'RS | Received shut down command!'
engine?.shutDown()
http_listener?.shutDown()

View file

@ -8,28 +8,31 @@ Configuration
(function() {
var exports, fetchProp, fs, loadConfigFile, log, path,
var exports, fetchProp, fs, loadConfigFile, path,
_this = this;
log = require('./logging');
fs = require('fs');
path = require('path');
/*
##Module call
Module call
-----------
Calling the module as a function will act as a constructor and load the config file.
It is possible to hand an args object with the properties nolog (true if no outputs shall
be generated) and configPath for a custom configuration file path.
Calling the module as a function will make it look for the `configPath` property in the
args object and then try to load a config file from that relative path.
@param {Object} args
*/
exports = module.exports = function(args) {
args = args != null ? args : {};
log(args);
if (typeof args.configPath === 'string') {
if (args.nolog) {
_this.nolog = true;
}
if (args.configPath) {
loadConfigFile(args.configPath);
} else {
loadConfigFile(path.join('config', 'config.json'));
@ -47,19 +50,26 @@ Configuration
loadConfigFile = function(configPath) {
var e;
var confProperties, e, isReady, prop, _i, _len;
_this.config = null;
confProperties = ['log', 'http-port', 'db-port', 'crypto-key'];
try {
_this.config = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', configPath)));
if (_this.config && _this.config.http_port && _this.config.db_port && _this.config.crypto_key && _this.config.session_secret) {
return log.print('CF', 'config file loaded successfully');
} else {
return log.error('CF', new Error("Missing property in config file, requires:\n- http_port\n- db_port\n- crypto_key\n- session_secret"));
isReady = true;
for (_i = 0, _len = confProperties.length; _i < _len; _i++) {
prop = confProperties[_i];
if (!_this.config[prop]) {
isReady = false;
}
}
if (!isReady && !_this.nolog) {
return console.error("Missing property in config file, requires: \n- " + (confProperties.join("\n -")));
}
} catch (_error) {
e = _error;
e.addInfo = 'no config ready';
return log.error('CF', e);
if (!_this.nolog) {
return console.error("Failed loading config file: " + e.message);
}
}
};
@ -95,7 +105,7 @@ Configuration
exports.getHttpPort = function() {
return fetchProp('http_port');
return fetchProp('http-port');
};
/*
@ -106,7 +116,18 @@ Configuration
exports.getDBPort = function() {
return fetchProp('db_port');
return fetchProp('db-port');
};
/*
***Returns*** the log conf object
@public getLogConf()
*/
exports.getLogConf = function() {
return fetchProp('log');
};
/*
@ -117,18 +138,7 @@ Configuration
exports.getCryptoKey = function() {
return fetchProp('crypto_key');
};
/*
***Returns*** the session secret
@public getSessionSecret()
*/
exports.getSessionSecret = function() {
return fetchProp('session_secret');
return fetchProp('crypto-key');
};
}).call(this);

View file

@ -1,23 +1,18 @@
'use strict';
var path = require('path'),
cp = require('child_process'),
log = require('./logging'),
qEvents = new (require('./queue')).Queue(), //TODO export queue into redis
// qEvents = new (require('./queue')).Queue(), //TODO export queue into redis
regex = /\$X\.[\w\.\[\]]*/g, // find properties of $X
listRules = {},
listActionModules = {},
isRunning = true,
ml, poller, db;
exports = module.exports = function(args) {
exports = module.exports = function( args ) {
args = args || {};
log(args);
ml = require('./module_loader')(args);
poller = cp.fork(path.resolve(__dirname, 'eventpoller'), [log.getLogType()]);
poller.on('message', function(evt) {
exports.pushEvent(evt);
});
return module.exports;
};

View file

@ -2,14 +2,13 @@
'use strict';
var fs = require('fs'),
path = require('path'),
log = require('./logging'),
var logger = require('./new_logging'),
listMessageActions = {},
listAdminCommands = {},
listEventModules = {},
listPoll = {}, //TODO this will change in the future because it could have
//several parameterized (user-specific) instances of each event module
log,
isRunning = true,
eId = 0,
db, ml;
@ -18,13 +17,26 @@ var fs = require('fs'),
function init() {
if(process.argv.length > 2) log({ logType: parseInt(process.argv[2]) || 0 });
var args = { logType: log.getLogType() };
(ml = require('./module_loader'))(args);
(db = require('./db_interface'))(args);
if(process.argv.length < 7){
console.error('Not all arguments have been passed!');
process.exit();
}
var logconf = {}
logconf['mode'] = process.argv[2]
logconf['io-level'] = process.argv[3]
logconf['file-level'] = process.argv[4]
logconf['file-path'] = process.argv[5]
logconf['nolog'] = process.argv[6]
log = logger(logconf);
var args = { logger: log };
(ml = require('./module_manager'))(args);
(db = require('./persistence'))(args);
initAdminCommands();
initMessageActions();
pollLoop();
log.info('Event Poller instantiated');
};
@ -128,11 +140,13 @@ function checkRemotes() {
err.additionalInfo = 'module: ' + p;
log.error('EP', err);
} else {
process.send({
event: p,
eventid: 'polled_' + eId++,
payload: obj
});
// FIXME this needs to be pushed into the db not passed to the process!
console.error('eventpoller needs to push event into db queue')
// process.send({
// event: p,
// eventid: 'polled_' + eId++,
// payload: obj
// });
}
};
})(prop)

View file

@ -54,11 +54,12 @@ HTTP Listener
exports.addHandlers = function(fShutDown) {
var e, http_port;
requestHandler.addHandlers(fShutDown);
var e, http_port, sess_sec;
requestHandler.addShutdownHandler(fShutDown);
app.use(express.cookieParser());
sess_sec = "149u*y8C:@kmN/520Gt\\v'+KFBnQ!\\r<>5X/xRI`sT<Iw";
app.use(express.session({
secret: config.getSessionSecret()
secret: sess_sec
}));
log.print('HL', 'no session backbone');
app.use('/', express["static"](path.resolve(__dirname, '..', 'webpages', 'public')));

View file

@ -11,6 +11,7 @@
*/
//TODO dynamic log file names (especially to track unit test logs)
var fs = require('fs'),
wst = require('winston'),
logTypes = [ flushToConsole, flushToFile, null],
logFile = require('path').resolve(__dirname, '..', 'server.log'),
logType = 0;
@ -20,6 +21,8 @@ exports = module.exports = function(args) {
if(args.logType) logType = parseInt(args.logType) || 0;
if(logType == 1) fs.truncateSync(logFile, 0);
if(logType > logTypes.length - 1) logType = 0;
// winston.add(winston.transports.File, { filename: 'somefile.log' });
// winston.remove(winston.transports.Console);
return module.exports;
};

View file

@ -1,81 +0,0 @@
'use strict';
var fs = require('fs'),
path = require('path'),
log = require('./logging');
exports = module.exports = function(args) {
args = args || {};
log(args);
return module.exports;
};
exports.requireFromString = function(src, name, dir) {
if(!dir) dir = __dirname;
var id = path.resolve(dir, name, name + '.vm');
var vm = require('vm'),
// FIXME not log but debug module is required to provide information to the user
sandbox = {
id: id, // use this to gather kill info
needle: require('needle'),
log: log,
exports: {}
};
//TODO child_process to run module!
// Define max runtime per loop as 10 seconds, after that the child will be killed
// it can still be active after that if there was a timing function or a callback used...
// kill the child each time? how to determine whether there's still a token in the module?
try {
var mod = vm.runInNewContext(src, sandbox, id);
} catch (err) {
log.error('ML', 'Error running module in sandbox: ' + err.message);
}
return sandbox.exports;
};
exports.loadModule = function(directory, name, callback) {
try {
fs.readFile(path.resolve(__dirname, '..', directory, name, name + '.js'), 'utf8', function (err, data) {
if (err) {
log.error('LM', 'Loading module file!');
return;
}
var mod = exports.requireFromString(data, name, directory);
if(mod && fs.existsSync(path.resolve(__dirname, '..', directory, name, 'credentials.json'))) {
fs.readFile(path.resolve(__dirname, '..', directory, name, 'credentials.json'), 'utf8', function (err, auth) {
if (err) {
log.error('LM', 'Loading credentials file for "' + name + '"!');
callback(name, data, mod, null);
return;
}
if(mod.loadCredentials) mod.loadCredentials(JSON.parse(auth));
callback(name, data, mod, auth);
});
} else {
// Hand back the name, the string contents and the compiled module
callback(name, data, mod, null);
}
});
} catch(err) {
log.error('LM', 'Failed loading module "' + name + '"');
}
};
exports.loadModules = function(directory, callback) {
fs.readdir(path.resolve(__dirname, '..', directory), function (err, list) {
if (err) {
log.error('LM', 'loading modules directory: ' + err);
return;
}
log.print('LM', 'Loading ' + list.length + ' modules from "' + directory + '"');
list.forEach(function (file) {
fs.stat(path.resolve(__dirname, '..', directory, file), function (err, stat) {
if (stat && stat.isDirectory()) {
exports.loadModule(directory, file, callback);
}
});
});
});
};

View file

@ -12,12 +12,11 @@
var fs = require('fs'),
path = require('path'),
log = require('./logging'),
ml, db, funcLoadAction, funcLoadRule;
db, funcLoadAction, funcLoadRule;
exports = module.exports = function(args) {
args = args || {};
log(args);
ml = require('./module_loader')(args);
return module.exports;
};
@ -25,12 +24,82 @@ exports.addDBLink = function(db_link) {
db = db_link;
};
exports.requireFromString = function(src, name, dir) {
if(!dir) dir = __dirname;
var id = path.resolve(dir, name, name + '.vm');
var vm = require('vm'),
// FIXME not log but debug module is required to provide information to the user
sandbox = {
id: id, // use this to gather kill info
needle: require('needle'), //https://github.com/tomas/needle
log: log,
exports: {}
};
//TODO child_process to run module!
// Define max runtime per loop as 10 seconds, after that the child will be killed
// it can still be active after that if there was a timing function or a callback used...
// kill the child each time? how to determine whether there's still a token in the module?
try {
var mod = vm.runInNewContext(src, sandbox, id);
} catch (err) {
log.error('ML', 'Error running module in sandbox: ' + err.message);
}
return sandbox.exports;
};
exports.loadModule = function(directory, name, callback) {
try {
fs.readFile(path.resolve(__dirname, '..', directory, name, name + '.js'), 'utf8', function (err, data) {
if (err) {
log.error('LM', 'Loading module file!');
return;
}
var mod = exports.requireFromString(data, name, directory);
if(mod && fs.existsSync(path.resolve(__dirname, '..', directory, name, 'credentials.json'))) {
fs.readFile(path.resolve(__dirname, '..', directory, name, 'credentials.json'), 'utf8', function (err, auth) {
if (err) {
log.error('LM', 'Loading credentials file for "' + name + '"!');
callback(name, data, mod, null);
return;
}
if(mod.loadCredentials) mod.loadCredentials(JSON.parse(auth));
callback(name, data, mod, auth);
});
} else {
// Hand back the name, the string contents and the compiled module
callback(name, data, mod, null);
}
});
} catch(err) {
log.error('LM', 'Failed loading module "' + name + '"');
}
};
exports.loadModules = function(directory, callback) {
fs.readdir(path.resolve(__dirname, '..', directory), function (err, list) {
if (err) {
log.error('LM', 'loading modules directory: ' + err);
return;
}
log.print('LM', 'Loading ' + list.length + ' modules from "' + directory + '"');
list.forEach(function (file) {
fs.stat(path.resolve(__dirname, '..', directory, file), function (err, stat) {
if (stat && stat.isDirectory()) {
exports.loadModule(directory, file, callback);
}
});
});
});
};
exports.storeEventModule = function (objUser, obj, answHandler) {
try {
// TODO in the future we might want to link the modules close to the user
// and allow for e.g. private modules
// we need a child process to run this code and kill it after invocation
var m = ml.requireFromString(obj.data, obj.id);
var m = exports.requireFromString(obj.data, obj.id);
obj.methods = Object.keys(m);
answHandler.answerSuccess('Thank you for the event module!');
db.storeEventModule(obj.id, obj);
@ -48,7 +117,7 @@ exports.getAllEventModules = function ( objUser, obj, answHandler ) {
};
exports.storeActionModule = function (objUser, obj, answHandler) {
var m = ml.requireFromString(obj.data, obj.id);
var m = exports.requireFromString(obj.data, obj.id);
obj.methods = Object.keys(m);
answHandler.answerSuccess('Thank you for the action module!');
db.storeActionModule(obj.id, obj);
@ -111,100 +180,3 @@ exports.storeRule = function (objUser, obj, answHandler) {
}
};
// FIXME REMOVE
/*
* Legacy file system loaders
*/
/*
* Load Rules from fs
* ------------------
*/
exports.loadRulesFromFS = function(args, answHandler) {
if(!args) args = {};
if(!args.name) args.name = 'rules';
if(!funcLoadRule) log.error('ML', 'no rule loader function available');
else {
fs.readFile(path.resolve(__dirname, '..', 'rules', args.name + '.json'), 'utf8', function (err, data) {
if (err) {
log.error('ML', 'Loading rules file: ' + args.name + '.json');
return;
}
try {
var arr = JSON.parse(data), txt = '';
log.print('ML', 'Loading ' + arr.length + ' rules:');
for(var i = 0; i < arr.length; i++) {
txt += arr[i].id + ', ';
db.storeRule(arr[i].id, 'james-t', JSON.stringify(arr[i]));
// funcLoadRule(arr[i]);
}
answHandler.answerSuccess('Yep, loaded rules: ' + txt);
} catch (e) {
log.error('ML', 'rules file was corrupt! (' + args.name + '.json)');
}
});
}
};
/*
* Load Action Modules from fs
* ---------------------------
*/
/**
*
* @param {Object} name
* @param {Object} data
* @param {Object} mod
* @param {String} [auth] The string representation of the auth json
*/
function loadActionCallback(name, data, mod, auth) {
db.storeActionModule(name, data); // store module in db
// funcLoadAction(name, mod); // hand back compiled module
if(auth) db.storeActionModuleAuth(name, auth);
}
exports.loadActionModuleFromFS = function (args, answHandler) {
if(ml) {
if(args && args.name) {
answHandler.answerSuccess('Loading action module ' + args.name + '...');
ml.loadModule('mod_actions', args.name, loadActionCallback);
} else log.error('MM', 'Action Module name not provided!');
}
};
exports.loadActionModulesFromFS = function(args, answHandler) {
if(ml) {
answHandler.answerSuccess('Loading action modules...');
ml.loadModules('mod_actions', loadActionCallback);
}
};
/*
* Load Event Modules from fs
* --------------------------
*/
function loadEventCallback(name, data, mod, auth) {
if(db) {
db.storeEventModule(name, data); // store module in db
if(auth) db.storeEventModuleAuth(name, auth);
}
}
exports.loadEventModuleFromFS = function(args, answHandler) {
if(ml) {
if(args && args.name) {
answHandler.answerSuccess('Loading event module ' + args.name + '...');
ml.loadModule('mod_events', args.name, loadEventCallback);
} else log.error('MM', 'Event Module name not provided!');
}
};
exports.loadEventModulesFromFS = function(args, answHandler) {
answHandler.answerSuccess('Loading event moules...');
ml.loadModules('mod_actions', loadEventCallback);
};

66
js-coffee/new_logging.js Normal file
View file

@ -0,0 +1,66 @@
// Generated by CoffeeScript 1.6.3
(function() {
var bunyan, exports, path,
_this = this;
path = require('path');
bunyan = require('bunyan');
/*
Module call
-----------
Calling the module as a function will act as a constructor and load the config file.
It is possible to hand an args object with the properties nolog (true if no outputs shall
be generated) and configPath for a custom configuration file path.
@param {Object} args
*/
exports = module.exports = function(args) {
var e, emptylog, opt;
emptylog = {
info: function() {},
warn: function() {},
error: function() {}
};
args = args != null ? args : {};
if (args.nolog) {
return emptylog;
} else {
try {
opt = {
name: "webapi-eca"
};
if (args['mode'] === 'development') {
opt.src = true;
}
if (args['file-path']) {
_this.logPath = path.resolve(__dirname, '..', 'logs', args['file-path']);
} else {
_this.logPath = path.resolve(__dirname, '..', 'logs', 'server.log');
}
opt.streams = [
{
level: args['io-level'],
stream: process.stdout
}, {
level: args['file-level'],
type: 'rotating-file',
path: _this.logPath,
period: '1d',
count: 3
}
];
return bunyan.createLogger(opt);
} catch (_error) {
e = _error;
console.error(e);
return emptylog;
}
}
};
}).call(this);

View file

@ -8,7 +8,7 @@ Request Handler
(function() {
var answerHandler, crypto, db, exports, fs, getHandlerFileAsString, getHandlerPath, getIncludeFileAsString, log, mm, mustache, objAdminCmds, objUserCmds, path, qs, renderPage, sendLoginOrPage,
var answerHandler, crypto, db, exports, fs, getHandlerFileAsString, getHandlerPath, getIncludeFileAsString, log, mm, mustache, objUserCmds, path, qs, renderPage, sendLoginOrPage,
_this = this;
log = require('./logging');
@ -27,14 +27,6 @@ Request Handler
crypto = require('crypto-js');
objAdminCmds = {
'loadrules': mm.loadRulesFromFS,
'loadaction': mm.loadActionModuleFromFS,
'loadactions': mm.loadActionModulesFromFS,
'loadevent': mm.loadEventModuleFromFS,
'loadevents': mm.loadEventModulesFromFS
};
objUserCmds = {
'store_action': mm.storeActionModule,
'get_actionmodules': mm.getAllActionModules,
@ -59,17 +51,16 @@ Request Handler
};
/*
This allows the parent to add handlers. The event handler will receive
the events that were received. The shutdown function will be called if the
admin command shutdown is issued.
This allows the parent to add the shutdown handler.
The shutdown function will be called if the admin command shutdown is issued.
@public addHandlers( *fShutdown* )
@public addShutdownHandler( *fShutdown* )
@param {function} fShutdown
*/
exports.addHandlers = function(fShutdown) {
return objAdminCmds.shutdown = function(args, answerHandler) {
exports.addShutdownHandler = function(fShutdown) {
return _this.objAdminCmds.shutdown = function(args, answerHandler) {
answerHandler.answerSuccess('Shutting down... BYE!');
return setTimeout(fShutdown, 500);
};
@ -344,13 +335,13 @@ Request Handler
exports.handleAdmin = function(req, resp) {
var q, _name;
var q, _base, _name;
if (req.session && req.session.user) {
if (req.session.user.isAdmin === "true") {
q = req.query;
log.print('RH', 'Received admin request: ' + req.originalUrl);
if (q.cmd) {
return typeof objAdminCmds[_name = q.cmd] === "function" ? objAdminCmds[_name](q, answerHandler(req, resp, true)) : void 0;
return typeof (_base = _this.objAdminCmds)[_name = q.cmd] === "function" ? _base[_name](q, answerHandler(req, resp, true)) : void 0;
} else {
return resp.send(404, 'Command unknown!');
}

View file

@ -6,27 +6,16 @@ Server
>This is the main module that is used to run the whole server:
>
> node server [log_type http_port]
> node server [opt]
>
>Valid `log_type`'s are:
>
>- `0`: standard I/O output (default)
>- `1`: log file (server.log)
>- `2`: silent
>
>`http_port` can be set to use another port, than defined in the
>[config](config.html) file, to listen to, e.g. used by the test suite.
>
>
TODO how about we allow spawning child processes with servers based on address?
> See below in the optimist CLI preparation for allowed optional parameters
*/
(function() {
var args, conf, db, engine, http_listener, init, log, procCmds, shutDown;
var argv, conf, cp, db, engine, fs, http_listener, init, logger, opt, optimist, path, procCmds, shutDown, usage;
log = require('./logging');
logger = require('./new_logging');
conf = require('./config');
@ -36,10 +25,69 @@ TODO how about we allow spawning child processes with servers based on address?
http_listener = require('./http_listener');
args = {};
fs = require('fs');
path = require('path');
cp = require('child_process');
optimist = require('optimist');
procCmds = {};
/*
Let's prepare the optimist CLI
*/
usage = 'This runs your webapi-based ECA engine';
opt = {
'h': {
alias: 'help',
describe: 'Display this'
},
'c': {
alias: 'config-path',
describe: 'Specify a path to a custom configuration file, other than "config/config.json"'
},
'w': {
alias: 'http-port',
describe: 'Specify a HTTP port for the web server'
},
'd': {
alias: 'db-port',
describe: 'Specify a port for the redis DB'
},
'm': {
alias: 'log-mode',
describe: 'Specify a log mode: [development|productive]'
},
'i': {
alias: 'log-io-level',
describe: 'Specify the log level for the I/O'
},
'f': {
alias: 'log-file-level',
describe: 'Specify the log level for the log file'
},
'p': {
alias: 'log-file-path',
describe: 'Specify the path to the log file within the "logs" folder'
},
'n': {
alias: 'nolog',
describe: 'Set this if no output shall be generated'
}
};
argv = optimist.usage(usage).options(opt).argv;
if (argv.help) {
console.log(optimist.help());
process.exit();
}
/*
Error handling of the express port listener requires special attention,
thus we have to catch the process error, which is issued if
@ -50,7 +98,7 @@ TODO how about we allow spawning child processes with servers based on address?
process.on('uncaughtException', function(err) {
switch (err.errno) {
case 'EADDRINUSE':
err.addInfo = 'http_port already in use, shutting down!';
err.addInfo = 'http-port already in use, shutting down!';
log.error('RS', err);
return shutDown();
default:
@ -66,48 +114,54 @@ TODO how about we allow spawning child processes with servers based on address?
init = function() {
log.print('RS', 'STARTING SERVER');
conf(args);
var args, log, logconf;
conf(argv.c);
if (!conf.isReady()) {
log.error('RS', 'Config file not ready!');
console.error('FAIL: Config file not ready! Shutting down...');
process.exit();
}
if (process.argv.length > 2) {
args.logType = parseInt(process.argv[2]) || 0;
switch (args.logType) {
case 0:
log.print('RS', 'Log type set to standard I/O output');
break;
case 1:
log.print('RS', 'Log type set to file output');
break;
case 2:
log.print('RS', 'Log type set to silent');
break;
default:
log.print('RS', 'Unknown log type, using standard I/O');
}
log(args);
} else {
log.print('RS', 'No log method argument provided, using standard I/O');
logconf = conf.getLogConf();
if (argv.m) {
logconf['mode'] = argv.m;
}
if (process.argv.length > 3) {
args.http_port = parseInt(process.argv[3]);
} else {
log.print('RS', 'No HTTP port passed, using standard port from config file');
if (argv.i) {
logconf['io-level'] = argv.i;
}
log.print('RS', 'Initialzing DB');
if (argv.f) {
logconf['file-level'] = argv.f;
}
if (argv.p) {
logconf['file-path'] = argv.p;
}
if (argv.n) {
logconf['nolog'] = argv.n;
}
try {
fs.unlinkSync(path.resolve(__dirname, '..', 'logs', logconf['file-path']));
} catch (_error) {}
log = logger(logconf);
log.info('RS | STARTING SERVER');
args = {
logger: log,
logconf: logconf
};
args['http-port'] = parseInt(argv.w || conf.getHttpPort());
log.info('RS | Initialzing DB');
db(args);
return db.isConnected(function(err, result) {
var cliArgs, poller;
if (!err) {
log.print('RS', 'Initialzing engine');
log.info('RS | Initialzing engine');
engine(args);
log.print('RS', 'Initialzing http listener');
log.info('RS | Initialzing http listener');
http_listener(args);
log.print('RS', 'Passing handlers to engine');
log.info('RS | Passing handlers to engine');
engine.addPersistence(db);
log.print('RS', 'Passing handlers to http listener');
return http_listener.addHandlers(shutDown);
log.info('RS | Passing handlers to http listener');
http_listener.addShutdownHandler(shutDown);
log.info('RS | For e child process for the event poller');
cliArgs = [args.logconf['mode'], args.logconf['io-level'], args.logconf['file-level'], args.logconf['file-path'], args.logconf['nolog']];
return poller = cp.fork(path.resolve(__dirname, 'event_poller'), cliArgs);
}
});
};
@ -120,7 +174,7 @@ TODO how about we allow spawning child processes with servers based on address?
shutDown = function() {
log.print('RS', 'Received shut down command!');
log.warn('RS | Received shut down command!');
if (engine != null) {
engine.shutDown();
}

View file

@ -9,7 +9,9 @@
* - 1 file
* - 2 silent
*/
//TODO dynamic log file names (especially to track unit test logs)
var fs = require('fs'),
wst = require('winston'),
logTypes = [ flushToConsole, flushToFile, null],
logFile = require('path').resolve(__dirname, '..', 'server.log'),
logType = 0;
@ -19,6 +21,8 @@ exports = module.exports = function(args) {
if(args.logType) logType = parseInt(args.logType) || 0;
if(logType == 1) fs.truncateSync(logFile, 0);
if(logType > logTypes.length - 1) logType = 0;
// winston.add(winston.transports.File, { filename: 'somefile.log' });
// winston.remove(winston.transports.Console);
return module.exports;
};
@ -29,8 +33,10 @@ function flush(err, msg) {
}
function flushToConsole(err, msg) {
if(err) console.error(msg);
if(err) console.error("\033[31m" + msg + "\033[0m");
else console.log(msg);
// if(err) console.error(msg);
// else console.log(msg);
}
function flushToFile(err, msg) {
@ -63,13 +69,13 @@ function printError(module, err, isSevere) {
// if(module) flush(true, ts + module + ' | ERROR AND BAD HANDLING: ' + err + '\n' + e.stack);
// else flush(true, ts + '!N/A! | ERROR, BAD HANDLING AND NO MODULE NAME: ' + err + '\n' + e.stack);
// } else if(err) {
if(err.addInfo) ai = ' (' + err.addInfo + ')';
if(!err.message) err.message = 'UNKNOWN REASON!\n' + err.stack;
if(module) {
var msg = ts + module + ' | ERROR'+ai+': ' + err.message;
if(isSevere) msg += '\n' + err.stack;
flush(true, msg);
} else flush(true, ts + '!N/A! | ERROR AND NO MODULE NAME'+ai+': ' + err.message + '\n' + err.stack);
if(err.addInfo) ai = ' (' + err.addInfo + ')';
if(!err.message) err.message = 'UNKNOWN REASON!\n' + err.stack;
if(module) {
var msg = ts + module + ' | ERROR'+ai+': ' + err.message;
if(isSevere) msg += '\n' + err.stack;
flush(true, msg);
} else flush(true, ts + '!N/A! | ERROR AND NO MODULE NAME'+ai+': ' + err.message + '\n' + err.stack);
// } else {
// var e = new Error('Unexpected error');
// flush(true, e.message + ': \n' + e.stack);
@ -93,3 +99,9 @@ exports.error = function(module, err) {
exports.severe = function(module, err) {
printError(module, err, true);
};
exports.obj = function (varname, obj) {
var arrS = (new Error).stack.split('\n');
console.log('Dumping object "' + varname + '"' + arrS[2]);
console.log(obj);
};

View file

@ -18,7 +18,9 @@
"nodeunit": "0.8.4",
"redis": "0.10.0",
"request": "2.33.0",
"coffee-script": "1.6.3"
"coffee-script": "1.6.3",
"bunyan": "0.22.1",
"optimist": "0.6.1"
},
"__comment": {
"dependencies": {

View file

@ -1,3 +1,3 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
node $DIR/js-coffee/server
node $DIR/js-coffee/server | $DIR/node_modules/bunyan/bin/bunyan

View file

@ -1,2 +1,15 @@
#!/usr/bin/env node
require('nodeunit').reporters.default.run(['testing']);
process.chdir(__dirname);
var fs = require('fs');
var path = require('path');
var args = process.argv.slice(2);
if( args[0] !== undefined ) {
var fl = path.join('testing', args[0]);
if (fs.existsSync(fl)) {
require('nodeunit').reporters.default.run([fl]);
} else {
console.error('File not found!!');
}
} else {
require('nodeunit').reporters.default.run(['testing']);
}

View file

@ -0,0 +1,5 @@
// Generated by CoffeeScript 1.6.3
(function() {
}).call(this);

View file

@ -0,0 +1,87 @@
// Generated by CoffeeScript 1.6.3
(function() {
var _this = this;
exports.setUp = function(cb) {
_this.path = require('path');
_this.db = require(path.join('..', 'js-coffee', 'persistence'));
console.log('setup');
console.log(_this.db);
_this.db({
logType: 2
});
return cb();
};
exports.tearDown = function(cb) {
_this.db.shutDown();
return cb();
};
/*
# Test AVAILABILITY
*/
exports.Availability = {
setUp: function(cb) {
_this.path = require('path');
_this.db = require(path.join('..', 'js-coffee', 'persistence'));
console.log('setup');
console.log(_this.db);
_this.db({
logType: 2
});
return cb();
},
testRequire: function(test) {
test.expect(1);
console.log('setup');
test.ok(_this.db, 'DB interface loaded');
return test.done();
},
testConnect: function(test) {
test.expect(1);
return _this.db.isConnected(function(err) {
test.ifError(err, 'Connection failed!');
return test.done();
});
},
testNoConfig: function(test) {
test.expect(1);
_this.db({
configPath: 'nonexistingconf.file'
});
return _this.db.isConnected(function(err) {
test.ok(err, 'Still connected!?');
return test.done();
});
},
testWrongConfig: function(test) {
test.expect(1);
_this.db({
configPath: _this.path.join('testing', 'jsonWrongConfig.json')
});
return _this.db.isConnected(function(err) {
test.ok(err, 'Still connected!?');
return test.done();
});
},
testPurgeQueue: function(test) {
var evt;
test.expect(2);
evt = {
eventid: '1',
event: 'mail'
};
_this.db.pushEvent(evt);
_this.db.purgeEventQueue();
return _this.db.popEvent(function(err, obj) {
test.ifError(err, 'Error during pop after purging!');
test.strictEqual(obj, null, 'There was an event in the queue!?');
return test.done();
});
}
};
}).call(this);

View file

@ -3,7 +3,7 @@ path = require 'path'
exports.setUp = ( cb ) =>
@conf = require path.join '..', 'js-coffee', 'config'
@conf
logType: 2
nolog: true
cb()
exports.testRequire = ( test ) =>
@ -12,23 +12,34 @@ exports.testRequire = ( test ) =>
test.done()
exports.testParameters = ( test ) =>
test.expect 4
reqProp = [
'mode'
'io-level'
'file-level'
'file-path'
]
test.expect 4 + reqProp.length
test.ok @conf.getHttpPort(), 'HTTP port does not exist!'
test.ok @conf.getDBPort(), 'DB port does not exist!'
test.ok @conf.getCryptoKey(), 'Crypto key does not exist!'
test.ok @conf.getSessionSecret(), 'Session Secret does not exist!'
logconf = @conf.getLogConf()
test.ok logconf, 'Log config does not exist!'
for prop in reqProp
test.ok logconf[prop], "Log conf property #{ prop } does not exist!"
test.done()
exports.testDifferentConfigFile = ( test ) =>
test.expect 1
@conf
@conf
nolog: true
configPath: path.join 'testing', 'files', 'jsonWrongConfig.json'
test.ok @conf.isReady(), 'Different path not loaded!'
test.done()
exports.testNoConfigFile = ( test ) =>
test.expect 1
@conf
@conf
nolog: true
configPath: 'wrongpath.file'
test.strictEqual @conf.isReady(), false, 'Wrong path still loaded!'
test.done()

View file

@ -0,0 +1,68 @@
exports.setUp = ( cb ) =>
@fs = require 'fs'
@path = require 'path'
@stdPath = @path.resolve __dirname, '..', 'logs', 'server.log'
try
@fs.unlinkSync @stdPath
catch e
@log = require @path.join '..', 'js-coffee', 'new_logging'
cb()
exports.tearDown = ( cb ) =>
cb()
exports.testInitIO = ( test ) =>
test.expect 1
conf =
logType: 0
@log.configure conf
@log.info 'TL', 'testInitIO - info'
@log.warn 'TL', 'testInitIO - warn'
@log.error 'TL', 'testInitIO - error'
test.ok !@fs.existsSync @stdPath
test.done()
exports.testInitFile = ( test ) =>
test.expect 1
conf =
logType: 1
@log.configure conf
@log.info 'UT', 'test 1'
fWait = () =>
@log.info 'UT', 'test 2'
test.ok @fs.existsSync @stdPath
test.done()
setTimeout fWait, 100
# exports.testInitSilent = ( test ) =>
# test.expect 1
# conf =
# logType: 2
# @log.configure conf
# @log.info 'test 3'
# test.ok true, 'yay'
# test.done()
# exports.testInitPath = ( test ) =>
# test.expect 1
# conf =
# logType: 1
# logPath: 'testing/log-initPath.log'
# @log.configure conf
# @log.info 'test 3'
# test.ok true, 'yay'
# test.done()
# exports.testPrint = ( test ) =>
# test.expect 1
# test.ok true, 'yay'
# @log.info 'test 3'
# test.done()

View file

@ -1,6 +1,7 @@
exports.setUp = ( cb ) =>
@db = require '../js-coffee/persistence'
@path = require 'path'
@db = require @path.join '..', 'js-coffee', 'persistence'
@db logType: 2
cb()
@ -38,7 +39,7 @@ exports.Availability =
testWrongConfig: ( test ) =>
test.expect 1
@db { configPath: 'testing/jsonWrongConfig.json' }
@db { configPath: @path.join 'testing', 'jsonWrongConfig.json' }
@db.isConnected ( err ) ->
test.ok err, 'Still connected!?'
test.done()