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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,243 @@
'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);
}

View file

@ -0,0 +1,64 @@
'use strict';
var Host = require('./host');
var Session = require('./session');
exports.help = {
group: {
title: 'Socket Host',
description: 'Socket host uses mozbase to manage gecko applications.'
},
arguments: {
'--serial': {
dest: 'device_serial',
defaultValue: null,
help: 'Serial port (only with buildapp=device)',
type: 'string'
},
'--symbols-path': {
dest: 'symbols_path',
help: 'Path to build symbols used by crash reporting.',
defaultValue: null,
type: 'string'
},
'--b2gpath': {
dest: 'b2g_home',
help: 'Path to b2g directory.',
defaultValue: null,
type: 'string'
},
'--buildapp': {
type: 'string',
choices: ['device', 'desktop', 'emulator'],
defaultValue: 'desktop',
help: 'Type of gecko application to run.'
},
'--runtime': {
defaultValue: null,
help: 'Path to b2g-bin when using buildapp=desktop'
},
'--device-type': {
dest: 'device_type',
defaultValue: 'phone'
},
'--dump-path': {
dest: 'dump_path',
defaultValue: process.cwd() + '/crash',
help: 'path in which to store crash dumps. Will default to ' +
'the \'minidumps\' directory in the current working directory'
},
'--chrome': {
defaultValue: 'chrome://b2g/content/shell.html',
help: '--chrome option to set when starting b2g-desktop'
}
}
};
exports.createHost = Host.create;
exports.createSession = Session.create;

View file

@ -0,0 +1,99 @@
'use strict';
/**
Node http/socket/promise wrapper.
*/
var http = require('http');
var debug = require('debug')('marionette-socket-host:request');
var Promise = require('promise');
var RETRIES = 60;
var RETRY_TIMER = 5000;
function PythonError(msg) {
Error.call(msg);
this.name = 'python_error';
}
PythonError.prototype = {
__proto__: Error.prototype
};
module.exports = function request(socketPath, path, json, retry) {
retry = retry || 0;
var body = JSON.stringify(json || {});
return new Promise(function(accept, reject) {
debug('issue request', socketPath, path, json);
var req = http.request({
socketPath: socketPath,
method: 'POST',
path: path,
headers: {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'application/json'
}
});
req.setTimeout(300 * 1000, function() {
reject(new Error('Host request timeout exceeded.'));
});
// The unix socket server may or may not be ready at this point so we retry
// up to 5 times to ensure we are connected...
req.once('error', function(err) {
if (retry > RETRIES) {
debug('At maximum retries...');
return reject(err);
}
debug('Error in socket request retrying...', err);
setTimeout(function() {
request(socketPath, path, json, retry + 1).then(accept, reject);
}, RETRY_TIMER);
});
// request sender....
req.write(body);
req.on('abort', function() {
debug('Request', retry, 'aborted.');
});
// response handler...
req.on('response', function(res) {
var data = '';
res.on('data', function(buffer) {
data += buffer;
});
res.on('end', function() {
debug(res.statusCode, data);
var json = JSON.parse(data);
if (res.statusCode < 200 || res.statusCode > 299) {
var err = new PythonError(json.message);
err.stack = json.stack;
reject(err);
}
else if (path === '/start_runner') {
if (data.length) {
accept(json);
}
else {
reject(
new Error('Request to start_runner expects data with response!')
);
}
}
else {
accept(json);
}
});
});
// send the request...
req.end();
});
};

View file

@ -0,0 +1,13 @@
'use strict';
var Promise = require('promise');
module.exports = function waitForEvent(listener, event) {
return new Promise(function(accept, reject) {
listener.on(event, function handler(message) {
// accept the message before clearing the listener
accept(message);
listener.removeListener(event, handler);
});
});
};

View file

@ -0,0 +1,18 @@
Metadata-Version: 1.1
Name: gaia-runner-service
Version: 0.3
Summary: A utility for running tests over a socket from JS to python
Home-page: UNKNOWN
Author: Andrew Halberstadt
Author-email: ahalberstadt@mozilla.com
License: MPL 2.0
Description: See https://github.com/mozilla-b2g/gaia/tree/master/tests/python/runner-service
Keywords: mozilla
Platform: UNKNOWN
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Software Development :: Libraries :: Python Modules

