-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(launcher): extract timeout, retry, process
So that we can easily reuse only some features.
- Loading branch information
Showing
12 changed files
with
956 additions
and
427 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,215 +1,130 @@ | ||
var spawn = require('child_process').spawn; | ||
var path = require('path'); | ||
var fs = require('fs'); | ||
var rimraf = require('rimraf'); | ||
|
||
var KarmaEventEmitter = require('../events').EventEmitter; | ||
var EventEmitter = require('events').EventEmitter; | ||
var q = require('q'); | ||
var log = require('../logger').create('launcher'); | ||
var env = process.env; | ||
|
||
var BEING_CAPTURED = 1; | ||
var CAPTURED = 2; | ||
var BEING_KILLED = 3; | ||
var FINISHED = 4; | ||
var BEING_TIMEOUTED = 5; | ||
var RESTARTING = 5; | ||
var BEING_FORCE_KILLED = 6; | ||
|
||
|
||
var BaseBrowser = function(id, emitter, captureTimeout, retryLimit) { | ||
var self = this; | ||
var capturingUrl; | ||
var exitCallbacks = []; | ||
/** | ||
* Base launcher that any custom launcher extends. | ||
*/ | ||
var BaseLauncher = function(id, emitter) { | ||
if (this.start) { | ||
return; | ||
} | ||
|
||
// TODO(vojta): figure out how to do inheritance with DI | ||
Object.keys(EventEmitter.prototype).forEach(function(method) { | ||
this[method] = EventEmitter.prototype[method]; | ||
}, this); | ||
KarmaEventEmitter.call(this); | ||
|
||
this.killTimeout = 2000; | ||
this.id = id; | ||
this.state = null; | ||
this._tempDir = path.normalize((env.TMPDIR || env.TMP || env.TEMP || '/tmp') + '/karma-' + | ||
id.toString()); | ||
|
||
this.start = function(url) { | ||
capturingUrl = url; | ||
self.state = BEING_CAPTURED; | ||
|
||
try { | ||
log.debug('Creating temp dir at ' + self._tempDir); | ||
fs.mkdirSync(self._tempDir); | ||
} catch (e) {} | ||
this.error = null; | ||
|
||
self._start(capturingUrl + '?id=' + self.id); | ||
|
||
if (captureTimeout) { | ||
setTimeout(self._onTimeout, captureTimeout); | ||
} | ||
}; | ||
var self = this; | ||
var killingPromise; | ||
var previousUrl; | ||
|
||
this.start = function(url) { | ||
previousUrl = url; | ||
|
||
this._start = function(url) { | ||
self._execCommand(self._getCommand(), self._getOptions(url)); | ||
this.error = null; | ||
this.state = BEING_CAPTURED; | ||
this.emit('start', url + '?id=' + this.id); | ||
}; | ||
|
||
|
||
this.markCaptured = function() { | ||
if (self.state === BEING_CAPTURED) { | ||
self.state = CAPTURED; | ||
this.kill = function() { | ||
// Already killed, or being killed. | ||
if (killingPromise) { | ||
return killingPromise; | ||
} | ||
}; | ||
|
||
|
||
this.isCaptured = function() { | ||
return self.state === CAPTURED; | ||
}; | ||
|
||
killingPromise = this.emitAsync('kill').then(function() { | ||
self.state = FINISHED; | ||
}); | ||
|
||
this.kill = function(callback) { | ||
var exitCallback = callback || function() {}; | ||
this.state = BEING_KILLED; | ||
|
||
log.debug('Killing %s', self.name); | ||
if (self.state === FINISHED) { | ||
process.nextTick(exitCallback); | ||
} else if (self.state === BEING_KILLED) { | ||
exitCallbacks.push(exitCallback); | ||
} else { | ||
self.state = BEING_KILLED; | ||
self._process.kill(); | ||
exitCallbacks.push(exitCallback); | ||
setTimeout(self._onKillTimeout, self.killTimeout); | ||
} | ||
return killingPromise; | ||
}; | ||
|
||
this.forceKill = function() { | ||
this.kill(); | ||
this.state = BEING_FORCE_KILLED; | ||
|
||
this._onKillTimeout = function() { | ||
if (self.state !== BEING_KILLED) { | ||
return; | ||
} | ||
|
||
log.warn('%s was not killed in %d ms, sending SIGKILL.', self.name, self.killTimeout); | ||
|
||
self._process.kill('SIGKILL'); | ||
return killingPromise; | ||
}; | ||
|
||
this._onTimeout = function() { | ||
if (self.state !== BEING_CAPTURED) { | ||
this.restart = function() { | ||
if (this.state === BEING_FORCE_KILLED) { | ||
return; | ||
} | ||
|
||
log.warn('%s have not captured in %d ms, killing.', self.name, captureTimeout); | ||
|
||
self.state = BEING_TIMEOUTED; | ||
self._process.kill(); | ||
}; | ||
|
||
|
||
this.toString = function() { | ||
return self.name; | ||
}; | ||
|
||
|
||
this._getCommand = function() { | ||
var cmd = path.normalize(env[self.ENV_CMD] || self.DEFAULT_CMD[process.platform]); | ||
|
||
if (!cmd) { | ||
log.error('No binary for %s browser on your platform.\n\t' + | ||
'Please, set "%s" env variable.', self.name, self.ENV_CMD); | ||
} | ||
|
||
return cmd; | ||
}; | ||
|
||
|
||
this._execCommand = function(cmd, args) { | ||
// normalize the cmd, remove quotes (spawn does not like them) | ||
if (cmd.charAt(0) === cmd.charAt(cmd.length - 1) && '\'`"'.indexOf(cmd.charAt(0)) !== -1) { | ||
cmd = cmd.substring(1, cmd.length - 1); | ||
log.warn('The path should not be quoted.\n Normalized the path to %s', cmd); | ||
if (!killingPromise) { | ||
killingPromise = this.emitAsync('kill'); | ||
} | ||
|
||
log.debug(cmd + ' ' + args.join(' ')); | ||
self._process = spawn(cmd, args); | ||
|
||
var errorOutput = ''; | ||
|
||
self._process.on('close', function(code) { | ||
self._onProcessExit(code, errorOutput); | ||
}); | ||
|
||
self._process.on('error', function(err) { | ||
if (err.code === 'ENOENT') { | ||
retryLimit = 0; | ||
errorOutput = 'Can not find the binary ' + cmd + '\n\t' + | ||
'Please set env variable ' + self.ENV_CMD; | ||
killingPromise.then(function() { | ||
if (self.state === BEING_FORCE_KILLED) { | ||
self.state = FINISHED; | ||
} else { | ||
errorOutput += err.toString(); | ||
killingPromise = null; | ||
log.debug('Restarting %s', self.name); | ||
self.start(previousUrl); | ||
} | ||
}); | ||
|
||
// Node 0.8 does not emit the error | ||
if (process.versions.node.indexOf('0.8') === 0) { | ||
self._process.stderr.on('data', function(data) { | ||
var msg = data.toString(); | ||
|
||
if (msg.indexOf('No such file or directory') !== -1) { | ||
retryLimit = 0; | ||
errorOutput = 'Can not find the binary ' + cmd + '\n\t' + | ||
'Please set env variable ' + self.ENV_CMD; | ||
} else { | ||
errorOutput += msg; | ||
} | ||
}); | ||
} | ||
self.state = RESTARTING; | ||
}; | ||
|
||
|
||
this._onProcessExit = function(code, errorOutput) { | ||
log.debug('Process %s exitted with code %d', self.name, code); | ||
|
||
if (self.state === BEING_CAPTURED) { | ||
log.error('Cannot start %s\n\t%s', self.name, errorOutput); | ||
} | ||
|
||
if (self.state === CAPTURED) { | ||
log.error('%s crashed.\n\t%s', self.name, errorOutput); | ||
this.markCaptured = function() { | ||
if (this.state === BEING_CAPTURED) { | ||
this.state = CAPTURED; | ||
} | ||
}; | ||
|
||
retryLimit--; | ||
|
||
if (self.state === BEING_CAPTURED || self.state === BEING_TIMEOUTED) { | ||
if (retryLimit > 0) { | ||
return self._cleanUpTmp(function() { | ||
log.info('Trying to start %s again.', self.name); | ||
self.start(capturingUrl); | ||
}); | ||
} else { | ||
emitter.emit('browser_process_failure', self); | ||
} | ||
} | ||
this.isCaptured = function() { | ||
return this.state === CAPTURED; | ||
}; | ||
|
||
self.state = FINISHED; | ||
self._cleanUpTmp(function(err) { | ||
exitCallbacks.forEach(function(exitCallback) { | ||
exitCallback(err); | ||
}); | ||
exitCallbacks = []; | ||
}); | ||
this.toString = function() { | ||
return this.name; | ||
}; | ||
|
||
this._done = function(error) { | ||
killingPromise = killingPromise || q(); | ||
|
||
this._cleanUpTmp = function(done) { | ||
log.debug('Cleaning temp dir %s', self._tempDir); | ||
rimraf(self._tempDir, done); | ||
}; | ||
this.error = this.error || error; | ||
this.emit('done'); | ||
|
||
if (this.error && this.state !== BEING_FORCE_KILLED && this.state !== RESTARTING) { | ||
emitter.emit('browser_process_failure', this); | ||
} | ||
|
||
this._getOptions = function(url) { | ||
return [url]; | ||
this.state = FINISHED; | ||
}; | ||
|
||
this.STATE_BEING_CAPTURED = BEING_CAPTURED; | ||
this.STATE_CAPTURED = CAPTURED; | ||
this.STATE_BEING_KILLED = BEING_KILLED; | ||
this.STATE_FINISHED = FINISHED; | ||
this.STATE_RESTARTING = RESTARTING; | ||
this.STATE_BEING_FORCE_KILLED = BEING_FORCE_KILLED; | ||
}; | ||
|
||
var baseBrowserDecoratorFactory = function(id, emitter, timeout) { | ||
return function(self) { | ||
BaseBrowser.call(self, id, emitter, timeout, 3); | ||
BaseLauncher.decoratorFactory = function(id, emitter) { | ||
return function(launcher) { | ||
BaseLauncher.call(launcher, id, emitter); | ||
}; | ||
}; | ||
baseBrowserDecoratorFactory.$inject = ['id', 'emitter', 'config.captureTimeout']; | ||
|
||
|
||
// PUBLISH | ||
exports.BaseBrowser = BaseBrowser; | ||
exports.decoratorFactory = baseBrowserDecoratorFactory; | ||
module.exports = BaseLauncher; |
Oops, something went wrong.