VulpesOS/gaia/tests/jsmarionette/runner/marionette-js-runner/host/host.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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);
}