View file

@ -0,0 +1,13 @@
setup.py
gaia_runner_service.egg-info/PKG-INFO
gaia_runner_service.egg-info/SOURCES.txt
gaia_runner_service.egg-info/dependency_links.txt
gaia_runner_service.egg-info/entry_points.txt
gaia_runner_service.egg-info/not-zip-safe
gaia_runner_service.egg-info/requires.txt
gaia_runner_service.egg-info/top_level.txt
runner_service/__init__.py
runner_service/listener.py
runner_service/runintegration.py
runner_service/handlers/__init__.py
runner_service/handlers/runner.py

View file

@ -0,0 +1,4 @@
[console_scripts]
gaia-integration=runner_service:runintegration.cli

View file

@ -0,0 +1,3 @@
mozrunner >= 6.11
mozprofile >= 0.21
mozcrash >= 0.16

View file

@ -0,0 +1,5 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import runintegration

View file

@ -0,0 +1,11 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from . import runner
runner_handlers = {
'desktop': runner.DesktopHandler,
'device': runner.DeviceHandler,
'emulator': runner.EmulatorHandler,
}

View file

@ -0,0 +1,207 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from abc import ABCMeta, abstractmethod, abstractproperty
import json
import os
import posixpath
import sys
from mozprofile import Profile
from mozrunner import (
B2GDesktopRunner,
B2GDeviceRunner,
B2GEmulatorRunner
)
# We log over a special pipe (fd3) which node knows to hook up.
GECKO_LOG_FD = 3
class DeviceUtils:
def __init__(self, deviceManager):
self.dm = deviceManager
def _purge_data(self):
# Clean up data on device.
print("cleaning up data on device.")
data_paths = ['/cache/*',
'/data/local/debug_info_trigger',
'/data/local/indexedDB',
'/data/local/OfflineCache',
'/data/local/permissions.sqlite',
'/data/local/storage/persistent',
'/data/local/storage/default']
for path in data_paths:
if self.dm.dirExists(path):
self.dm.removeDir(path)
def _purge_wifi_networks(self):
# Remove remembered networks
print("removing remembered wifi networks.")
self.dm.removeDir('/data/misc/wifi/wpa_supplicant.conf')
def _purge_storage_data(self):
# Clean up storage.
print("cleaning up storage on device.")
# TODO: Remove hard-coded paths once bug 1018079 is resolved
storage_paths = ['/mnt/sdcard/',
'/mnt/extsdcard/',
'/storage/sdcard/',
'/storage/sdcard0/',
'/storage/sdcard1/']
for path in storage_paths:
if self.dm.dirExists(path):
for item in self.dm.listFiles(path):
self.dm.removeDir('/'.join([path, item]))
def _purge_installed_apps(self):
# Clean up apps on device.
apps = json.loads(self.dm.pullFile('/data/local/webapps/webapps.json'))
system_install_time = apps['system.gaiamobile.org']['installTime']
for app in apps.values():
if app.get('installTime') > system_install_time:
# removing any webapps installed since build time
path = posixpath.join(app.get('basePath'), app.get('id'))
print('Removing %s' % path)
self.dm.removeDir(path)
def _stop_b2g(self):
print("stopping b2g on device before starting runner.")
self.dm.shellCheckOutput(['stop', 'b2g'])
class MozrunnerHandler(object):
__metaclass__ = ABCMeta
runner = None
def __init__(self, symbols_path=None, *args, **kwargs):
fd = os.fdopen(GECKO_LOG_FD, 'a')
process_args = {
'stream': fd,
'onFinish': self.on_finish
}
dump_path = kwargs.pop('dump_path', os.path.join(os.getcwd(), "minidumps"))
self.common_runner_args = {
'process_args': process_args,
'symbols_path': symbols_path,
'dump_save_path': dump_path,
'env': {
# Required for crash reporting...
'MOZ_CRASHREPORTER': '1',
'MOZ_CRASHREPORTER_NO_REPORT': '1'
}
}
# Ensure we set the DISPLAY if it is set in the current environment so
# things like Xvfb work correctly...
if 'DISPLAY' in os.environ:
self.common_runner_args['env']['DISPLAY'] = os.environ['DISPLAY']
@abstractmethod
def start_runner(self, target, options):
pass
@abstractmethod
def stop_runner(self):
pass
def on_finish(self):
pass
def cleanup(self):
if self.runner:
self.runner.cleanup()
class DesktopHandler(MozrunnerHandler):
def start_runner(self, binary, options):
profile = Profile(profile=options.get('profile'))
cmdargs = options.get('argv', [])
if 'screen' in options and \
'width' in options['screen'] and \
'height' in options['screen']:
screen = '--screen=%sx%s' % (options['screen']['width'], \
options['screen']['height'])
if 'dpi' in options['screen']:
screen = '%s@%s' % (screen, options['screen']['dpi'])
cmdargs.append(screen)
if options.get('noRemote', True):
cmdargs.append('-no-remote')
if options.get('url'):
cmdargs.append(options['url'])
if options.get('chrome'):
cmdargs.extend(['-chrome', options['chrome']])
if options.get('startDebugger'):
cmdargs.extend(['-start-debugger-server', options['startDebugger']])
self.runner = B2GDesktopRunner(binary, cmdargs=cmdargs, profile=profile,
**self.common_runner_args)
self.runner.start()
def stop_runner(self):
self.runner.stop()
class EmulatorHandler(MozrunnerHandler):
def __init__(self, b2g_home, *args, **kwargs):
MozrunnerHandler.__init__(self, *args, **kwargs)
self.runner = B2GEmulatorRunner(b2g_home=b2g_home, **self.common_runner_args)
self.runner.device.start()
def start_runner(self, binary, options):
port = options.get('port')
profile = Profile(profile=options.get('profile'))
self.runner.device.setup_port_forwarding(local_port=port, remote_port=port)
self.runner.profile = profile
self.runner.start()
if not self.runner.device.wait_for_port(port):
raise Exception("Wait for port timed out")
def stop_runner(self):
self.runner.stop()
class DeviceHandler(MozrunnerHandler):
def __init__(self, *args, **kwargs):
serial = kwargs.pop('serial', None)
MozrunnerHandler.__init__(self, *args, **kwargs)
self.runner = B2GDeviceRunner(serial=serial, **self.common_runner_args)
self.runner.device.connect()
def start_runner(self, target, options):
du = DeviceUtils(self.runner.device.dm)
device = self.runner.device
port = options.get('port')
profile = Profile(profile=options.get('profile'))
device.setup_port_forwarding(local_port=port, remote_port=port)
du._stop_b2g()
du._purge_data()
du._purge_wifi_networks()
du._purge_storage_data()
self.runner.profile = profile
self.runner.start()
if not device.wait_for_port(port=port, timeout=600):
raise Exception("Wait for port timed out")
def stop_runner(self):
self.runner.stop()

