diff --git a/coffee/config.coffee b/coffee/config.coffee index 105f275..1a014d8 100644 --- a/coffee/config.coffee +++ b/coffee/config.coffee @@ -6,7 +6,8 @@ Configuration ### -# **Requires:** +# **Loads Modules:** + # - Node.js Modules: [fs](http://nodejs.org/api/fs.html) and # [path](http://nodejs.org/api/path.html) fs = require 'fs' @@ -45,7 +46,6 @@ loadConfigFile = ( configPath ) => 'log' 'http-port' 'db-port' - 'crypto-key' ] #TODO Try to get rid of crypto key try @@ -89,7 +89,7 @@ exports.getHttpPort = -> fetchProp 'http-port' @public getDBPort() ### -exports.getDBPort = -> fetchProp 'db-port' +exports.getDbPort = -> fetchProp 'db-port' ### ***Returns*** the log conf object diff --git a/coffee/http-listener.coffee b/coffee/http-listener.coffee index 10eb39a..3b55351 100644 --- a/coffee/http-listener.coffee +++ b/coffee/http-listener.coffee @@ -8,10 +8,7 @@ HTTP Listener ### -# **Requires:** - -# - [Logging](logging.html) -log = require './logging' +# **Loads Modules:** # - [Request Handler](request-handler.html) requestHandler = require './request-handler' @@ -25,7 +22,8 @@ qs = require 'querystring' express = require 'express' app = express() -#RedisStore = require('connect-redis')(express), # TODO use RedisStore for persistent sessions +#TODO use RedisStore for persistent sessions +#RedisStore = require('connect-redis')(express), ### Module call @@ -34,9 +32,8 @@ Initializes the HTTP listener and its request handler. @param {Object} args ### -exports = module.exports = ( args ) -> - args = args ? {} - log args +exports = module.exports = ( args ) => + @log = args.logger requestHandler args initRouting args[ 'http-port' ] module.exports @@ -47,15 +44,15 @@ Initializes the request routing and starts listening on the given port. @param {int} port @private initRouting( *fShutDown* ) ### -initRouting = ( port ) -> +initRouting = ( port ) => # Add cookie support for session handling. app.use express.cookieParser() - #TODO The session secret appriach needs to be fixed! + #TODO The session secret approach needs to be fixed! sess_sec = "149u*y8C:@kmN/520Gt\\v'+KFBnQ!\\r<>5X/xRI`sT # - **`POST` to _"/user"_:** User requests are possible for all users with an account app.post '/usercommand', requestHandler.handleUserCommand try - app.listen port # inbound event channel + @server = app.listen port # inbound event channel catch e - e.addInfo = 'opening port' - log.error e + @log.error e, 'HL | Unable to listen...' ### Adds the shutdown handler to the admin commands. @@ -102,7 +98,11 @@ Shuts down the http listener. @public shutDown() ### -exports.shutDown = () -> - log.print 'HL', 'Shutting down HTTP listener' - process.exit() # This is a bit brute force... +exports.shutDown = () => + @log.warn 'HL | Shutting down HTTP listener' + console.log 'apppp' + console.log app + @server.close() + #TODO This is a bit brute force... + #process.exit() diff --git a/coffee/logging.coffee b/coffee/logging.coffee index 19c672e..a1061da 100644 --- a/coffee/logging.coffee +++ b/coffee/logging.coffee @@ -9,7 +9,7 @@ # `node myapp.js | bunyan` -# **Requires:** +# **Loads Modules:** # - Node.js Module: [fs](http://nodejs.org/api/fs.html) and [path](http://nodejs.org/api/path.html) fs = require 'fs' diff --git a/coffee/persistence.coffee b/coffee/persistence.coffee index 6cfdc63..7f2e7d3 100644 --- a/coffee/persistence.coffee +++ b/coffee/persistence.coffee @@ -19,7 +19,7 @@ Persistence ### -# **Requires:** +# **Loads Modules:** # - External Modules: # [crypto-js](https://github.com/evanvosberg/crypto-js) and @@ -30,28 +30,21 @@ redis = require 'redis' ### Module call ----------- -Initializes the DB connection. Requires a valid configuration file which contains -a db port and a crypto key. +Initializes the DB connection with the given `db-port` property in the `args` object. @param {Object} args ### exports = module.exports = ( args ) => @log = args.logger - #TODO remove config, do it through args - config = require './config' - config args @db?.quit() - if config.isReady() - @crypto_key = config.getCryptoKey() - @db = redis.createClient config.getDBPort(), - 'localhost', { connect_timeout: 2000 } - @db.on 'error', ( err ) => - err.addInfo = 'message from DB' - @log.error 'DB', err - @ep = new IndexedModules( 'event-poller', @db, @log ) - @ai = new IndexedModules( 'action-invoker', @db, @log ) - else - @log.error 'DB', 'Initialization failed because of missing config file!' + #TODO we need to have a secure concept here, private keys per user + @crypto_key = "}f6y1y}B{.an$}2c$Yl.$mSnF\\HX149u*y8C:@kmN/520Gt\\v'+KFBnQ!\\r<>5X/xRI`sT + @log.warn err, 'message from DB' + @ep = new IndexedModules( 'event-poller', @db, @log ) + @ai = new IndexedModules( 'action-invoker', @db, @log ) ### Checks whether the db is connected and passes either an error on failure after @@ -66,7 +59,7 @@ exports.isConnected = ( cb ) => numAttempts = 0 fCheckConnection = => if @db.connected - @log.info 'DB', 'Successfully connected to DB!' + @log.info 'DB | Successfully connected to DB!' cb() else if numAttempts++ < 10 setTimeout fCheckConnection, 100 @@ -83,10 +76,9 @@ Abstracts logging for simple action replies from the DB. replyHandler = ( action ) => ( err, reply ) => if err - err.addInfo = "during '#{ action }'" - @log.error 'DB', err + @log.warn err, "during '#{ action }'" else - @log.info 'DB', "#{ action }: #{ reply }" + @log.info "DB | #{ action }: #{ reply }" ### Push an event into the event queue. @@ -96,10 +88,10 @@ Push an event into the event queue. ### exports.pushEvent = ( oEvent ) => if oEvent - @log.info 'DB', "Event pushed into the queue: '#{ oEvent.eventid }'" + @log.info "DB | Event pushed into the queue: '#{ oEvent.eventid }'" @db.rpush 'event_queue', JSON.stringify( oEvent ) else - @log.error 'DB', 'Why would you give me an empty event...' + @log.warn 'DB | Why would you give me an empty event...' ### @@ -133,8 +125,7 @@ hash = ( plainText ) => try ( crypto.SHA3 plainText, { outputLength: 512 } ).toString() catch err - err.addInfo = 'during hashing' - @log.error 'DB', err + @log.warn err, 'DB | during hashing' null @@ -149,8 +140,7 @@ encrypt = ( plainText ) => try crypto.AES.encrypt plainText, @crypto_key catch err - err.addInfo = 'during encryption' - @log.error 'DB', err + @log.warn err, 'DB | during encryption' null ### @@ -165,8 +155,7 @@ decrypt = ( crypticText ) => dec = crypto.AES.decrypt crypticText, @crypto_key dec.toString(crypto.enc.Utf8) catch err - err.addInfo = 'during decryption' - @log.error 'DB', err + @log.warn err, 'DB | during decryption' null ### @@ -181,13 +170,12 @@ data objects via the provided function and returns the results to cb(err, obj). the retrieved data or an error ### getSetRecords = ( set, fSingle, cb ) => - @log.info 'DB', "Fetching set records: '#{ set }'" + @log.info "DB | Fetching set records: '#{ set }'" # Fetch all members of the set @db.smembers set, ( err, arrReply ) => if err # If an error happens we return it to the callback function - err.addInfo = "fetching '#{ set }'" - @log.error 'DB', err + @log.warn err, "DB | fetching '#{ set }'" cb err else if arrReply.length == 0 # If the set was empty we return null to the callback @@ -210,11 +198,10 @@ getSetRecords = ( set, fSingle, cb ) => ( err, data ) => --semaphore if err - err.addInfo = "fetching single element: '#{ prop }'" - @log.error 'DB', err + @log.warn err, "DB | fetching single element: '#{ prop }'" else if not data # There was no data behind the key - @log.error 'DB', new Error "Empty key in DB: '#{ prop }'" + @log.warn new Error "Empty key in DB: '#{ prop }'" else # We found a valid record and add it to the reply object objReplies[ prop ] = data @@ -229,52 +216,52 @@ getSetRecords = ( set, fSingle, cb ) => class IndexedModules constructor: ( @setname, @db, @log ) -> - @log.info 'DB', "Instantiated indexed modules for '#{ @setname }'" + @log.info "DB | Instantiated indexed modules for '#{ @setname }'" storeModule: ( mId, data ) => - @log.info 'DB', "storeModule(#{ @setname }): #{ mId }" + @log.info "DB | storeModule(#{ @setname }): #{ mId }" @db.sadd "#{ @setname }s", mId, replyHandler "Storing '#{ @setname }' key '#{ mId }'" @db.set "#{ @setname }:#{ mId }", data, replyHandler "Storing '#{ @setname }:#{ mId }'" getModule: ( mId, cb ) => - @log.info 'DB', "getModule('#{ @setname }): #{ mId }'" + @log.info "DB | getModule('#{ @setname }): #{ mId }'" @db.get "#{ @setname }:#{ mId }", cb getModuleIds: ( cb ) => - @log.info 'DB', "getModuleIds(#{ @setname })" + @log.info "DB | getModuleIds(#{ @setname })" @db.smembers "#{ @setname }s", cb getModules: ( cb ) => - @log.info 'DB', "getModules(#{ @setname })" + @log.info "DB | getModules(#{ @setname })" getSetRecords "#{ @setname }s", @getModule, cb deleteModule: ( mId ) => - @log.info 'DB', "deleteModule(#{ @setname }): #{ mId }" + @log.info "DB | deleteModule(#{ @setname }): #{ mId }" @db.srem "#{ @setname }s", mId, replyHandler "Deleting '#{ @setname }' key '#{ mId }'" @db.del "#{ @setname }:#{ mId }", replyHandler "Deleting '#{ @setname }:#{ mId }'" storeParameters: ( mId, userId, data ) => - @log.info 'DB', "storeParameters(#{ @setname }): '#{ mId }:#{ userId }'" + @log.info "DB | storeParameters(#{ @setname }): '#{ mId }:#{ userId }'" @db.sadd "#{ @setname }-params", "#{ mId }:#{ userId }", replyHandler "Storing '#{ @setname }' module parameters key '#{ mId }'" @db.set "#{ @setname }-params:#{ mId }:#{ userId }", encrypt(data), replyHandler "Storing '#{ @setname }' module parameters '#{ mId }:#{ userId }'" getParameters: ( mId, userId, cb ) => - @log.info 'DB', "getParameters(#{ @setname }): '#{ mId }:#{ userId }'" + @log.info "DB | getParameters(#{ @setname }): '#{ mId }:#{ userId }'" @db.get "#{ @setname }-params:#{ mId }:#{ userId }", ( err, data ) -> cb err, decrypt data getParametersIds: ( cb ) => - @log.info 'DB', "getParametersIds(#{ @setname })" + @log.info "DB | getParametersIds(#{ @setname })" @db.smembers "#{ @setname }-params", cb deleteParameters: ( mId, userId ) => - @log.info 'DB', "deleteParameters(#{ @setname }): '#{ mId }:#{ userId }'" + @log.info "DB | deleteParameters(#{ @setname }): '#{ mId }:#{ userId }'" @db.srem "#{ @setname }-params", "#{ mId }:#{ userId }", replyHandler "Deleting '#{ @setname }-params' key '#{ mId }:#{ userId }'" @db.del "#{ @setname }-params:#{ mId }:#{ userId }", @@ -479,7 +466,7 @@ Query the DB for a rule and pass it to cb(err, obj). @param {function} cb ### exports.getRule = ( ruleId, cb ) => - @log.info 'DB', "getRule: '#{ ruleId }'" + @log.info "DB | getRule: '#{ ruleId }'" @db.get "rule:#{ ruleId }", cb ### @@ -489,7 +476,7 @@ Fetch all rules and pass them to cb(err, obj). @param {function} cb ### exports.getRules = ( cb ) => - @log.info 'DB', 'Fetching all Rules' + @log.info 'DB | Fetching all Rules' getSetRecords 'rules', exports.getRule, cb ### @@ -499,7 +486,7 @@ Fetch all rule IDs and hand it to cb(err, obj). @param {function} cb ### exports.getRuleIds = ( cb ) => - @log.info 'DB', 'Fetching all Rule IDs' + @log.info 'DB | Fetching all Rule IDs' @db.smembers 'rules', cb ### @@ -510,7 +497,7 @@ Store a string representation of a rule in the DB. @param {String} data ### exports.storeRule = ( ruleId, data ) => - @log.info 'DB', "storeRule: '#{ ruleId }'" + @log.info "DB | storeRule: '#{ ruleId }'" @db.sadd 'rules', "#{ ruleId }", replyHandler "storing rule key '#{ ruleId }'" @db.set "rule:#{ ruleId }", data, @@ -524,7 +511,7 @@ Delete a string representation of a rule. @param {String} userId ### exports.deleteRule = ( ruleId ) => - @log.info 'DB', "deleteRule: '#{ ruleId }'" + @log.info "DB | deleteRule: '#{ ruleId }'" @db.srem "rules", ruleId, replyHandler "Deleting rule key '#{ ruleId }'" @db.del "rule:#{ ruleId }", replyHandler "Deleting rule '#{ ruleId }'" @@ -552,7 +539,7 @@ Associate a rule to a user. @param {String} userId ### exports.linkRule = ( ruleId, userId ) => - @log.info 'DB', "linkRule: '#{ ruleId }' for user '#{ userId }'" + @log.info "DB | linkRule: '#{ ruleId }' for user '#{ userId }'" @db.sadd "rule:#{ ruleId }:users", userId, replyHandler "storing user '#{ userId }' for rule key '#{ ruleId }'" @db.sadd "user:#{ userId }:rules", ruleId, @@ -566,7 +553,7 @@ Get rules linked to a user and hand it to cb(err, obj). @param {function} cb ### exports.getUserLinkedRules = ( userId, cb ) => - @log.info 'DB', "getUserLinkedRules: for user '#{ userId }'" + @log.info "DB | getUserLinkedRules: for user '#{ userId }'" @db.smembers "user:#{ userId }:rules", cb ### @@ -577,7 +564,7 @@ Get users linked to a rule and hand it to cb(err, obj). @param {function} cb ### exports.getRuleLinkedUsers = ( ruleId, cb ) => - @log.info 'DB', "getRuleLinkedUsers: for rule '#{ ruleId }'" + @log.info "DB | getRuleLinkedUsers: for rule '#{ ruleId }'" @db.smembers "rule:#{ ruleId }:users", cb ### @@ -588,7 +575,7 @@ Delete an association of a rule to a user. @param {String} userId ### exports.unlinkRule = ( ruleId, userId ) => - @log.info 'DB', "unlinkRule: '#{ ruleId }:#{ userId }'" + @log.info "DB | unlinkRule: '#{ ruleId }:#{ userId }'" @db.srem "rule:#{ ruleId }:users", userId, replyHandler "removing user '#{ userId }' for rule key '#{ ruleId }'" @db.srem "user:#{ userId }:rules", ruleId, @@ -602,7 +589,7 @@ Activate a rule. @param {String} userId ### exports.activateRule = ( ruleId, userId ) => - @log.info 'DB', "activateRule: '#{ ruleId }' for '#{ userId }'" + @log.info "DB | activateRule: '#{ ruleId }' for '#{ userId }'" @db.sadd "rule:#{ ruleId }:active-users", userId, replyHandler "storing activated user '#{ userId }' in rule '#{ ruleId }'" @db.sadd "user:#{ userId }:active-rules", ruleId, @@ -616,7 +603,7 @@ Get rules activated for a user and hand it to cb(err, obj). @param {function} cb ### exports.getUserActivatedRules = ( userId, cb ) => - @log.info 'DB', "getUserActivatedRules: for user '#{ userId }'" + @log.info "DB | getUserActivatedRules: for user '#{ userId }'" @db.smembers "user:#{ userId }:active-rules", cb ### @@ -627,7 +614,7 @@ Get users activated for a rule and hand it to cb(err, obj). @param {function} cb ### exports.getRuleActivatedUsers = ( ruleId, cb ) => - @log.info 'DB', "getRuleActivatedUsers: for rule '#{ ruleId }'" + @log.info "DB | getRuleActivatedUsers: for rule '#{ ruleId }'" @db.smembers "rule:#{ ruleId }:active-users", cb ### @@ -638,7 +625,7 @@ Deactivate a rule. @param {String} userId ### exports.deactivateRule = ( ruleId, userId ) => - @log.info 'DB', "deactivateRule: '#{ ruleId }' for '#{ userId }'" + @log.info "DB | deactivateRule: '#{ ruleId }' for '#{ userId }'" @db.srem "rule:#{ ruleId }:active-users", userId, replyHandler "removing activated user '#{ userId }' in rule '#{ ruleId }'" @db.srem "user:#{ userId }:active-rules", ruleId, @@ -651,7 +638,7 @@ Fetch all active ruleIds and pass them to cb(err, obj). @param {function} cb ### exports.getAllActivatedRuleIdsPerUser = ( cb ) => - @log.info 'DB', "Fetching all active rules" + @log.info "DB | Fetching all active rules" @db.smembers 'users', ( err, obj ) => result = {} if obj.length is 0 @@ -681,7 +668,7 @@ The password should be hashed before it is passed to this function. exports.storeUser = ( objUser ) => #TODO Only store user if not already existing, or at least only then add a private key #for his encryption. we would want to have one private key per user, right? - @log.info 'DB', "storeUser: '#{ objUser.username }'" + @log.info "DB | storeUser: '#{ objUser.username }'" if objUser and objUser.username and objUser.password @db.sadd 'users', objUser.username, replyHandler "storing user key '#{ objUser.username }'" @@ -689,7 +676,7 @@ exports.storeUser = ( objUser ) => @db.hmset "user:#{ objUser.username }", objUser, replyHandler "storing user properties '#{ objUser.username }'" else - @log.error 'DB', new Error 'username or password was missing' + @log.warn new Error 'DB | username or password was missing' ### Fetch all user IDs and pass them to cb(err, obj). @@ -698,7 +685,7 @@ Fetch all user IDs and pass them to cb(err, obj). @param {function} cb ### exports.getUserIds = ( cb ) => - @log.info 'DB', "getUserIds" + @log.info "DB | getUserIds" @db.smembers "users", cb ### @@ -709,7 +696,7 @@ Fetch a user by id and pass it to cb(err, obj). @param {function} cb ### exports.getUser = ( userId, cb ) => - @log.info 'DB', "getUser: '#{ userId }'" + @log.info "DB | getUser: '#{ userId }'" @db.hgetall "user:#{ userId }", cb ### @@ -719,7 +706,7 @@ Deletes a user and all his associated linked and active rules. @param {String} userId ### exports.deleteUser = ( userId ) => - @log.info 'DB', "deleteUser: '#{ userId }'" + @log.info "DB | deleteUser: '#{ userId }'" @db.srem "users", userId, replyHandler "Deleting user key '#{ userId }'" @db.del "user:#{ userId }", replyHandler "Deleting user '#{ userId }'" @@ -763,14 +750,14 @@ because we only store hashes of passwords for security6 reasons. ### #TODO verify and test whole function exports.loginUser = ( userId, password, cb ) => - @log.info 'DB', "User '#{ userId }' tries to log in" + @log.info "DB | User '#{ userId }' tries to log in" fCheck = ( pw ) => ( err, obj ) => if err cb err, null else if obj and obj.password if pw == obj.password - @log.info 'DB', "User '#{ obj.username }' logged in!" + @log.info "DB | User '#{ obj.username }' logged in!" cb null, obj else cb (new Error 'Wrong credentials!'), null @@ -793,7 +780,7 @@ Associate a role with a user. @param {String} role ### exports.storeUserRole = ( userId, role ) => - @log.info 'DB', "storeUserRole: '#{ userId }:#{ role }'" + @log.info "DB | storeUserRole: '#{ userId }:#{ role }'" @db.sadd 'roles', role, replyHandler "adding role '#{ role }' to role index set" @db.sadd "user:#{ userId }:roles", role, replyHandler "adding role '#{ role }' to user '#{ userId }'" @@ -808,7 +795,7 @@ Fetch all roles of a user and pass them to cb(err, obj). @param {function} cb ### exports.getUserRoles = ( userId, cb ) => - @log.info 'DB', "getUserRoles: '#{ userId }'" + @log.info "DB | getUserRoles: '#{ userId }'" @db.smembers "user:#{ userId }:roles", cb ### @@ -819,7 +806,7 @@ Fetch all users of a role and pass them to cb(err, obj). @param {function} cb ### exports.getRoleUsers = ( role, cb ) => - @log.info 'DB', "getRoleUsers: '#{ role }'" + @log.info "DB | getRoleUsers: '#{ role }'" @db.smembers "role:#{ role }:users", cb ### @@ -830,7 +817,7 @@ Remove a role from a user. @param {String} userId ### exports.removeUserRole = ( userId, role ) => - @log.info 'DB', "removeRoleFromUser: role '#{ role }', user '#{ userId }'" + @log.info "DB | removeRoleFromUser: role '#{ role }', user '#{ userId }'" @db.srem "user:#{ userId }:roles", role, replyHandler "Removing role '#{ role }' from user '#{ userId }'" @db.srem "role:#{ role }:users", userId, diff --git a/coffee/request-handler.coffee b/coffee/request-handler.coffee index 6c4f5b5..fb15e78 100644 --- a/coffee/request-handler.coffee +++ b/coffee/request-handler.coffee @@ -2,14 +2,14 @@ Request Handler ============ -> TODO Add documentation +> The request handler (surprisingly) handles requests made through HTTP to +> the [HTTP Listener](http-listener.html). It will handle user requests for +> pages as well as POST requests such as user login, module storing, event +> invocation and also admin commands. ### -# **Requires:** - -# - [Logging](logging.html) -log = require './logging' +# **Loads Modules:** # - [Persistence](persistence.html) db = require './persistence' @@ -40,8 +40,7 @@ objUserCmds = objAdminCmds = {} exports = module.exports = ( args ) -> - args = args ? {} - log args + log = args.logger db args mm args mm.addDBLink db @@ -105,7 +104,7 @@ exports.handleLogin = ( req, resp ) -> db.loginUser obj.username, obj.password, ( err, usr ) -> if(err) # Tapping on fingers, at least in log... - log.print 'RH', "AUTH-UH-OH (#{obj.username}): " + err.message + log.warn err, "RH | AUTH-UH-OH (#{obj.username})" else # no error, so we can associate the user object from the DB to the session req.session.user = usr @@ -280,7 +279,7 @@ 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 + log.info 'RH | Received admin request: ' + req.originalUrl if q.cmd @objAdminCmds[q.cmd]? q, answerHandler req, resp, true else diff --git a/coffee/sandbox.coffee b/coffee/sandbox.coffee deleted file mode 100644 index 7d77c21..0000000 --- a/coffee/sandbox.coffee +++ /dev/null @@ -1,16 +0,0 @@ -bunyan = require 'bunyan' -opt = - name: "webapi-eca" -opt.streams = [ - { - level: 'info' - stream: process.stdout - }, - { - level: 'info' - path: 'logs/server.log' - } - ] -# Finally we create the bunyan logger -logger = bunyan.createLogger opt -logger.info 'weeee' \ No newline at end of file diff --git a/coffee/webapi-eca.coffee b/coffee/webapi-eca.coffee index 7e0befc..37cdb76 100644 --- a/coffee/webapi-eca.coffee +++ b/coffee/webapi-eca.coffee @@ -10,7 +10,7 @@ WebAPI-ECA Engine > See below in the optimist CLI preparation for allowed optional parameters `[opt]`. ### -# **Requires:** +# **Loads Modules:** # - [Logging](logging.html) logger = require './logging' @@ -92,20 +92,19 @@ Error handling of the express port listener requires special attention, thus we have to catch the process error, which is issued if the port is already in use. ### -process.on 'uncaughtException', ( err ) -> +process.on 'uncaughtException', ( err ) => switch err.errno when 'EADDRINUSE' - err.addInfo = 'http-port already in use, shutting down!' - log.error 'RS', err + @log.error err, 'RS | http-port already in use, shutting down!' shutDown() - # else log.error 'RS', err + # else @log.error 'RS', err else throw err ### This function is invoked right after the module is loaded and starts the server. @private init() ### -init = -> +init = => conf argv.c # > Check whether the config file is ready, which is required to start the server. if !conf.isReady() @@ -125,36 +124,39 @@ init = -> logconf[ 'nolog' ] = argv.n try fs.unlinkSync path.resolve __dirname, '..', 'logs', logconf[ 'file-path' ] - log = logger logconf - log.info 'RS | STARTING SERVER' + @log = logger.getLogger logconf + @log.info 'RS | STARTING SERVER' args = - logger: log + logger: @log logconf: logconf # > Fetch the `http-port` argument args[ 'http-port' ] = parseInt argv.w || conf.getHttpPort() + args[ 'db-port' ] = parseInt argv.w || conf.getDbPort() - log.info '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 - + db.isConnected ( err, result ) => + if err + shutDown() + + else # > Initialize all required modules with the args object. - log.info 'RS | Initialzing engine' + @log.info 'RS | Initialzing engine' engine args - log.info 'RS | Initialzing http listener' + @log.info 'RS | Initialzing http listener' http args # > Distribute handlers between modules to link the application. - log.info 'RS | Passing handlers to engine' + @log.info 'RS | Passing handlers to engine' engine.addPersistence db - log.info 'RS | Passing handlers to http listener' + @log.info 'RS | Passing handlers to http listener' #TODO engine pushEvent needs to go into redis queue http.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' + @log.info 'RS | Forking child process for the event poller' cliArgs = [ args.logconf['mode'] args.logconf['io-level'] @@ -169,10 +171,11 @@ Shuts down the server. @private shutDown() ### -shutDown = -> - log.warn 'RS | Received shut down command!' +shutDown = => + @log.warn 'RS | Received shut down command!' engine?.shutDown() http?.shutDown() + process.exit() ### ## Process Commands @@ -182,6 +185,9 @@ from the parent process (e.g. the testing suite) ### process.on 'message', ( cmd ) -> procCmds[cmd]?() +process.on 'SIGINT', shutDown +process.on 'SIGTERM', shutDown + # The die command redirects to the shutDown function. procCmds.die = shutDown diff --git a/run_doc.sh b/gen_doc.sh similarity index 100% rename from run_doc.sh rename to gen_doc.sh diff --git a/js-coffee/config.js b/js-coffee/config.js index 789b924..de034c7 100644 --- a/js-coffee/config.js +++ b/js-coffee/config.js @@ -52,7 +52,7 @@ Configuration loadConfigFile = function(configPath) { var confProperties, e, isReady, prop, _i, _len; _this.config = null; - confProperties = ['log', 'http-port', 'db-port', 'crypto-key']; + confProperties = ['log', 'http-port', 'db-port']; try { _this.config = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', configPath))); isReady = true; @@ -115,7 +115,7 @@ Configuration */ - exports.getDBPort = function() { + exports.getDbPort = function() { return fetchProp('db-port'); }; diff --git a/js-coffee/engine.js b/js-coffee/engine.js index 9d3a2c9..47201a2 100644 --- a/js-coffee/engine.js +++ b/js-coffee/engine.js @@ -1,17 +1,15 @@ 'use strict'; var path = require('path'), - log = require('./logging'), // qEvents = new (require('./queue')).Queue(), //TODO export queue into redis regex = /\$X\.[\w\.\[\]]*/g, // find properties of $X listRules = {}, listActionModules = {}, isRunning = true, - mm, poller, db; + mm, poller, db, log; exports = module.exports = function( args ) { - args = args || {}; - log(args); + log = args.logger; mm = require('./module-manager')(args); return module.exports; }; @@ -28,12 +26,12 @@ exports.addPersistence = function(db_link) { // if(err) log.error('EN', 'retrieving Action Modules from DB!'); // else { // if(!obj) { - // log.print('EN', 'No Action Modules found in DB!'); + // log.info('EN', 'No Action Modules found in DB!'); // loadRulesFromDB(); // } else { // var m; // for(var el in obj) { - // log.print('EN', 'Loading Action Module from DB: ' + el); + // log.info('EN', 'Loading Action Module from DB: ' + el); // try{ // m = mm.requireFromString(obj[el], el); // db.getActionModuleAuth(el, function(mod) { @@ -75,7 +73,7 @@ exports.loadActionModule = function(name, objModule) { // TODO only load module once, load user specific parameters per user // when rule is activated by user. invoked action then uses user specific // parameters - log.print('EN', 'Action module "' + name + '" loaded'); + log.info('EN', 'Action module "' + name + '" loaded'); listActionModules[name] = objModule; }; @@ -89,17 +87,17 @@ exports.getActionModule = function(name) { */ exports.addRule = function(objRule) { //TODO validate rule - log.print('EN', 'Loading Rule'); - log.print('EN', objRule); - log.print('EN', 'Loading Rule: ' + objRule.id); - if(listRules[objRule.id]) log.print('EN', 'Replacing rule: ' + objRule.id); + log.info('EN', 'Loading Rule'); + log.info('EN', objRule); + log.info('EN', 'Loading Rule: ' + objRule.id); + if(listRules[objRule.id]) log.info('EN', 'Replacing rule: ' + objRule.id); listRules[objRule.id] = objRule; // Notify poller about eventual candidate try { poller.send('event|'+objRule.event); } catch (err) { - log.print('EN', 'Unable to inform poller about new active rule!'); + log.info('EN', 'Unable to inform poller about new active rule!'); } }; @@ -119,7 +117,7 @@ function pollQueue() { * @param {Object} evt The event object */ function processEvent(evt) { - log.print('EN', 'processing event: ' + evt.event + '(' + evt.eventid + ')'); + log.info('EN', 'processing event: ' + evt.event + '(' + evt.eventid + ')'); var actions = checkEvent(evt); for(var i = 0; i < actions.length; i++) { invokeAction(evt, actions[i]); @@ -137,7 +135,7 @@ function checkEvent(evt) { //TODO this needs to get depth safe, not only data but eventually also // on one level above (eventid and other meta) if(listRules[rn].event === evt.event && validConditions(evt.payload, listRules[rn])) { - log.print('EN', 'Rule "' + rn + '" fired'); + log.info('EN', 'Rule "' + rn + '" fired'); actions = actions.concat(listRules[rn].actions); } } @@ -181,7 +179,7 @@ function invokeAction(evt, action) { log.error('EN', 'during action execution: ' + err); } } - else log.print('EN', 'No api interface found for: ' + action.module); + else log.info('EN', 'No api interface found for: ' + action.module); } /** @@ -229,7 +227,7 @@ function preprocessActionArguments(evt, act, res) { } exports.shutDown = function() { - log.print('EN', 'Shutting down Poller and DB Link'); + log.info('EN', 'Shutting down Poller and DB Link'); isRunning = false; if(poller) poller.send('cmd|shutdown'); if(db) db.shutDown(); diff --git a/js-coffee/event-poller.js b/js-coffee/event-poller.js index c059bd7..818479a 100644 --- a/js-coffee/event-poller.js +++ b/js-coffee/event-poller.js @@ -29,7 +29,7 @@ function init() { logconf['file-path'] = process.argv[5] logconf['nolog'] = process.argv[6] - log = logger(logconf); + log = logger.getLogger(logconf); var args = { logger: log }; (ml = require('./module-manager'))(args); (db = require('./persistence'))(args); @@ -39,6 +39,12 @@ function init() { log.info('Event Poller instantiated'); }; +function shutDown() { + log.info('EP', 'Shutting down DB Link'); + isRunning = false; + if(db) db.shutDown(); + process.exit(); +} function loadEventModule(el, cb) { if(db && ml) db.getEventModule(el, function(err, obj) { @@ -47,7 +53,7 @@ function loadEventModule(el, cb) { else log.error('EP', 'Retrieving Event Module ' + el + ' from DB!'); } else { - // log.print('EP', 'Loading Event Module: ' + el); + // log.info('EP', 'Loading Event Module: ' + el); try { var m = ml.requireFromString(obj, el); db.getEventModuleAuth(el, function(mod) { @@ -71,13 +77,13 @@ function fetchPollFunctionFromModule(mod, func) { if(mod) mod = mod[func[i]]; } if(mod) { - log.print('EP', 'Found active event module "' + func.join('->') + '", adding it to polling list'); + log.info('EP', 'Found active event module "' + func.join('->') + '", adding it to polling list'); //FIXME change this to [module][prop] = module; because like this identical properties get overwritten // also add some on a per user basis information because this should go into a user context for the users // that sat up this rule! listPoll[func.join('->')] = mod; } else { - log.print('EP', 'No property "' + func.join('->') + '" found'); + log.info('EP', 'No property "' + func.join('->') + '" found'); } } @@ -88,11 +94,11 @@ function initMessageActions() { if(listEventModules[arrModule[0]]) { fetchPollFunctionFromModule(listEventModules[arrModule[0]], arrModule); } else { - log.print('EP', 'Event Module ' + arrModule[0] + ' needs to be loaded, doing it now...'); + log.info('EP', 'Event Module ' + arrModule[0] + ' needs to be loaded, doing it now...'); loadEventModule(arrModule[0], function(err, obj) { if(err || !obj) log.error('EP', 'Event Module "' + arrModule[0] + '" not found: ' + err); else { - log.print('EP', 'Event Module ' + arrModule[0] + ' found and loaded'); + log.info('EP', 'Event Module ' + arrModule[0] + ' found and loaded'); fetchPollFunctionFromModule(obj, arrModule); } }); @@ -115,14 +121,13 @@ function initMessageActions() { if(func) func(arrProps); } }); + + // very important so the process doesnt linger on when the paren process is killed + process.on('disconnect', shutDown); } function initAdminCommands() { - listAdminCommands['shutdown'] = function(args) { - log.print('EP', 'Shutting down DB Link'); - isRunning = false; - if(db) db.shutDown(); - }; + listAdminCommands['shutdown'] = shutDown } function checkRemotes() { diff --git a/js-coffee/http-listener.js b/js-coffee/http-listener.js index 997201d..590a0fe 100644 --- a/js-coffee/http-listener.js +++ b/js-coffee/http-listener.js @@ -10,9 +10,8 @@ HTTP Listener (function() { - var app, exports, express, initRouting, log, path, qs, requestHandler; - - log = require('./logging'); + var app, exports, express, initRouting, path, qs, requestHandler, + _this = this; requestHandler = require('./request-handler'); @@ -34,8 +33,7 @@ HTTP Listener exports = module.exports = function(args) { - args = args != null ? args : {}; - log(args); + _this.log = args.logger; requestHandler(args); initRouting(args['http-port']); return module.exports; @@ -56,7 +54,7 @@ HTTP Listener app.use(express.session({ secret: sess_sec })); - log.print('HL', 'no session backbone'); + _this.log.info('HL | no session backbone'); app.use('/', express["static"](path.resolve(__dirname, '..', 'webpages', 'public'))); app.get('/admin', requestHandler.handleAdmin); app.get('/forge_modules', requestHandler.handleForgeModules); @@ -67,11 +65,10 @@ HTTP Listener app.post('/logout', requestHandler.handleLogout); app.post('/usercommand', requestHandler.handleUserCommand); try { - return app.listen(port); + return _this.server = app.listen(port); } catch (_error) { e = _error; - e.addInfo = 'opening port'; - return log.error(e); + return _this.log.error(e, 'HL | Unable to listen...'); } }; @@ -95,8 +92,10 @@ HTTP Listener exports.shutDown = function() { - log.print('HL', 'Shutting down HTTP listener'); - return process.exit(); + _this.log.warn('HL | Shutting down HTTP listener'); + console.log('apppp'); + console.log(app); + return _this.server.close(); }; }).call(this); diff --git a/js-coffee/module-manager.js b/js-coffee/module-manager.js index 2ec67f3..45b35e8 100644 --- a/js-coffee/module-manager.js +++ b/js-coffee/module-manager.js @@ -11,12 +11,12 @@ var fs = require('fs'), path = require('path'), - log = require('./logging'), + log, db, funcLoadAction, funcLoadRule; exports = module.exports = function(args) { args = args || {}; - log(args); + log = args.logger; return module.exports; }; @@ -82,7 +82,7 @@ exports.loadModules = function(directory, callback) { log.error('LM', 'loading modules directory: ' + err); return; } - log.print('LM', 'Loading ' + list.length + ' modules from "' + directory + '"'); + log.info('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()) { diff --git a/js-coffee/persistence.js b/js-coffee/persistence.js index edda71d..fa9df19 100644 --- a/js-coffee/persistence.js +++ b/js-coffee/persistence.js @@ -32,35 +32,27 @@ Persistence /* Module call ----------- - Initializes the DB connection. Requires a valid configuration file which contains - a db port and a crypto key. + Initializes the DB connection with the given `db-port` property in the `args` object. @param {Object} args */ exports = module.exports = function(args) { - var config, _ref; + var _ref; _this.log = args.logger; - config = require('./config'); - config(args); if ((_ref = _this.db) != null) { _ref.quit(); } - if (config.isReady()) { - _this.crypto_key = config.getCryptoKey(); - _this.db = redis.createClient(config.getDBPort(), 'localhost', { - connect_timeout: 2000 - }); - _this.db.on('error', function(err) { - err.addInfo = 'message from DB'; - return _this.log.error('DB', err); - }); - _this.ep = new IndexedModules('event-poller', _this.db, _this.log); - return _this.ai = new IndexedModules('action-invoker', _this.db, _this.log); - } else { - return _this.log.error('DB', 'Initialization failed because of missing config file!'); - } + _this.crypto_key = "}f6y1y}B{.an$}2c$Yl.$mSnF\\HX149u*y8C:@kmN/520Gt\\v'+KFBnQ!\\r<>5X/xRI`sT TODO Add documentation +> The request handler (surprisingly) handles requests made through HTTP to +> the [HTTP Listener](http-listener.html). It will handle user requests for +> pages as well as POST requests such as user login, module storing, event +> invocation and also admin commands. */ (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, mm, mustache, objAdminCmds, objUserCmds, path, qs, renderPage, sendLoginOrPage, _this = this; - log = require('./logging'); - db = require('./persistence'); mm = require('./module-manager'); @@ -38,9 +39,8 @@ Request Handler objAdminCmds = {}; exports = module.exports = function(args) { - var user, users, _i, _len; - args = args != null ? args : {}; - log(args); + var log, user, users, _i, _len; + log = args.logger; db(args); mm(args); mm.addDBLink(db); @@ -122,7 +122,7 @@ Request Handler obj = qs.parse(body); return db.loginUser(obj.username, obj.password, function(err, usr) { if (err) { - log.print('RH', ("AUTH-UH-OH (" + obj.username + "): ") + err.message); + log.warn(err, "RH | AUTH-UH-OH (" + obj.username + ")"); } else { req.session.user = usr; } @@ -341,7 +341,7 @@ Request Handler if (req.session && req.session.user) { if (req.session.user.isAdmin === "true") { q = req.query; - log.print('RH', 'Received admin request: ' + req.originalUrl); + log.info('RH | Received admin request: ' + req.originalUrl); if (q.cmd) { return typeof (_base = _this.objAdminCmds)[_name = q.cmd] === "function" ? _base[_name](q, answerHandler(req, resp, true)) : void 0; } else { diff --git a/js-coffee/sandbox.js b/js-coffee/sandbox.js deleted file mode 100644 index fcfdd91..0000000 --- a/js-coffee/sandbox.js +++ /dev/null @@ -1,25 +0,0 @@ -// Generated by CoffeeScript 1.6.3 -(function() { - var bunyan, logger, opt; - - bunyan = require('bunyan'); - - opt = { - name: "webapi-eca" - }; - - opt.streams = [ - { - level: 'info', - stream: process.stdout - }, { - level: 'info', - path: 'logs/server.log' - } - ]; - - logger = bunyan.createLogger(opt); - - logger.info('weeee'); - -}).call(this); diff --git a/js-coffee/webapi-eca.js b/js-coffee/webapi-eca.js index edcf809..b579a92 100644 --- a/js-coffee/webapi-eca.js +++ b/js-coffee/webapi-eca.js @@ -13,7 +13,8 @@ WebAPI-ECA Engine (function() { - var argv, conf, cp, db, engine, fs, http, init, logger, opt, optimist, path, procCmds, shutDown, usage; + var argv, conf, cp, db, engine, fs, http, init, logger, opt, optimist, path, procCmds, shutDown, usage, + _this = this; logger = require('./logging'); @@ -98,8 +99,7 @@ WebAPI-ECA Engine process.on('uncaughtException', function(err) { switch (err.errno) { case 'EADDRINUSE': - err.addInfo = 'http-port already in use, shutting down!'; - log.error('RS', err); + _this.log.error(err, 'RS | http-port already in use, shutting down!'); return shutDown(); default: throw err; @@ -114,7 +114,7 @@ WebAPI-ECA Engine init = function() { - var args, log, logconf; + var args, logconf; conf(argv.c); if (!conf.isReady()) { console.error('FAIL: Config file not ready! Shutting down...'); @@ -139,27 +139,30 @@ WebAPI-ECA Engine try { fs.unlinkSync(path.resolve(__dirname, '..', 'logs', logconf['file-path'])); } catch (_error) {} - log = logger(logconf); - log.info('RS | STARTING SERVER'); + _this.log = logger.getLogger(logconf); + _this.log.info('RS | STARTING SERVER'); args = { - logger: log, + logger: _this.log, logconf: logconf }; args['http-port'] = parseInt(argv.w || conf.getHttpPort()); - log.info('RS | Initialzing DB'); + args['db-port'] = parseInt(argv.w || conf.getDbPort()); + _this.log.info('RS | Initialzing DB'); db(args); return db.isConnected(function(err, result) { var cliArgs, poller; - if (!err) { - log.info('RS | Initialzing engine'); + if (err) { + return shutDown(); + } else { + _this.log.info('RS | Initialzing engine'); engine(args); - log.info('RS | Initialzing http listener'); + _this.log.info('RS | Initialzing http listener'); http(args); - log.info('RS | Passing handlers to engine'); + _this.log.info('RS | Passing handlers to engine'); engine.addPersistence(db); - log.info('RS | Passing handlers to http listener'); + _this.log.info('RS | Passing handlers to http listener'); http.addShutdownHandler(shutDown); - log.info('RS | For e child process for the event poller'); + _this.log.info('RS | Forking 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); } @@ -174,11 +177,14 @@ WebAPI-ECA Engine shutDown = function() { - log.warn('RS | Received shut down command!'); + _this.log.warn('RS | Received shut down command!'); if (engine != null) { engine.shutDown(); } - return http != null ? http.shutDown() : void 0; + if (http != null) { + http.shutDown(); + } + return process.exit(); }; /* @@ -193,6 +199,10 @@ WebAPI-ECA Engine return typeof procCmds[cmd] === "function" ? procCmds[cmd]() : void 0; }); + process.on('SIGINT', shutDown); + + process.on('SIGTERM', shutDown); + procCmds.die = shutDown; init(); diff --git a/logs/.gitignore b/logs/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/logs/testing/.gitignore b/logs/testing/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/testing/test_config.coffee b/testing/test_config.coffee index e1da583..144dd4e 100644 --- a/testing/test_config.coffee +++ b/testing/test_config.coffee @@ -21,10 +21,9 @@ exports.testParameters = ( test ) => 'file-level' 'file-path' ] - test.expect 4 + reqProp.length + test.expect 3 + 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.getDbPort(), 'DB port does not exist!' logconf = @conf.getLogConf() test.ok logconf, 'Log config does not exist!' for prop in reqProp diff --git a/testing/test_persistence.coffee b/testing/test_persistence.coffee index 9cb8ed3..b30a25d 100644 --- a/testing/test_persistence.coffee +++ b/testing/test_persistence.coffee @@ -5,8 +5,10 @@ exports.setUp = ( cb ) => @log = logger.getLogger nolog: true @db = require @path.join '..', 'js-coffee', 'persistence' - @db + opts = logger: @log + opts[ 'db-port' ] = 6379 + @db opts cb() exports.tearDown = ( cb ) => @@ -31,22 +33,14 @@ exports.Availability = test.ifError err, 'Connection failed!' test.done() - testNoConfig: ( test ) => + # We cannot test for no db-port, since node-redis then assumes standard port + testWrongDbPort: ( test ) => test.expect 1 - @db + opts = logger: @log - configPath: 'nonexistingconf.file' - @db.isConnected ( err ) -> - test.ok err, 'Still connected!?' - test.done() - - testWrongConfig: ( test ) => - test.expect 1 - - @db - logger: @log - configPath: @path.join 'testing', 'jsonWrongConfig.json' + opts[ 'db-port' ] = 63214 + @db opts @db.isConnected ( err ) -> test.ok err, 'Still connected!?' test.done() diff --git a/testing/test_webapi-eca.coffee b/testing/test_webapi-eca.coffee new file mode 100644 index 0000000..4ac3419 --- /dev/null +++ b/testing/test_webapi-eca.coffee @@ -0,0 +1,104 @@ + +cp = require 'child_process' +path = require 'path' + +# exports.setUp = ( cb ) => +# cb() + +# exports.tearDown = ( cb ) => +# @engine.send('die') +# cb() + +# # TODO wrong db-port or http-port will make the engine stop properly before starting +# # goes hand in hand with wrong config file +# # http command shutdown does it properly, as well as sending the process the die command + +exports.testShutDown = ( test ) => + test.expect 1 + + isRunning = true + pth = path.resolve 'js-coffee', 'webapi-eca' + engine = cp.fork pth, [ '-n' ] # [ '-i' , 'warn' ] + + engine.on 'exit', ( code, signal ) -> + test.ok true, 'Engine stopped' + isRunning = false + test.done() + + fWaitForStartup = () -> + engine.send 'die' + setTimeout fWaitForDeath, 5000 + + # Garbage collect eventually still running process + fWaitForDeath = () -> + if isRunning + test.ok false, 'Engine didn\'t shut down!' + engine.kill() + test.done() + + setTimeout fWaitForStartup, 1000 + +exports.testKill = ( test ) => + test.expect 1 + + pth = path.resolve 'js-coffee', 'webapi-eca' + engine = cp.fork pth, [ '-n' ] # [ '-i' , 'warn' ] + + fWaitForStartup = () -> + engine.kill() + setTimeout fWaitForDeath, 1000 + + # Garbage collect eventually still running process + fWaitForDeath = () -> + test.ok engine.killed, 'Engine didn\'t shut down!' + test.done() + + setTimeout fWaitForStartup, 1000 + +exports.testHttpPortAlreadyUsed = ( test ) => + test.expect 1 + isRunning = true + pth = path.resolve 'js-coffee', 'webapi-eca' + @engine_one = cp.fork pth, ['-n'] # [ '-i' , 'warn' ] + + fWaitForFirstStartup = () => + @engine_two = cp.fork pth, ['-n'] # [ '-i' , 'warn' ] + + @engine_two.on 'exit', ( code, signal ) -> + test.ok true, 'Engine stopped' + isRunning = false + test.done() + + setTimeout fWaitForDeath, 3000 + + # Garbage collect eventually still running process + fWaitForDeath = () => + if isRunning + test.ok false, 'Engine didn\'t shut down!' + test.done() + + @engine_one.kill() + @engine_two.kill() + + setTimeout fWaitForFirstStartup, 1000 + +exports.testHttpPortInvalid = ( test ) => + test.expect 1 + + isRunning = true + pth = path.resolve 'js-coffee', 'webapi-eca' + engine = cp.fork pth, ['-n', '-w', '-1'] # [ '-i' , 'warn' ] + engine.on 'exit', ( code, signal ) -> + test.ok true, 'Engine stopped' + isRunning = false + test.done() + + # Garbage collect eventually still running process + fWaitForDeath = () => + if isRunning + test.ok false, 'Engine didn\'t shut down!' + test.done() + + engine.kill() + + setTimeout fWaitForDeath, 1000 diff --git a/run_tests.sh b/unit_tests.sh similarity index 100% rename from run_tests.sh rename to unit_tests.sh