Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
243 lines
7 KiB
JavaScript
Executable file
243 lines
7 KiB
JavaScript
Executable file
'use strict';
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var Promise = require('promise');
|
|
var debug = require('debug')('host/host');
|
|
var request = require('./lib/request');
|
|
var spawn = require('child_process').spawn;
|
|
var uuid = require('uuid');
|
|
var _ = require('lodash');
|
|
|
|
var VENV = 'VIRTUALENV_PATH' in process.env ?
|
|
process.env.VIRTUALENV_PATH :
|
|
__dirname + '/../venv';
|
|
|
|
var _FORCE_DEBUG = false;
|
|
|
|
function Host(socketPath, process, log) {
|
|
debug('Created new host', socketPath);
|
|
this.socketPath = socketPath;
|
|
this.process = process;
|
|
this.log = log;
|
|
this.sessions = {};
|
|
this.pendingSessions = [];
|
|
this.error = null;
|
|
this.restarting = Promise.resolve();
|
|
this.isRequestInProgress = false;
|
|
this.didCrashDuringRequest = false;
|
|
this.onerror = this.onerror.bind(this);
|
|
this.onexit = this.onexit.bind(this);
|
|
|
|
process.on('error', this.onerror);
|
|
process.on('exit', this.onexit);
|
|
|
|
var restart = this.restart.bind(this);
|
|
|
|
// XXXDebug Pro Tip: set _FORCE_DEBUG to see this output instead of piping it.
|
|
if (!_FORCE_DEBUG) {
|
|
process.stdout.on('data', function(chunk) {
|
|
var str = chunk.toString();
|
|
if (str.indexOf('Exception') !== -1) {
|
|
console.info('Will restart -- Found error in "', str, '"');
|
|
restart();
|
|
}
|
|
});
|
|
|
|
process.stderr.on('data', function(chunk) {
|
|
var str = chunk.toString();
|
|
|
|
// Bad file descriptor is a non-issue. Don't restart when this happens.
|
|
// It simply means that the file descriptor we use to pipe the logs was
|
|
// closed pre-maturely. We'll recover from this.
|
|
if (str.indexOf('error') !== -1 &&
|
|
str.indexOf('Bad file descriptor') === -1) {
|
|
console.info('Will restart -- Found error in "', str, '"');
|
|
restart();
|
|
}
|
|
});
|
|
}
|
|
|
|
EventEmitter.call(this);
|
|
}
|
|
module.exports = Host;
|
|
|
|
Host.prototype = {
|
|
__proto__: EventEmitter.prototype,
|
|
|
|
destroy: function() {
|
|
// If there are any pending session creates wait for those to cleanly finish
|
|
// first.
|
|
if (this.pendingSessions.length) {
|
|
return Promise.all(this.pendingSessions).then(this.destroy.bind(this));
|
|
}
|
|
|
|
return Promise.all(
|
|
_.map(this.sessions, function(session) {
|
|
return session.destroy();
|
|
})
|
|
)
|
|
.then(function() {
|
|
if (Object.keys(this.sessions).length !== 0) {
|
|
return Promise.reject(new Error('Not all sessions were deleted'));
|
|
}
|
|
|
|
var proc = this.process;
|
|
proc.removeListener('exit', this.onexit);
|
|
return new Promise(function(accept, reject) {
|
|
proc.once('exit', function() {
|
|
proc.removeListener('error', this.onerror);
|
|
if (this.error) return reject(this.error);
|
|
accept();
|
|
}.bind(this));
|
|
|
|
proc.kill();
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
restart: function() {
|
|
// Python died so let's restart! Yay! Long live python.
|
|
console.log('Python crashed! Will restart service...');
|
|
if (this.isRequestInProgress) {
|
|
console.log('There was a request in progress!');
|
|
this.didCrashDuringRequest = true;
|
|
}
|
|
|
|
this.restarting = new Promise(function(resolve) {
|
|
// Kill ourselves.
|
|
this.process.removeListener('error', this.onerror);
|
|
this.process.removeListener('exit', this.onexit);
|
|
|
|
if (!this.process.connected) {
|
|
return resolve();
|
|
}
|
|
|
|
this.process.on('exit', resolve);
|
|
this.process.kill();
|
|
}.bind(this))
|
|
.then(function() {
|
|
// Start a new python child.
|
|
return spawnPythonChild();
|
|
})
|
|
.then(function(details) {
|
|
this.socketPath = details.socketPath;
|
|
this.process = details.process;
|
|
this.log = details.log;
|
|
this.process.on('error', this.onerror);
|
|
this.process.on('exit', this.onexit);
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
Issue a request to the hosts underlying python http server.
|
|
*/
|
|
request: function(path, options) {
|
|
if (this.error) {
|
|
return Promise.reject(this.error);
|
|
}
|
|
|
|
return this.restarting.then(function() {
|
|
this.isRequestInProgress = true;
|
|
return request(this.socketPath, path, options);
|
|
}.bind(this))
|
|
.catch(function() {}) // TODO(gaye): Replace with finally
|
|
.then(function(result) {
|
|
this.isRequestInProgress = false;
|
|
if (this.didCrashDuringRequest) {
|
|
console.log('Will retry ' + path + ' request...');
|
|
this.didCrashDuringRequest = false;
|
|
return this.request(path, options);
|
|
}
|
|
|
|
return result;
|
|
}.bind(this));
|
|
},
|
|
|
|
onerror: function(error) {
|
|
debug('Python process error', error.toString());
|
|
this.error = error;
|
|
this.process.removeListener('exit', this.onexit);
|
|
this.process.kill();
|
|
},
|
|
|
|
onexit: function(exit) {
|
|
debug('Python process exited unexpectedly', JSON.stringify(exit));
|
|
this.error = new Error('Python unexpected exit ' + JSON.stringify(exit));
|
|
this.process.removeListener('exit', this.onexit);
|
|
}
|
|
};
|
|
|
|
Host.create = function() {
|
|
return spawnPythonChild().then(function(details) {
|
|
return new Host(details.socketPath, details.process, details.log);
|
|
});
|
|
};
|
|
|
|
function spawnPythonChild() {
|
|
var socketPath = '/tmp/marionette-socket-host-' + uuid.v1() + '.sock';
|
|
|
|
// XXXDebug Pro Tip: set stdio to [0, 1, 2] to get Python output.
|
|
var pythonChild = spawnVirtualEnv(
|
|
'gaia-integration',
|
|
['--path=' + socketPath],
|
|
{ stdio: _FORCE_DEBUG === false ?
|
|
['pipe', 'pipe', 'pipe', 'pipe'] :
|
|
[0, 1, 2] }
|
|
);
|
|
|
|
var failOnChildError = new Promise(function(accept, reject) {
|
|
function onError(error) {
|
|
debug('Python process error during initial connect', error.toString());
|
|
reject(error);
|
|
}
|
|
|
|
pythonChild.once('error', onError);
|
|
pythonChild.once('exit', function(exit) {
|
|
// Ensure we don't call error callback somehow...
|
|
pythonChild.removeListener('error', onError);
|
|
reject(new Error(
|
|
'Unexpected exit during connect: ' +
|
|
'signal = ' + exit.signal + ', ' +
|
|
'code = ' + exit.code
|
|
));
|
|
});
|
|
});
|
|
|
|
var connect = request(socketPath, '/connect').then(function() {
|
|
pythonChild.removeAllListeners('error');
|
|
pythonChild.removeAllListeners('exit');
|
|
return {
|
|
socketPath: socketPath,
|
|
process: pythonChild,
|
|
log: pythonChild.stdio[3]
|
|
};
|
|
});
|
|
|
|
return Promise.race([connect, failOnChildError]);
|
|
}
|
|
|
|
/**
|
|
Wrapper for spawning a python process which expects a venv with particular
|
|
packages. Same interface as spawn but overrides path and ensures certain env
|
|
variables are not set which conflict.
|
|
|
|
@param {String} bin path to binary to execute.
|
|
@param {Array} argv list of arguments.
|
|
@param {Object} opts options for node's spawn.
|
|
@return {ChildProcess}
|
|
*/
|
|
function spawnVirtualEnv(bin, argv, opts) {
|
|
opts = opts || {};
|
|
// Clone current environment variables...
|
|
var env = {};
|
|
_.assign(env, process.env);
|
|
opts.env = env;
|
|
|
|
// Prepend binary wrappers to path.
|
|
env.PATH = VENV + '/bin/:' + process.env.PATH;
|
|
|
|
// Ensure we don't conflict with other wrappers or package managers.
|
|
delete env.PYTHONHOME;
|
|
|
|
return spawn(bin, argv, opts);
|
|
}
|