View file

@ -0,0 +1,157 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import socket
import sys
import os
import json
import uuid
import BaseHTTPServer
import traceback
import mozcrash
from .handlers import runner_handlers
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
REQUESTS = {}
INACTIVE = []
def log_message(self, format, *args):
'''
Override log message to bypass certain behaviors which assume tcp
sockets
'''
pass
def log_error(self, format, *args):
'''
Override log error to bypass certain behaviors which assume tcp
sockets
'''
print(args)
def send_json(self, status, payload):
'''
Respond to incoming http request...
:param int status:
:param dict payload: payload to send as json to the socket.
'''
payload = json.dumps(payload)
self.send_response(status)
self.send_header('Content-Length', len(payload))
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(payload)
def do_GET(self):
pass
def do_POST(self):
content_length = self.headers.getheader('content-length', 0)
payload = json.loads(self.rfile.read(int(content_length)))
handler = self.path[1:]
method = 'do_' + handler
# No matter what we send application/json
if hasattr(self, method):
try:
func = getattr(self, method)
result = func(payload)
except Exception as e:
self.send_json(500, {
'message': str(e),
'stack': traceback.format_exc()
})
else:
self.send_json(200, result)
else:
self.send_json(404, { message: 'Unknown path :' + self.path })
def do_test_end(self, data):
print 'TEST-END | %s' % data['test']
def do_test_start(self, data):
print 'TEST-START | %s' % data['test']
def do_test_status(self, data):
print 'TEST-STATUS | %s | %s' % (data['subtest'], data['status'])
def do_connect(self, payload):
'''
Simple connection ready hook to signal socket readiness...
'''
return {}
def do_get_crash_info(self, payload):
# Somewhat terrible (maybe) hack to get CrashInfo private interface.
crash_info = mozcrash.mozcrash.CrashInfo(**payload)
# Return first error or nothing...
for info in crash_info:
return {
'signature': info.signature,
'stackwalk_stdout': info.stackwalk_stdout,
'stackwalk_stderr': info.stackwalk_stderr,
'stackwalk_retcode': info.stackwalk_retcode,
'stackwalk_errors': info.stackwalk_errors
}
return {}
def do_start_runner(self, payload):
'''
Begin a runner
:param str|None binary: Target binary (usually b2g-bin) for desktop.
:param dict options: Options specific to the buildapp type.
'''
binary = payload.get('binary')
options = payload.get('options', {})
handler_args = {
'symbols_path': options.get('symbols_path')
}
if 'b2g_home' in options:
handler_args['b2g_home'] = options['b2g_home']
if options['buildapp'] == 'device' and 'serial' in options:
handler_args['serial'] = options['serial']
if 'dump_path' in options:
handler_args['dump_path'] = options['dump_path']
start_id = str(uuid.uuid4())
handler = runner_handlers[options['buildapp']](**handler_args)
handler.start_runner(binary, options)
print "Started runner with id = {0}".format(start_id)
self.REQUESTS[start_id] = handler
return { 'id': start_id }
def do_stop_runner(self, payload):
print "Stopping runner with id = {0}".format(payload['id'])
payload_id = payload['id']
if not payload_id in self.REQUESTS:
if payload_id in self.INACTIVE:
sys.stderr.write(
'Received request to stop ' +
payload_id +
' but already stopped so nothing to do.\n')
return {}
runner_ids = str.join(
', ',
map(lambda x: str(x), self.REQUESTS.keys() + self.INACTIVE))
msg = 'Cannot stop %s - service has %s' % (payload_id, runner_ids)
raise Exception(msg)
self.INACTIVE.append(payload_id)
handler = self.REQUESTS.pop(payload_id)
handler.stop_runner()
handler.cleanup()
return {}

