blob: 7b081eaa11490a03b9c57244f437e138c23ae7ab [file] [log] [blame]
'use strict';
// See https://github.com/jshint/jshint/issues/1747 for context
/* global -Promise */
var Promise = require('es6-promise').Promise;
var net = require('net');
var spawn = require('child_process').spawn;
var fs = require('fs');
var portFinder = require('portfinder');
var findSimulators = require('node-firefox-find-simulators');
module.exports = startSimulator;
function startSimulator(options) {
options = options || {};
var detached = options.detached ? true : false;
var verbose = options.verbose ? true : false;
var port = options.port;
var timeout = options.timeout || 25000;
var simulatorOptions = {};
if (options.version) {
simulatorOptions.version = options.version;
}
return new Promise(function(resolve, reject) {
Promise.all([findSimulator(simulatorOptions), findAvailablePort(port)]).then(function(results) {
var simulator = results[0];
port = results[1];
launchSimulatorAndWaitUntilReady({
binary: simulator.bin,
profile: simulator.profile,
port: port,
detached: detached,
verbose: verbose,
timeout: timeout,
version: simulator.version
}).then(function(simulatorDetails) {
resolve(simulatorDetails);
}, function(simulatorLaunchError) {
reject(simulatorLaunchError);
});
}, function(error) {
reject(error);
});
});
}
// Find a simulator that matches the options
function findSimulator(options) {
return new Promise(function(resolve, reject) {
findSimulators(options).then(function(results) {
if (!results || results.length === 0) {
reject(new Error('No simulators installed, or cannot find them'));
}
// just returning the first result for now
resolve(results[0]);
}, function(error) {
reject(error);
});
});
}
function findAvailablePort(preferredPort) {
return new Promise(function(resolve, reject) {
// Start searching with the preferred port, if specified
if (preferredPort !== undefined) {
portFinder.basePort = preferredPort;
}
portFinder.getPort({
host: '127.0.0.1'
}, function(err, port) {
if (err) {
reject(err);
} else {
resolve(port);
}
});
});
}
// Launches the simulator and wait until it's ready to be used
function launchSimulatorAndWaitUntilReady(options) {
var port = options.port;
var binary = options.binary;
var profile = options.profile;
var timeout = options.timeout;
return new Promise(function(resolve, reject) {
launchSimulator(options).then(function(simulatorProcess) {
waitUntilSimulatorIsReady({ port: port, timeout: timeout }).then(function() {
resolve({
process: simulatorProcess,
pid: simulatorProcess.pid,
port: port,
binary: binary,
profile: profile
});
}, function(timedOutError) {
reject(timedOutError);
});
}, function(simulatorLaunchError) {
reject(simulatorLaunchError);
});
});
}
// Launches the simulator in the specified port
function launchSimulator(options) {
var detached = options.detached;
return new Promise(function(resolve, reject) {
startSimulatorProcess(options).then(function(simulatorProcess) {
// If the simulator is not detached, we need to kill its process
// once our own process exits
if (!detached) {
process.once('exit', function() {
simulatorProcess.kill('SIGTERM');
});
process.once('uncaughtException', function(error) {
if (process.listeners('uncaughtException').length === 0) {
simulatorProcess.kill('SIGTERM');
throw error;
}
});
} else {
// Totally make sure we don't keep references to this new child--
// this removes the child from the parent's event loop
// See http://nodejs.org/api/child_process.html#child_process_options_detached
simulatorProcess.unref();
}
resolve(simulatorProcess);
}, function(error) {
reject(error);
});
});
}
function startSimulatorProcess(options) {
return new Promise(function(resolve, reject) {
var simulatorBinary = options.binary;
var childOptions = { stdio: ['ignore', 'ignore', 'ignore'] };
var startDebuggerServer = '-start-debugger-server';
// Simple sanity check: make sure the simulator binary exists
if (!fs.existsSync(simulatorBinary)) {
return reject(new Error(simulatorBinary + ' does not exist'));
}
if (options.detached) {
childOptions.detached = true;
}
if (options.verbose) {
childOptions.stdio = [ process.stdin, process.stdout, process.stderr ];
}
if (options.version === '1.3' || options.version === '1.2') {
startDebuggerServer = '-dbgport';
}
// TODO do we want to pipe stdin/stdout/stderr as in commandB2G?
// https://github.com/nicola/fxos-start/blob/6b4794814e3a5c97d60abf2ab8619c635d6c3c94/index.js#L55-L57
var simulatorProcess = spawn(
simulatorBinary,
[
'-profile', options.profile,
startDebuggerServer, options.port,
'-no-remote',
'-foreground'
],
childOptions
);
resolve(simulatorProcess);
});
}
function waitUntilSimulatorIsReady(options) {
var attemptInterval = 1000;
var elapsedTime = 0;
var port = options.port;
var timeout = options.timeout;
return new Promise(function(resolve, reject) {
function ping() {
var socket = new net.Socket();
socket.on('connect', function() {
resolve();
socket.destroy();
}).on('error', function(error) {
if (error && error.code !== 'ECONNREFUSED') {
throw error;
}
socket.destroy();
maybeTryAgain();
}).connect(port, 'localhost');
}
function maybeTryAgain() {
elapsedTime += attemptInterval;
if (elapsedTime < timeout) {
setTimeout(ping, attemptInterval);
} else {
reject(new Error('Timed out trying to connect to the simulator in ' + port));
}
}
ping();
});
}
// These actually make it so that child process get killed when this process
// gets killed (except when the child process is detached, obviously)
process.once('SIGTERM', function() {
process.exit(0);
});
process.once('SIGINT', function() {
process.exit(0);
});