define(function (require) { var woodman = require('woodman'); var logger = woodman.getLogger('AbstractSyncClock'); var EventTarget = require('event-target'); /** * Default constructor for a synchronized clock * * @class * @param {Number} initialSkew The initial clock skew * @param {Number} initialDelta The initial static delta */ var SyncClock = function (initialSkew, initialDelta) { var self = this; /** * The current estimation of the skew with the reference clock, in ms */ var skew = initialSkew || 0.0; /** * Some agreed fixed delta delay, in ms. */ var delta = initialDelta || 0.0; /** * The ready state of the synchronized clock */ var readyState = 'connecting'; /** * Define the "readyState", "skew" and "delta" properties. Note that * setting these properties may trigger "readystatechange" and "change" * events. */ Object.defineProperties(this, { readyState: { get: function () { return readyState; }, set: function (state) { if (state !== readyState) { readyState = state; logger.log('ready state updated, dispatch "readystatechange" event'); // Dispatch the event on next loop to give code that wants to // listen to the initial change to "open" time to attach an event // listener (locally synchronized clocks typically set the // readyState property to "open" directly within the constructor) setTimeout(function () { self.dispatchEvent({ type: 'readystatechange', value: state }); }, 0); } } }, delta: { get: function () { return delta; }, set: function (value) { var previousDelta = delta; delta = value; if (previousDelta === delta) { logger.log('delta updated, same as before'); } else { logger.log('delta updated, dispatch "change" event'); self.dispatchEvent({ type: 'change' }); } } }, skew: { get: function () { return skew; }, set: function (value) { var previousSkew = skew; skew = value; if (readyState !== 'open') { logger.log('skew updated, clock not open'); } else if (previousSkew === skew) { logger.log('skew updated, same as before'); } else { logger.log('skew updated, dispatch "change" event'); self.dispatchEvent({ type: 'change' }); } } } }); }; // Synchronized clocks implement EventTarget SyncClock.prototype.addEventListener = EventTarget.addEventListener; SyncClock.prototype.removeEventListener = EventTarget.removeEventListener; SyncClock.prototype.dispatchEvent = EventTarget.dispatchEvent; /** * Returns the time at the reference clock that corresponds to the local * time provided (both in milliseconds since 1 January 1970 00:00:00 UTC) * * @function * @param {Number} localTime The local time in milliseconds * @returns {Number} The corresponding time on the reference clock */ SyncClock.prototype.getTime = function (localTime) { return localTime + this.skew - this.delta; }; /** * Returns the number of milliseconds elapsed since * 1 January 1970 00:00:00 UTC on the reference clock * * @function * @returns {Number} The current timestamp */ SyncClock.prototype.now = function () { return this.getTime(Date.now()); }; /** * Stops synchronization with the reference clock. * * In derived classes, this should typically be used to stop background * synchronization mechanisms. * * @function */ SyncClock.prototype.close = function () { if ((this.readyState === 'closing') || (this.readyState === 'closed')) { return; } this.readyState = 'closing'; this.readyState = 'closed'; }; // Expose the class to the outer world return SyncClock; });
{ level: (argv.verbose ? 'log' : 'error'), appenders: [ { type: 'Console', name: 'console', layout: { type: 'pattern', pattern: '%d{HH:mm:ss} [%level] %message%n' } } ] } ] }); var logger = woodman.getLogger('runner'); /** * Display usage information if so requested */ if (argv.help) { optimist.showHelp(); process.exit(0); } /** * Expand path of custom before/after JS files if they were provided */ if (argv.before) {
var domainMiddleware = require('express-domain-middleware'); app.use(domainMiddleware); app.use(bodyParser.json({ type: 'application/vnd.api+json' })); if (node_env === 'sandbox') { var serveIndex = require('serve-index'); var localPath = __dirname + '/provision/logs'; app.use('/logs', serveIndex(localPath)); app.use('/logs', express.static(localPath)); } woodman.load('console %domain - %message'); var logger = woodman.getLogger('app'); morgan.token('domain', function (req, res) { return process.domain.id; }); app.use(morgan({ immediate: true, format: ':domain - :method :url' })); app.get('/', function(req, res){ res.send('hello world !'); }); app.use(function (err, req, res, next) { var message = err.stack; logger.error(message); if (node_env === "sandbox") {
define(function (require) { var woodman = require('woodman'); var logger = woodman.getLogger('SocketSyncClock'); var AbstractSyncClock = require('./AbstractSyncClock'); var isNumber = require('./utils').isNumber; var stringify = require('./utils').stringify; // Web Sockets ready state constants var CONNECTING = 0; var OPEN = 1; var CLOSING = 2; var CLOSED = 3; // Number of exchanges to make with the server to compute the first skew var initialAttempts = 10; // Interval between two exchanges during initialization (in ms) var initialInterval = 10; // Maximum number of attempts before giving up var maxAttempts = 10; // Interval between two attempts when the clock is open (in ms) var attemptInterval = 500; // Interval between two synchronization batches (in ms) var batchInterval = 10000; // Minimum roundtrip threshold (in ms) var minRoundtripThreshold = 5; /** * Creates a Socket synchronization clock * * @class * @param {String} url The URL of the remote timing object for which we * want to synchronize the clock (only used to check permissions) * @param {WebSocket} socket A Web socket to use as communication channel. */ var SocketSyncClock = function (url, socket) { // Initialize the base class with default data AbstractSyncClock.call(this); var self = this; /** * The Web Socket that will be used to exchange sync information with * the online server */ this.socket = socket; /** * Minimum round trip detected so far (in ms) */ var roundtripMin = 1000; /** * Current round trip threshold above which the "sync" * request is considered to be a failure (in ms) * * NB: this threshold must always be higher than the minimum round trip */ var roundtripThreshold = 1000; /** * Number of "sync" attempts in the current batch so far. * The clock will attempt up to maxAttempts attempts in a row * each time it wants to synchronize */ var attempts = 0; /** * Valid responses received from the server for the current batch */ var initialSyncMessages = []; /** * ID of the attempt response we are currently waiting for */ var attemptId = null; /** * The attempt timeout */ var attemptTimeout = null; /** * Timeout to detect when the server fails to respond in time */ var timeoutTimeout = null; if (socket.readyState === OPEN) { logger.info('WebSocket already opened'); sendSyncRequest(); } else if (socket.readyState === CLOSED) { logger.log('WebSocket closed'); this.readyState = 'closed'; } var errorHandler = function (err) { logger.warn('WebSocket error', err); // TODO: properly deal with network errors return true; }; var openHandler = function () { logger.info('WebSocket client connected'); sendSyncRequest(); return true; }; var closeHandler = function () { logger.log('WebSocket closed'); self.close(); return true; }; var messageHandler = function (evt) { var msg = null; var received = Date.now(); var skew = 0; if (typeof evt.data !== 'string') { logger.log('message from server is not a string, pass on'); return true; } try { msg = JSON.parse(evt.data) || {}; } catch (err) { logger.warn('message from server is not JSON, pass on'); return true; } if (msg.type !== 'sync') { logger.log('message from server is not a sync message, pass on'); return true; } if (!msg.client || !msg.server || !isNumber(msg.client.sent) || !isNumber(msg.server.received) || !isNumber(msg.server.sent)) { logger.log('sync message is incomplete, ignore'); return true; } if (msg.id !== attemptId) { logger.log('sync message is not the expected one, ignore'); return true; } // Message is for us attempts += 1; // Compute round trip duration var roundtripDuration = received - msg.client.sent; // Check round trip duration if ((self.readyState !== 'connecting') && (roundtripDuration > roundtripThreshold)) { logger.log('sync message took too long, ignore'); return false; } if (timeoutTimeout) { // Cancel the timeout set to detect server timeouts. clearTimeout(timeoutTimeout); timeoutTimeout = null; } else { // A timeout already occurred // (should have normally be trapped by the check on round trip // duration, but timeout scheduling and the event loop are not // an exact science) logger.log('sync message took too long, ignore'); return false; } // During initialization, simply store the response, // we'll process things afterwards if (self.readyState === 'connecting') { logger.log('sync message during initialization, store'); initialSyncMessages.push({ received: received, roundtrip: roundtripDuration, msg: msg }); if (attempts >= initialAttempts) { initialize(); scheduleNextBatch(); } else { scheduleNextAttempt(); } return false; } // Adjust the minimum round trip and threshold if needed if (roundtripDuration < roundtripMin) { roundtripThreshold = Math.ceil( roundtripThreshold * (roundtripDuration / roundtripMin)); if (roundtripThreshold < minRoundtripThreshold) { roundtripThreshold = minRoundtripThreshold; } roundtripMin = roundtripDuration; } // Sync message can be directly applied skew = ((msg.server.sent + msg.server.received) - (msg.client.sent + received)) / 2.0; if (Math.abs(skew - self.skew) < 1) { skew = self.skew; } else { skew = Math.round(skew); } logger.info('sync message received, skew={}', skew); // Save the new skew // (this triggers a "change" event if value changed) self.skew = skew; // No need to schedule another attempt, // let's simply schedule the next sync batch of attempts scheduleNextBatch(); return false; }; // NB: calling "addEventListener" does not work in a Node.js environment // because the WebSockets library used only supports basic "onXXX" // constructs. The code below works around that limitation but note that // only works provided the clock is associated with the socket *after* the // timing provider object! var previousErrorHandler = this.socket.onerror; var previousOpenHandler = this.socket.onopen; var previousCloseHandler = this.socket.onclose; var previousMessageHandler = this.socket.onmessage; if (this.socket.addEventListener) { this.socket.addEventListener('error', errorHandler); this.socket.addEventListener('open', openHandler); this.socket.addEventListener('close', closeHandler); this.socket.addEventListener('message', messageHandler); } else { this.socket.onerror = function (evt) { var propagate = errorHandler(evt); if (propagate && previousErrorHandler) { previousErrorHandler(evt); } }; this.socket.onopen = function (evt) { var propagate = openHandler(evt); if (propagate && previousOpenHandler) { previousOpenHandler(evt); } }; this.socket.onclose = function (evt) { var propagate = closeHandler(evt); if (propagate && previousCloseHandler) { previousCloseHandler(evt); } }; this.socket.onmessage = function (evt) { var propagate = messageHandler(evt); if (propagate && previousMessageHandler) { previousMessageHandler(evt); } }; } /** * Helper function to send a "sync" request to the socket server */ var sendSyncRequest = function () { logger.log('send a "sync" request'); attemptId = url + '#' + Date.now(); self.socket.send(stringify({ type: 'sync', id: attemptId, client: { sent: Date.now() } })); attemptTimeout = null; timeoutTimeout = setTimeout(function () { attempts += 1; timeoutTimeout = null; logger.log('sync request timed out'); if (attempts >= maxAttempts) { if (self.readyState === 'connecting') { initialize(); } else { roundtripThreshold = Math.ceil(roundtripThreshold * 1.20); logger.log('all sync attempts failed, increase threshold to {}', roundtripThreshold); } scheduleNextBatch(); } else { scheduleNextAttempt(); } }, roundtripThreshold); }; /** * Helper function to schedule the next sync attempt * * @function */ var scheduleNextAttempt = function () { var interval = (self.readyState === 'connecting') ? initialInterval : attemptInterval; if (timeoutTimeout) { clearTimeout(timeoutTimeout); timeoutTimeout = null; } if (attemptTimeout) { clearTimeout(attemptTimeout); attemptTimeout = null; } attemptTimeout = setTimeout(sendSyncRequest, interval); }; /** * Helper function to schedule the next batch of sync attempts * * @function */ var scheduleNextBatch = function () { if (timeoutTimeout) { clearTimeout(timeoutTimeout); timeoutTimeout = null; } if (attemptTimeout) { clearTimeout(attemptTimeout); attemptTimeout = null; } attempts = 0; attemptTimeout = setTimeout(sendSyncRequest, batchInterval); }; /** * Helper function that computes the initial skew based on the * sync messages received so far and adjust the roundtrip threshold * accordingly. * * The function also sets the clock's ready state to "open". * * @function */ var initialize = function () { var msg = null; var skew = null; var received = 0; var pos = 0; logger.log('compute initial settings'); // Sort messages received according to round trip initialSyncMessages.sort(function (a, b) { return a.roundtrip - b.roundtrip; }); // Use the first message to compute the initial skew if (initialSyncMessages.length > 0) { msg = initialSyncMessages[0].msg; received = initialSyncMessages[0].received; roundtripMin = initialSyncMessages[0].roundtrip; if (isNumber(msg.delta)) { self.delta = msg.delta; } skew = ((msg.server.sent + msg.server.received) - (msg.client.sent + received)) / 2.0; if (Math.abs(skew - self.skew) < 1) { skew = self.skew; } else { skew = Math.round(skew); } self.skew = skew; } // Adjust the threshold to preserve at least half of the sync messages // that should have been received. pos = Math.ceil(initialAttempts / 2) - 1; if (pos >= initialSyncMessages.length) { pos = initialSyncMessages.length - 1; } if (pos >= 0) { roundtripThreshold = initialSyncMessages[pos].roundtrip; } // Ensure the threshold is not too low compared to the // known minimum roundtrip duration if (roundtripThreshold < roundtripMin * 1.30) { roundtripThreshold = Math.ceil(roundtripMin * 1.30); } if (roundtripThreshold < minRoundtripThreshold) { roundtripThreshold = minRoundtripThreshold; } // Clock is ready logger.info('clock is ready: ' + 'skew={}, delta={}, roundtrip min={}, threshold={}', self.skew, self.delta, roundtripMin, roundtripThreshold); self.readyState = 'open'; initialSyncMessages = []; }; /** * Method that stops the background synchronization */ this.stopSync = function () { if (attemptTimeout) { clearTimeout(attemptTimeout); attemptTimeout = null; } if (timeoutTimeout) { clearTimeout(timeoutTimeout); timeoutTimeout = null; } }; logger.info('created'); }; SocketSyncClock.prototype = new AbstractSyncClock(); /** * Stops synchronizing the clock with the reference clock * * Note that a closed synchronized clock object cannot be re-used. * * @function */ SocketSyncClock.prototype.close = function () { if ((this.readyState === 'closing') || (this.readyState === 'closed')) { return; } this.readyState = 'closing'; this.stopSync(); this.socket = null; this.readyState = 'closed'; }; // Expose the class to the outer world return SocketSyncClock; });
define(function (require) { var woodman = require('woodman'); var logger = woodman.getLogger('SocketTimingProvider'); var AbstractTimingProvider = require('./AbstractTimingProvider'); var StateVector = require('./StateVector'); var SocketSyncClock = require('./SocketSyncClock'); var isNull = require('./utils').isNull; var stringify = require('./utils').stringify; var W3CWebSocket = null; try { W3CWebSocket = require('websocket').w3cwebsocket; } catch (err) { W3CWebSocket = window.WebSocket; } // Web Sockets ready state constants var CONNECTING = 0; var OPEN = 1; var CLOSING = 2; var CLOSED = 3; /** * Creates a timing provider * * @class * @param {String} url The Web socket URL of the remote timing object * @param {WebSocket} socket An opened Web socket to use as communication * channel. The parameter is optional, the object will create the * communication channel if not given. * @param {AbstractSyncClock} clock A clock to use for synchronization with * the online server clock. If not given, a clock that uses the underlying * WebSocket will be created and used. */ var SocketTimingProvider = function (url, socket, clock) { var self = this; /** * The URL of the online object, it is used as * identifier in exchanges with the backend server */ this.url = url; /** * The current vector as returned by the server. * * Updating the property through the setter automatically updates * the exposed vector as well, converting the server timestamp into * a local timestamp based on the underlying synchronized clock's readings */ var serverVector = null; Object.defineProperty(this, 'serverVector', { get: function () { return serverVector; }, set: function (vector) { var now = Date.now(); serverVector = vector; self.vector = new StateVector({ position: vector.position, velocity: vector.velocity, acceleration: vector.acceleration, timestamp: vector.timestamp + (now - self.clock.getTime(now)) / 1000.0 }); } }); /** * List of "change" events already received from the server but * whose estimated timestamps lie in the future */ var pendingChanges = []; /** * The ID of the timeout used to trigger the first of the remaining * pending change events to process */ var pendingTimeoutId = null; /** * Helper function that schedules the propagation of the next pending * change. Note that the function calls itself as long as there are * pending changes to schedule. * * The function should be called whenever the synchronized clock reports * changes on its skew evaluation, since that affects the time at which * pending changes need to be executed. */ var scheduleNextPendingChange = function () { stopSchedulingPendingChanges(); if (pendingChanges.length === 0) { return; } var now = Date.now(); var vector = pendingChanges[0]; var localTimestamp = (vector.timestamp * 1000.0) + now - self.clock.getTime(now); logger.log('schedule next pending change', 'delay=' + (localTimestamp - now)); var applyNextPendingChange = function () { // Since we cannot control when this function runs precisely, // note we may have to skip over the first few changes. We'll // only trigger the change that is closest to now logger.log('apply next pending change'); var now = Date.now(); var vector = pendingChanges.shift(); var nextVector = null; var localTimestamp = 0.0; while (pendingChanges.length > 0) { nextVector = pendingChanges[0]; localTimestamp = nextVector.timestamp * 1000.0 + now - self.clock.getTime(now); if (localTimestamp > now) { break; } vector = pendingChanges.shift(); } self.serverVector = vector; scheduleNextPendingChange(); }; if (localTimestamp > now) { pendingTimeoutId = setTimeout( applyNextPendingChange, localTimestamp - now); } else { applyNextPendingChange(); } }; /** * Helper function that stops the pending changes scheduler * * @function */ var stopSchedulingPendingChanges = function () { logger.log('stop scheduling pending changes'); if (pendingTimeoutId) { clearTimeout(pendingTimeoutId); pendingTimeoutId = null; } }; /** * Helper function that processes the "info" message from the * socket server when the clock is ready. * * @function */ var processInfoWhenPossible = function (msg) { // This should really just happen during initialization if (self.readyState !== 'connecting') { logger.warn( 'timing info to process but state is "{}"', self.readyState); return; } // If clock is not yet ready, schedule processing for when it is // (note that this function should only really be called once but // not a big deal if we receive more than one info message from the // server) if (self.clock.readyState !== 'open') { self.clock.addEventListener('readystatechange', function () { if (self.clock.readyState === 'open') { processInfoWhenPossible(msg); } }); return; } if (self.clock.delta) { // The info will be applied right away, but if the server imposes // some delta to all clients (to improve synchronization), it // should be applied to the timestamp received. msg.vector.timestamp -= (self.clock.delta / 1000.0); } self.serverVector = new StateVector(msg.vector); // TODO: set the range as well when feature is implemented // The timing provider object should now be fully operational self.readyState = 'open'; }; // Initialize the base class with default data AbstractTimingProvider.call(this); // Connect to the Web socket if (socket) { this.socket = socket; this.socketProvided = true; } else { this.socket = new W3CWebSocket(url, 'echo-protocol'); this.socketProvided = false; } this.socket.onerror = function (err) { logger.warn('WebSocket error', err); // TODO: implement a connection recovery mechanism }; this.socket.onopen = function () { logger.info('WebSocket client connected'); self.socket.send(stringify({ type: 'info', id: url })); }; this.socket.onclose = function() { logger.info('WebSocket closed'); self.close(); }; this.socket.onmessage = function (evt) { var msg = null; var vector = null; var now = Date.now(); var localTimestamp = 0; if (typeof evt.data === 'string') { try { msg = JSON.parse(evt.data) || {}; } catch (err) { logger.warn('message received from server could not be parsed as JSON'); return; } if (msg.id !== url) { logger.log('message is for another timing object, ignored'); return; } switch (msg.type) { case 'info': // Info received from the socket server but note that the clock may // not yet be synchronized with that of the server, let's wait for // that. logger.log('timing object info received', msg.vector); processInfoWhenPossible(msg); break; case 'change': if (self.readyState !== 'open') { logger.log('change message received, but not yet open, ignored'); return; } // TODO: not sure what to do when the server sends an update with // a timestamp that lies in the past of the current vector we have, // ignoring for now if (msg.vector.timestamp < self.serverVector.timestamp) { logger.warn('change message received, but more ancient than current vector, ignored'); return; } // Create a new Media state vector from the one received vector = new StateVector(msg.vector); // Determine whether the change event is to be applied now or to be // queued up for later localTimestamp = vector.timestamp * 1000.0 + now - self.clock.getTime(now); if (localTimestamp < now) { logger.log('change message received, execute now'); self.serverVector = vector; } else { logger.log('change message received, queue for later'); pendingChanges.push(vector); pendingChanges.sort(function (a, b) { return a.timestamp - b.timestamp; }); scheduleNextPendingChange(); } break; } } }; // Create the clock if (clock) { this.clock = clock; } else { this.clock = new SocketSyncClock(url, this.socket); this.clock.addEventListener('change', function () { if (self.readyState !== 'open') { return; } logger.log('apply new skew to pending changes'); scheduleNextPendingChange(); }); } // Check the initial state of the socket connection if (this.socket.readyState === OPEN) { logger.info('WebSocket client connected'); this.socket.send(stringify({ type: 'info', id: url })); } else if (this.socket.readyState === CLOSED) { logger.log('WebSocket closed'); self.close(); } logger.info('created'); }; SocketTimingProvider.prototype = new AbstractTimingProvider(); /** * Sends an update command to the online timing service. * * @function * @param {Object} vector The new motion vector * @param {Number} vector.position The new motion position. * If null, the position at the current time is used. * @param {Number} vector.velocity The new velocity. * If null, the velocity at the current time is used. * @param {Number} vector.acceleration The new acceleration. * If null, the acceleration at the current time is used. * @returns {Promise} The promise to get an updated StateVector that * represents the updated motion on the server once the update command * has been processed by the server. * The promise is rejected if the connection with the online timing service * is not possible for some reason (no connection, timing object on the * server was deleted, timeout, permission issue). */ SocketTimingProvider.prototype.update = function (vector) { vector = vector || {}; logger.log('update', '(position=' + vector.position + ', velocity=' + vector.velocity + ', acceleration=' + vector.acceleration + ')'); if (this.readyState !== 'open') { return new Promise(function (resolve, reject) { logger.warn('update', 'socket was closed, cannot process update'); reject(new Error('Underlying socket was closed')); }); } this.socket.send(stringify({ type: 'update', id: this.url, vector: vector })); return new Promise(function (resolve, reject) { // TODO: To be able to resolve the promise, we would need to know // when the server has received and processed the request. This // requires an ack that does not yet exist. Also, should the promise // only be resolved when the update is actually done (which may take // place after some time and may actually not take place at all?) resolve(); }); }; /** * Closes the timing provider object, releasing any resource that the * object might use. * * Note that a closed timing provider object cannot be re-used. * * @function */ SocketTimingProvider.prototype.close = function () { if ((this.readyState === 'closing') || (this.readyState === 'closed')) { return; } this.readyState = 'closing'; this.clock.close(); if (!this.socketProvided && (this.socket.readyState !== CLOSED)) { this.socket.close(); } this.socket = null; this.readyState = 'closed'; }; // Expose the class to the outer world return SocketTimingProvider; });
define(function (require) { var woodman = require('woodman'); var logger = woodman.getLogger('TimingMediaController'); var EventTarget = require('event-target'); var TimingObject = require('./TimingObject'); var StateVector = require('./StateVector'); /** * Constructor of a timing media controller * * @class * @param {TimingObject} timing The timing object attached to the controller * @param {Object} options controller settings */ var TimingMediaController = function (timing, options) { var self = this; options = options || {}; if (!timing || (!timing instanceof TimingObject)) { throw new Error('No timing object provided'); } /** * The timing media controller's internal settings */ var settings = { // Media elements are considered in sync with the timing object if the // difference between the position they report and the position of the // timing object is below that threshold (in seconds). minDiff: options.minDiff || 0.010, // Maximum delay for catching up (in seconds). // If the code cannot meet the maxDelay constraint, // it will have the media element directly seek to the right position. maxDelay: options.maxDelay || 1.0, // Amortization period (in seconds). // The amortization period is used when adjustments are made to // the playback rate of the video. amortPeriod: options.amortPeriod || 2.0 }; /** * The list of Media elements controlled by this timing media controller. * * For each media element, the controller maintains a state vector * representation of the element's position and velocity, a drift rate * to adjust the playback rate, whether we asked the media element to * seek or not, and whether there is an amortization period running for * the element * * { * vector: {}, * driftRate: 0.0, * seeked: false, * amortization: false, * element: {} * } */ var controlledElements = []; /** * The timing object's state vector last time we checked it. * This variable is used in particular at the end of the amortization * period to compute the media element's drift rate */ var timingVector = null; /** * Pointer to the amortization period timeout. * The controller uses only one amortization period for all media elements * under control. */ var amortTimeout = null; Object.defineProperties(this, { /** * Report the state of the underlying timing object * * TODO: should that also take into account the state of the controlled * elements? Hard to find a proper definition though */ readyState: { get: function () { return timingProvider.readyState; } }, /** * The currentTime attribute returns the position that all controlled * media elements should be at, in other words the position of the * timing media controller when this method is called. * * On setting, the timing object's state vector is updated with the * provided value, which will (asynchronously) affect all controlled * media elements. * * Note that getting "currentTime" right after setting it may not return * the value that was just set. */ currentTime: { get: function () { return timing.currentPosition; }, set: function (value) { timing.update(value, null); } }, /** * The current playback rate of the controller (controlled media elements * may have a slightly different playback rate since the role of the * controller is precisely to adjust their playback rate to ensure they * keep up with the controller's position. * * On setting, the timing object's state vector is updated with the * provided value, which will (asynchronously) affect all controlled * media elements. * * Note that getting "playbackRate" right after setting it may not return * the value that was just set. */ playbackRate: { get: function () { return timing.currentVelocity; }, set: function (value) { timing.update(null, value); } } }); /** * Start playing the controlled elements * * @function */ this.play = function () { timing.update(null, 1.0); }; /** * Pause playback * * @function */ this.pause = function () { timing.update(null, 0.0); }; /** * Add a media element to the list of elements controlled by this * controller * * @function * @param {MediaElement} element The media element to associate with the * controller. */ this.addMediaElement = function (element) { var found = false; controlledElements.forEach(function (wrappedEl) { if (wrappedEl.element === element) { found = true; } }); if (found) { return; } controlledElements.push({ element: element, vector: null, driftRate: 0.0, seeked: false, amortization: false }); }; /** * Helper function that cancels a running amortization period */ var cancelAmortizationPeriod = function () { if (!amortTimeout) { return; } clearTimeout(amortTimeout); amortTimeout = null; controlledElements.forEach(function (wrappedEl) { wrappedEl.amortization = false; wrappedEl.seeked = false; }); }; /** * Helper function to stop the playback adjustment once the amortization * period is over. */ var stopAmortizationPeriod = function () { var now = Date.now() / 1000.0; amortTimeout = null; controlledElements.forEach(function (wrappedEl) { // Nothing to do if element was not part of amortization period if (!wrappedEl.amortization) { return; } wrappedEl.amortization = false; // Don't adjust playback rate and drift rate if video was seeked // or if element was not part of that amortization period. if (wrappedEl.seeked) { logger.log('end of amortization period for seek'); wrappedEl.seeked = false; return; } // Compute the difference between the position the video should be and // the position it is reported to be at. var diff = wrappedEl.vector.computePosition(now) - wrappedEl.element.currentTime; // Compute the new video drift rate wrappedEl.driftRate = diff / (now - wrappedEl.vector.timestamp); // Switch back to the current vector's velocity, // adjusted with the newly computed drift rate wrappedEl.vector.velocity = timingVector.velocity + wrappedEl.driftRate; wrappedEl.element.playbackRate = wrappedEl.vector.velocity; logger.log('end of amortization period', 'new drift=' + wrappedEl.driftRate, 'playback rate=' + wrappedEl.vector.velocity); }); }; /** * React to timing object's changes, harnessing the controlled * elements to align them with the timing object's position and velocity */ var onTimingChange = function () { cancelAmortizationPeriod(); controlElements(); }; /** * Ensure media elements are aligned with the current timing object's * state vector */ var controlElements = function () { // Do not adjust anything during an amortization period if (amortTimeout) { return; } // Get new readings from Timing object timingVector = timing.query(); controlledElements.forEach(controlElement); var amortNeeded = false; controlledElements.forEach(function (wrappedEl) { if (wrappedEl.amortization) { amortNeeded = true; } }); if (amortNeeded) { logger.info('start amortization period'); amortTimeout = setTimeout(stopAmortizationPeriod, settings.amortPeriod * 1000); } // Queue a task to fire a simple event named "timeupdate" setTimeout(function () { self.dispatchEvent({ type: 'timeupdate' }, 0); }); }; /** * Ensure the given media element (wrapped in info structure) is aligned * with the current timing object's state vector */ var controlElement = function (wrappedEl) { var element = wrappedEl.element; var diff = 0.0; var futurePos = 0.0; if ((timingVector.velocity === 0.0) && (timingVector.acceleration === 0.0)) { logger.info('stop element and seek to right position'); element.pause(); element.currentTime = timingVector.position; wrappedEl.vector = new StateVector(timingVector); } else if (element.paused) { logger.info('play video'); wrappedEl.vector = new StateVector({ position: timingVector.position, velocity: timingVector.velocity + wrappedEl.driftRate, acceleration: 0.0, timestamp: timingVector.timestamp }); wrappedEl.seeked = true; wrappedEl.amortization = true; element.currentTime = wrappedEl.vector.position; element.playbackRate = wrappedEl.vector.velocity; element.play(); } else { wrappedEl.vector = new StateVector({ position: element.currentTime, velocity: wrappedEl.vector.velocity, }); diff = timingVector.position - wrappedEl.vector.position; if (Math.abs(diff) < settings.minDiff) { logger.info('video and vector are in sync!'); } else if (Math.abs(diff) > settings.maxDelay) { logger.info('seek video to pos={}', timingVector.position); wrappedEl.vector.position = timingVector.position; wrappedEl.vector.velocity = timingVector.velocity + wrappedEl.driftRate; wrappedEl.seeked = true; wrappedEl.amortization = true; element.currentTime = wrappedEl.vector.position; element.playbackRate = wrappedEl.vector.velocity; } else { futurePos = timingVector.computePosition( timingVector.timestamp + settings.amortPeriod); wrappedEl.vector.velocity = wrappedEl.driftRate + (futurePos - wrappedEl.vector.position) / settings.amortPeriod; wrappedEl.amortization = true; element.playbackRate = wrappedEl.vector.velocity; logger.info('new playbackrate={}', wrappedEl.vector.velocity); } } }; /********************************************************************** Listen to the timing object **********************************************************************/ logger.info('add listener to "timeupdate" events...'); timing.addEventListener('timeupdate', controlElements); timing.addEventListener('change', onTimingChange); logger.info('add listener to "timeupdate" events... done'); logger.info('add listener to "readystatechange" events...'); timing.addEventListener('readystatechange', function (evt) { self.dispatchEvent(evt); }); logger.info('add listener to "readystatechange" events... done'); logger.info('created'); }; // TimingMediaController implements EventTarget TimingMediaController.prototype.addEventListener = EventTarget.addEventListener; TimingMediaController.prototype.removeEventListener = EventTarget.removeEventListener; TimingMediaController.prototype.dispatchEvent = EventTarget.dispatchEvent; // Expose the class to the outer world return TimingMediaController; });
/** * @fileOverview Basic Web socket server that can manage a set of timing objects * * To run the server from the root repository folder: * node src/server.js */ var woodman = require('woodman'); var woodmanConfig = require('./woodmanConfig'); var logger = woodman.getLogger('main'); var fs = require('fs'); var path = require('path'); var _ = require('underscore'); var TimingObject = require('../src/TimingObject'); var stringify = require('../src/utils').stringify; var cfg = { ssl: true, port: 8080, ssl_key: '/home/VICOMTECH/itamayo/projects/tbargia-aldalur/certs/key.pem', ssl_cert: '/home/VICOMTECH/itamayo/projects/tbargia-aldalur/certs/cert.pem', passphrase:'XXX' }; var httpServ = ( cfg.ssl ) ? require('https') : require('http'); var WebSocketServer = require('websocket').server;
define(function (require) { var fs = require('fs'); var mkdirp = require('mkdirp'); var path = require('path'); var async = require('async'); var uuid = require('node-uuid'); var _ = require('underscore'); var woodman = require('woodman'); var Mutex = require('./Mutex'); var ParamError = require('./errors/ParamError'); var ProxyError = require('./errors/ProxyError'); var InternalError = require('./errors/InternalError'); var logger = woodman.getLogger('filequeue'); /** * Mutex used to serialize task files operations */ var mutex = new Mutex(); /** * Creates a new task queue that processes tasks one after the other. * * @class * @param {function(Object, function)} worker Function to call to run a task. * The function receives the task's parameters as first parameter and a * callback function that it must call when the task is over (with a * potential error as first parameter). */ var TaskQueue = function (worker, options) { /** * The tasks that are currently running. Tasks are run in parallel * up to options.maxItems if defined. */ this.runningTasks = []; /** * The worker function to run to process each task */ this.worker = worker; /** * Queue options */ this.options = options || {}; if (typeof this.options.maxItems === 'undefined') { this.options.maxItems = -1; } /** * Base folder to store tasks */ this.baseFolder = path.resolve(__dirname, '..', (options.taskFolder || 'tasks')); /** * Has the file storage been initialized? */ this.initialized = false; // Start next pending task if needed this.start(); }; /** * Starts the task queue, scheduling the first pending task * if there is one. * * @function * @private */ TaskQueue.prototype.start = function () { this.whenReady(_.bind(function (err) { if (err) return; this.checkNextTask(); }, this)); }; /** * Initializes the task queue, creating the appropriate folders if needed * * @function * @private * @param {function} callback Callback function called when folders are ready */ TaskQueue.prototype.whenReady = function (callback) { callback = callback || function () {}; if (this.initialized) return callback(); if (this.error) return callback(this.error); var self = this; async.each([ 'id', 'pending', 'running' ], function (folder, next) { var fullPath = self.baseFolder + path.sep + folder; mkdirp(fullPath, function (err) { if (err) { return next(new InternalError( 'Path "' + fullPath + '" could not be created', err)); } return next(); }); }, function (err) { self.initialized = true; self.error = err; if (err) { return callback(err); } // Ensure that the "running" folder is empty, warn about that otherwise fs.readdir(self.baseFolder + path.sep + 'running', function (err, files) { if (err) { logger.error('when ready', 'could not check "running" folder', err); return callback(err); } var file = _.find(files, function (file) { return file.match(/\.json$/); }); if (file) { logger.warn('ghost task in "running" folder', 'file=' + file); } return callback(); }); }); }; /** * Takes the lock on the task queue and runs the callback once locked * * @function * @param {Object} task The task that wants to lock the queue * @param {function} callback Function called when lock has been taken */ TaskQueue.prototype.lock = function (task, callback) { callback = callback || function () {}; mutex.lock(task.id, _.bind(function () { this.locked = task.id; this.whenReady(callback); }, this)); }; /** * Releases the lock on the queue * * @function * @param {Object} task Task that had the lock */ TaskQueue.prototype.unlock = function (task) { mutex.unlock(task.id); }; /** * Saves the given task to the specified folder * * @function * @param {Object} task The task to save * @param {string} folder The folder the task should be saved to * @param {function} callback Function called when task was saved. */ TaskQueue.prototype.saveTaskToFolder = function (task, folder, callback) { callback = callback || function () {}; if (!task || !task.id) return callback(); var filename = this.baseFolder + path.sep + folder + path.sep + task.id + '.json'; fs.writeFile(filename, JSON.stringify(task, null, 2), function (err) { if (err) { logger.error('save', 'taskId=' + task.id, 'folder=' + folder, 'error', err.toString()); return callback(new InternalError( 'Could not save task to file', err)); } logger.log('save', 'taskId=' + task.id, 'folder=' + folder, 'done'); return callback(); }); }; /** * Removes the given task from the given folder * * @function * @param {Object} task The task to save * @param {string} folder The folder from which the task should be removed * @param {function} callback Function called when task was deleted. */ TaskQueue.prototype.removeTaskFromFolder = function (task, folder, callback) { callback = callback || function () {}; if (!task || !task.id) return callback(); var filename = this.baseFolder + path.sep + folder + path.sep + task.id + '.json'; fs.unlink(filename, function (err) { if (err) { logger.error('remove', 'taskId=' + task.id, 'folder=' + folder, 'error', err.toString()); return callback(new InternalError( 'Could not save task to file', err)); } logger.log('remove', 'taskId=' + task.id, 'folder=' + folder, 'done'); return callback(); }); }; /** * Creates a new task with the given parameters. * * The new task gets processed when possible. * * @function * @param {Object} params Task params * @param {function} callback Called with the created task ID */ TaskQueue.prototype.push = function (params, callback) { callback = callback || function () {}; if (!params) { logger.warn('push', 'no task received'); throw new ParamError('Invalid empty build task received'); } var task = { id: uuid.v1(), params: params, status: 'pending', dateCreated: (new Date()).toISOString() }; logger.log('push', 'taskId=' + task.id, 'name=' + params.name); var self = this; async.waterfall([ function (next) { logger.log('push', 'taskId=' + task.id, 'take the lock'); self.lock(task, next); }, function (next) { logger.log('push', 'taskId=' + task.id, 'save task to "id" folder'); self.saveTaskToFolder(task, 'id', next); }, function (next) { logger.log('push', 'taskId=' + task.id, 'save task to "pending" folder'); self.saveTaskToFolder(task, 'pending', next); } ], function (err) { logger.log('push', 'taskId=' + task.id, 'release the lock'); self.unlock(task); if (err) { logger.error('push', 'taskId=' + task.id, 'error', err.toString()); return callback(err); } logger.log('push', 'taskId=' + task.id, 'schedule check for next task'); _.defer(_.bind(self.checkNextTask, self)); return callback(null, task.id); }); }; /** * Returns the number of tasks that are running * * @function * @return {Number} The number of tasks */ TaskQueue.prototype.getNbRunningTasks = function () { if (!this.runningTasks) return 0; return this.runningTasks.length; }; /** * Checks whether we may run another task. Schedules next pending task for * execution if we can. * * @function * @private */ TaskQueue.prototype.checkNextTask = function () { // Fake "runner" task to take the lock var runnerTask = { id: 'runner-' + uuid.v1() }; // Nothing to do if the maximum number of tasks that may be run in // parallel has been reached. The task will eventually be picked up // in the "pending" folder once a slot becomes available. if (this.runningTasks && (this.options.maxItems > 0) && (this.runningTasks.length >= this.options.maxItems)) { logger.log('check', 'need to wait, too many tasks running at once'); return; } var self = this; var runningTask = null; async.waterfall([ function (next) { logger.log('check', 'take the lock'); self.lock(runnerTask, next); }, function (next) { logger.log('check', 'read "pending" folder'); fs.readdir(self.baseFolder + path.sep + 'pending', next); }, function (files, next) { logger.log('check', 'find first task to run'); var file = _.find(files, function (file) { return file.match(/\.json$/); }); if (!file) { logger.log('check', 'no more task to run'); return next('all run'); } return next(null, file); }, function (file, next) { logger.log('check', 'read task file', 'file=' + file); fs.readFile( self.baseFolder + path.sep + 'pending' + path.sep + file, next); }, function (data, next) { logger.log('check', 'parse JSON'); var task = null; try { task = JSON.parse(data); return next(null, task); } catch (err) { return next(err); } }, function (task, next) { logger.log('check', 'taskId=' + task.id, 'set status to "running" and save to "id" folder'); self.runningTasks.push(task); runningTask = task; task.status = 'running'; self.saveTaskToFolder(task, 'id', next); }, function (next) { logger.log('check', 'taskId=' + runningTask.id, 'save task in "running" folder'); self.saveTaskToFolder(runningTask, 'running', next); }, function (next) { logger.log('check', 'taskId=' + runningTask.id, 'remove task from "pending" folder'); self.removeTaskFromFolder(runningTask, 'pending', next); } ], function (err) { logger.log('check', 'release the lock'); self.unlock(runnerTask); if (err) { // No more task to process? Great! if (err === 'all run') { return; } if (runningTask) { logger.error('check', 'taskId=' + runningTask.id, 'error', err.toString()); } else { logger.error('check', 'error', err.toString()); } return; } logger.info('check', 'taskId=' + runningTask.id, 'schedule execution'); _.defer(function () { self.runTask(runningTask); }); return; }); }; /** * Processes the given task * * @function * @private * @param {Object} task The task to run */ TaskQueue.prototype.runTask = function (task) { if (!task) return; var self = this; logger.log('run task', 'taskId=' + task.id, 'apply worker'); this.worker(task.params, function (err, result) { if (err) { task.status = 'failure'; task.error = err.toString(); if (err instanceof ParamError) { logger.warn('run task', 'taskId=' + task.id, 'wrong parameters', task.error); task.errorCode = 400; } else if (err instanceof ProxyError) { logger.warn('run task', 'taskId=' + task.id, 'third party error', task.error); task.errorCode = 503; } else { logger.error('run task', 'taskId=' + task.id, 'error', task.error); task.errorCode = 500; } } else { logger.log('run task', 'taskId=' + task.id, 'done'); task.status = 'success'; if (result) { task.result = result; } } task.dateFinished = (new Date()).toISOString(); self.runningTasks = _.without(self.runningTasks, task); self.saveTaskToFolder(task, 'id', function (err) { if (err) { logger.error('run task', 'taskId=' + task.id, 'could not save task result', 'status=' + task.status, err.toString()); } self.removeTaskFromFolder(task, 'running', function (err) { if (err) { logger.error('run task', 'taskId=' + task.id, 'could not remove task from "running" folder', err.toString()); } // On to next pending task logger.log('run task', 'taskId=' + task.id, 'on to next task'); _.defer(_.bind(self.checkNextTask, self)); return; }); }); }); }; /** * Retrieves information about the task given as parameter. * * Note the function returns a copy of the task, not the task itself. * * @function * @param {string} taskId The ID of the task to retrieve */ TaskQueue.prototype.get = function (taskId, callback) { callback = callback || function () {}; var getTask = { id: 'get-' + uuid.v1() }; var self = this; async.waterfall([ function (next) { // logger.log('get', 'taskId=' + taskId, 'take the lock'); self.lock(getTask, next); }, function (next) { var file = self.baseFolder + path.sep + 'id' + path.sep + taskId + '.json'; fs.exists(file, function (exists) { if (exists) { fs.readFile(file, next); } else { return next('not found'); } }); }, function (data, next) { var task = null; try { task = JSON.parse(data); return next(null, task); } catch (err) { return next(err); } } ], function (err, task) { // logger.log('get', 'taskId=' + taskId, 'release the lock'); self.unlock(getTask); if (err === 'not found') { // logger.log('get', 'taskId=' + taskId, 'not found'); return callback(); } if (err) { // logger.error('get', 'taskId=' + taskId, 'error', err.toString()); return callback(err); } // logger.log('get', 'taskId=' + taskId, 'status=' + task.status); return callback(null, task); }); }; return TaskQueue; });
define(function (require) { var woodman = require('woodman'); var logger = woodman.getLogger('StateVector'); /** * Default constructor for a state vector * * @class * @param {Object} vector The initial motion vector * @param {Number} vector.position The initial position (0.0 if null) * @param {Number} vector.velocity The initial velocity (0.0 if null) * @param {Number} vector.acceleration The initial acceleration (0.0 if null) * @param {Number} vector.timestamp The initial time in seconds (now if null) */ var StateVector = function (vector) { vector = vector || {}; /** * The position of the motion along its axis. * * The position unit may be anything. */ this.position = vector.position || 0.0; /** * The velocity of the motion in position units per second. */ this.velocity = vector.velocity || 0.0; /** * The acceleration of the motion in position units per second squared. */ this.acceleration = vector.acceleration || 0.0; /** * The local time in milliseconds when the position, velocity and * acceleration are evaluated. */ this.timestamp = vector.timestamp || (Date.now() / 1000.0); logger.info('created', this); }; /** * Computes the position along the uni-dimensional axis at the given time * * @function * @param {Number} timestamp The reference time in seconds */ StateVector.prototype.computePosition = function (timestamp) { var elapsed = timestamp - this.timestamp; var result = this.position + this.velocity * elapsed + 0.5 * this.acceleration * elapsed * elapsed; logger.log('compute position returns', result); return result; }; /** * Computes the velocity along the uni-dimensional axis at the given time * * @function * @param {Number} timestamp The reference time in seconds */ StateVector.prototype.computeVelocity = function (timestamp) { var elapsed = timestamp - this.timestamp; var result = this.velocity + this.acceleration * elapsed; logger.log('compute velocity returns', result); return result; }; /** * Computes the acceleration along the uni-dimensional axis at the given time * * Note that this function merely exists for symmetry with computePosition and * computeAcceleration. In practice, this function merely returns the vector's * acceleration which is unaffected by time. * * @function * @param {Number} timestamp The reference time in seconds */ StateVector.prototype.computeAcceleration = function (timestamp) { logger.log('compute acceleration returns', this.acceleration); return this.acceleration; }; /** * Compares this vector with the specified vector for order. Returns a * negative integer, zero, or a positive integer as this vector is less than, * equal to, or greater than the specified object. * * Note that the notions of "less than" or "greater than" do not necessarily * mean much when comparing motions. In practice, the specified vector is * evaluated at the timestamp of this vector. Position is compared first. * If equal, velocity is compared next. If equal, acceleration is compared. * * TODO: the function probably returns differences in cases where it should * not because of the limited precision of floating numbers. Fix that. * * @function * @param {StateVector} vector The vector to compare * @returns {Integer} The comparison result */ StateVector.prototype.compareTo = function (vector) { var timestamp = this.timestamp; var value = 0.0; value = vector.computePosition(timestamp); if (this.position < value) { return -1; } else if (this.position > value) { return 1; } value = vector.computeVelocity(timestamp); if (this.velocity < value) { return -1; } else if (this.velocity > value) { return 1; } value = vector.computeAcceleration(timestamp); if (this.acceleration < value) { return -1; } else if (this.acceleration > value) { return 1; } return 0; }; /** * Overrides toString to return a meaningful string serialization of the * object for logging * * @function * @returns {String} A human-readable serialization of the vector */ StateVector.prototype.toString = function () { return '(position=' + this.position + ', velocity=' + this.velocity + ', acceleration=' + this.acceleration + ', timestamp=' + this.timestamp + ')'; }; // Expose the Media State Vector constructor return StateVector; });
var woodman = require('woodman'); var _ = require("lodash"); // since this initiates the code with a worker process we need to configure woodman woodman.load('console %domain - %message'); var logger = woodman.getLogger('events-reader'); // initiate the oplog eventsReader with the Mongodb oplog url and optionally start tailing module.exports = function (harvestApp) { (!harvestApp.options.oplogConnectionString) && (function () { throw new Error("Missing config.options.oplogConnectionString") }()); return harvestApp.eventsReader(harvestApp.options.oplogConnectionString).then(function (EventsReader) { logger.info('start tailing the oplog'); var eventsReader = new EventsReader(); eventsReader.tail(); return eventsReader; }).catch(function (e) { logger.error(e); throw e; }); };
define(function (require) { var spawn = require('child_process').spawn; var path = require('path'); var _ = require('underscore'); var woodman = require('woodman'); var ParamError = require('./errors/ParamError'); var InternalError = require('./errors/InternalError'); var logger = woodman.getLogger('gitaction'); return function (action, callback) { callback = callback || function () {}; action.branch = action.branch || 'master'; logger.info('action received', 'origin=' + action.origin, 'branch=' + action.branch, 'script=' + action.script, 'check=' + (action.check || 'none')); if (!action.origin) { logger.warn('Git origin not found'); return callback(new ParamError( 'The origin of the Git repository to clone must be specified')); } if (!action.script) { logger.warn('Git origin not found'); return callback(new ParamError('No action script to run')); } var env = _.clone(action.env || {}); env.PATH = process.env.PATH; if (action.privatekey) { env.GIT_SSH = path.resolve(__dirname, '..', action.dataFolder, 'deploykeys', 'ssh-' + action.privatekey + '.sh'); } else { env.GIT_SSH = path.resolve(__dirname, 'ssh-noprompt.sh'); } var script = spawn('lib/gitaction.sh', [ action.origin, (action.dataFolder || 'data'), action.branch, action.script, (action.check ? ' ' + action.check : '') ], { cwd: path.resolve(__dirname, '..'), env: env }); // Kill the script if it takes too much time var timeout = setTimeout(function () { if (!script) return; script.kill('SIGTERM'); setTimeout(function () { if (!script) return; script.kill('SIGKILL'); return; }, 10000); // Give 10 seconds to the process to exit }, 1000 * (action.timeout || (60 * 10))); // 10 minutes by default var outFragment = ''; var errFragment = ''; var log = function (type) { var fragment = (type === 'stdout') ? outFragment : errFragment; return function (data) { var str = fragment + data; var lines = str.split('\n'); // Save the line if not the end of it if (lines.length === 1) { if (type === 'stdout') { outFragment = lines[0]; } else { errFragment = lines[0]; } return; } var i = 0; while (i < lines.length - 1) { logger.log(type + ' |', lines[i].replace(/\s+$/g, '')); i += 1; } if (type === 'stdout') { outFragment = lines[i]; } else { errFragment = lines[i]; } }; }; script.stdout.on('data', log('stdout')); script.stderr.on('data', log('stderr')); script.on('close', function (code) { clearTimeout(timeout); script = null; if (outFragment) { logger.log('stdout |', outFragment.replace(/\s+$/g, '')); outFragment = null; } if (errFragment) { logger.log('stderr |', outFragment.replace(/\s+$/g, '')); errFragment = null; } if (code === null) { logger.error('git action got killed', 'origin=' + action.origin, 'branch=' + action.branch, 'script=' + action.script, 'check=' + (action.check || 'none')); return callback(new InternalError( 'git action script got killed', code)); } if (code !== 0) { logger.error('could not run git action', 'origin=' + action.origin, 'branch=' + action.branch, 'script=' + action.script, 'check=' + (action.check || 'none'), 'exit code=' + code); return callback(new InternalError( 'git action script reported an error', code)); } logger.log('run action', 'done', 'origin=' + action.origin, 'branch=' + action.branch, 'script=' + action.script, 'check=' + (action.check || 'none')); return callback(); }); }; });