View file

@ -0,0 +1,50 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from argparse import ArgumentParser
import threading
import sys
import socket
import os
import time
import SocketServer
from .listener import RequestHandler
class Server(SocketServer.ThreadingMixIn, SocketServer.UnixStreamServer):
pass
def cli(args=sys.argv[1:]):
'''
Begin the server note that the majority of arguments will be passed via the
cli but over the unix socket instead...
'''
parser = ArgumentParser()
parser.add_argument('--path',
dest='path',
default='/tmp/marionette_socket_host_worker',
help='Path to bind unix socket to'
)
args = parser.parse_args(args)
# Make sure the socket does not already exist.
try:
# XXX: This is potentially a bad thing maybe we should raise or warn
# here...
os.unlink(args.path)
except OSError:
if os.path.exists(args.path):
raise
server = Server(args.path, RequestHandler)
server.timeout = 300
server_thread = threading.Thread(target=server.serve_forever)
server_thread.deamon = True
server.running = True
server_thread.start()
if __name__ == '__main__':
sys.exit(cli())

View file

@ -0,0 +1,40 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
from setuptools import setup, find_packages
# NOTE: import the following due to linux bug http://bugs.python.org/issue15881
try:
import multiprocessing
except ImportError:
pass
PACKAGE_VERSION = '0.4'
deps = ['mozrunner >= 6.11',
'mozprofile >= 0.21']
setup(name='gaia-runner-service',
version=PACKAGE_VERSION,
description='A utility for running tests over a socket from JS to python',
long_description='See https://github.com/mozilla-b2g/gaia/tree/master/tests/python/runner-service',
classifiers=['Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
],
keywords='mozilla',
author='Andrew Halberstadt',
author_email='ahalberstadt@mozilla.com',
license='MPL 2.0',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=deps,
entry_points="""
[console_scripts]
gaia-integration=runner_service:runintegration.cli
""")

View file

@ -0,0 +1,127 @@
'use strict';
var Promise = require('promise');
var assign = require('object-assign');
var fsPath = require('path');
var indent = require('indent-string');
var mozrunner = require('mozilla-runner');
var util = require('util');
var detectBinary = Promise.denodeify(mozrunner.detectBinary);
var DEFAULT_LOCATION = fsPath.join(process.cwd(), 'firefox');
function Session(host, id, options) {
this.host = host;
this.id = id;
this.options = options;
}
module.exports = Session;
Session.prototype = {
$rpc: { methods: ['destroy', 'checkError'] },
checkError: function(profileConfig, err) {
var request = {
// TODO: This does not work for devices as is...
dump_directory: profileConfig.profile + '/minidumps',
symbols_path: this.options.symbols_path,
dump_save_path: this.options.dump_path
};
return this.host.request('/get_crash_info', request)
.then(function(info) {
// If info is empty, we didn't crash the process. We threw an error
// in JavaScript code instead.
if (Object.keys(info).length === 0) {
// The error we were passed originally will suffice in this case.
return err;
}
// Otherwise, check to see if the stackwalk binary return cleanly or with
// error.
if (info.stackwalk_retcode !== 0) {
// Error during stackwalk.
return parseCrashInfoWithStackwalkError(info);
}
// Everything is OK with the stackwalk, let's format a nice error with it.
return parseCrashInfo(info);
});
},
destroy: function() {
return this.host.request('/stop_runner', { id : this.id })
.then(function() {
delete this.host.sessions[this.id];
this.id = null;
}.bind(this));
}
};
Session.create = function(host, profile, options) {
options = assign({ profile: profile, buildapp: 'desktop' }, options);
if (options.b2g_home && options.buildapp === 'desktop') {
return Promise.reject('Only specify --b2gpath for device or emulator');
}
if (!options.b2g_home && options.buildapp === 'emulator') {
return Promise.reject('Must specify --b2gpath for emulator');
}
var startSession = resolveBinary(options)
.then(function(binary) {
return host.request('/start_runner', { binary: binary, options: options });
})
.then(function(result) {
var session = new Session(host, result.id, options);
host.sessions[result.id] = session;
// Session no longer pending.
host.pendingSessions.splice(host.pendingSessions.indexOf(startSession), 1);
return session;
})
.catch(function(error) {
console.error(error);
});
host.pendingSessions.push(startSession);
return startSession;
};
/**
Figure out where the b2g-bin lives based on options.
@param {Object} options (as described by .help).
@return {Promise<Null|String>} null if none is needed or a path.
*/
function resolveBinary(options) {
if (options.buildapp !== 'desktop') return Promise.resolve();
if (options.runtime) return Promise.resolve(options.runtime);
var binary = options.target || DEFAULT_LOCATION;
return detectBinary(binary, { product: 'firefox' });
}
function parseCrashInfo(info) {
var msg = util.format('Crash detected at: %s', info.signature);
var error = new Error(msg);
error.stack = msg + '\n' + info.stackwalk_stdout;
error.name = 'ProcessCrash Stackwalk';
return error;
}
function parseCrashInfoWithStackwalkError(info) {
var msg = 'Crash detected but error running stackwalk\n';
if (Array.isArray(info.stackwalk_errors)) {
info.stackwalk_errors.forEach(function(err) {
msg += indent(err, ' ', 2) + '\n';
});
}
var error = new Error(msg);
error.name = 'ProcessCrash';
error.stack = msg +
'\n' +
indent((info.stackwalk_stderr || ''), ' ', 4);
return error;
}