monaca-lib
Version:
Monaca cloud API bindings for JavaScript
524 lines (435 loc) • 13 kB
JavaScript
(function() {
'use strict';
var path = require('path'),
os = require('os'),
fs = require('fs'),
spawn = require('child_process').spawn,
request = require('request'),
portfinder = require('portfinder'),
adb = require('adbkit'),
client = adb.createClient(),
Q = require('q'),
http = require('http'),
Padlock = require('padlock').Padlock;
// Failed starting the proxy. Probably because no iOS device is connected.
var ERROR_START_PROXY = 'ERROR_START_PROXY',
// Failed to find page to inspect. Probably because the app is not running in the Debugger.
ERROR_NO_PAGE = 'ERROR_NO_PAGE',
// Device is not connected by USB or USB debugging is not enabled.
ERROR_USB_CONNECTION = 'ERROR_USB_CONNECTION',
// Generic ADB error. Will happen when adb fails to launch or port forwarding fails.
ERROR_ADB = 'ERROR_ADB',
// Will happen if a request is made to inspect a device that is not "android" or "ios".
ERROR_NO_SUCH_DEVICE = 'ERROR_NO_SUCH_DEVICE';
var adbProc = [];
var config = {
'adbPath': 'adb',
'proxyPath': function() {
switch (os.platform()) {
case 'darwin':
return path.join(__dirname, '..', 'bin', 'ios-webkit-debug-proxy', 'darwin', 'ios_webkit_debug_proxy');
case 'win32':
return path.join(__dirname, '..', 'bin', 'ios-webkit-debug-proxy', 'windows', 'ios-webkit-debug-proxy.exe');
default:
return '/usr/local/bin/ios_webkit_debug_proxy';
}
}(),
'inspectorCallback': function(args) {
console.log('Unimplemented. Called with the following args', args);
}
};
if (!global.webkitProxyLock) {
global.webkitProxyLock = new Padlock();
}
var getPort = function(basePort) {
var deferred = Q.defer();
if (basePort) {
portfinder.basePort = basePort;
}
else {
portfinder.basePort = 8002;
}
portfinder.getPort(function(err, port) {
if (err) {
deferred.reject(err);
}
else {
deferred.resolve(port);
}
});
return deferred.promise;
};
var startDevTools = function(webSocketUrl) {
var deferred = Q.defer();
try {
var result = config.inspectorCallback({
app: path.join(__dirname, 'inspector-app'),
webSocketUrl: webSocketUrl,
nwUrl: path.join(__dirname, 'inspector-app', 'devtools', 'front_end', 'inspector.html'),
electronUrl: 'file://' + path.join(__dirname, 'inspector-app', 'electron.html'),
inspectorUrl: 'devtools/front_end/inspector.html?ws=' + webSocketUrl.replace(/^ws:\/\//, ''),
args: '?ws=' + webSocketUrl.replace(/^ws:\/\//, '')
});
deferred.resolve(result);
}
catch (err) {
console.log('Calling inspector callback has failed. ' + err);
deferred.reject(err);
}
return deferred.promise;
};
var startProxy = function() {
var spawnProcess = function(binary, port) {
var deferred = Q.defer();
try {
var proc = spawn(binary, ['-c', 'null:' + port + ',:' + (port + 1) + '-' + (port + 100)]);
proc.on('error', function(error) {
deferred.reject(error);
});
setTimeout(function() {
if (proc.exitCode === null) {
deferred.resolve(proc);
}
else {
deferred.reject();
}
}, 400);
}
catch (e) {
deferred.reject(e);
}
return deferred.promise;
};
var runNext = function(port, nbrOfTimes) {
var binary = config.proxyPath,
nbrOfTimes = nbrOfTimes || 0;
if (!config.proxyPath || !fs.existsSync(config.proxyPath)) {
return Q.reject(ERROR_START_PROXY);
}
return spawnProcess(binary, port).then(
function(proc) {
var url = 'http://localhost:' + port + '/json';
global.iosWebkitProxyProc = proc;
global.iosWebkitProxyUrl = url;
proc.on('exit', function() {
delete global.iosWebkitProxyProc;
delete global.iosWebkitProxyUrl;
});
return Q.resolve(url);
},
function() {
if (nbrOfTimes > 3) {
return Q.reject(ERROR_START_PROXY);
}
var deferred = Q.defer();
setTimeout(function() {
runNext(port, nbrOfTimes + 1)
.then(
function(result) {
deferred.resolve(result);
},
function(error) {
deferred.reject(error);
}
);
}, 500);
return deferred.promise;
}
);
};
var deferred = Q.defer(),
lock = global.webkitProxyLock;
lock.runwithlock(function() {
var proc = global.iosWebkitProxyProc;
if (proc) {
lock.release();
return deferred.resolve(global.iosWebkitProxyUrl);
}
getPort(8002)
.then(runNext)
.then(
function(result) {
deferred.resolve(result);
},
function(error) {
deferred.reject(error);
}
)
.finally(function() {
lock.release();
});
});
return deferred.promise;
};
var stopProxy = function() {
if (global.iosWebkitProxyProc) {
try {
global.iosWebkitProxyProc.kill();
}
catch (e) {
}
delete global.iosWebkitProxyProc;;
delete global.iosWebkitProxyUrl;
}
return Q.resolve();
};
var firstSuccess = function(promises) {
var deferred = Q.defer(),
successes = [], errors = [];
var onSuccess = function(data) {
successes.push(data);
};
var onError = function(error) {
errors.push(error);
};
var onComplete = function() {
if (successes.length + errors.length === promises.length) {
if (successes.length) {
deferred.resolve(successes[0]);
}
else {
deferred.reject(errors[0]);
}
}
};
for (var i = 0, l = promises.length; i < l; i ++) {
promises[i]
.then(onSuccess, onError)
.finally(onComplete);
}
return deferred.promise;
};
var searchDeviceForUrl = function(infoUrl, options, retries) {
var deferred = Q.defer();
retries = retries || 0;
request({
url: infoUrl,
json: true
}, function(error, response, body) {
if (error) {
deferred.reject(error);
}
else if (response.statusCode !== 200) {
deferred.reject(response.statusMessage);
}
else if (body.length === 0) {
// Retry since sometimes it takes some time for the device to turn up in the list.
if (retries < 10) {
setTimeout(function() {
deferred.resolve(searchDeviceForUrl(infoUrl, options, retries + 1));
}, 500);
}
else {
deferred.reject(ERROR_NO_PAGE);
}
}
else {
var result = body.filter(function(page) {
var ret = true;
if (options.pageUrl) {
ret = ret && options.pageUrl.trim() === page.url.trim();
}
if (options.projectId) {
ret = ret && page.url.indexOf('/' + options.projectId.trim() + '/') >= 0;
}
return ret;
});
if (result[0]) {
deferred.resolve(result[0].webSocketDebuggerUrl);
}
else {
deferred.reject(ERROR_NO_PAGE);
}
}
});
return deferred.promise;
};
var findWebSocketUrl = function(listUrl, options) {
var deferred = Q.defer();
request({
url: listUrl,
json: true,
timeout: 1000
}, function(error, response, body) {
if (error || body.length === 0 || response.statusCode !== 200) {
return deferred.reject(ERROR_USB_CONNECTION);
}
var promises = body.map(function(device) {
var infoUrl = 'http://' + device.url + '/json';
return searchDeviceForUrl(infoUrl, options);
});
firstSuccess(promises).then(
function(url) {
deferred.resolve(url);
},
function(error) {
deferred.reject(error);
}
);
});
return deferred.promise;
};
var launchIOS = function(options) {
return startProxy()
.then(
function(listUrl) {
return findWebSocketUrl(listUrl, options)
.catch(
function(error) {
return stopProxy()
.then(
function() {
return Q.reject(error);
}
);
}
);
}
)
.then(startDevTools);
};
var findAbstractSockets = function() {
return client.listDevices()
.then(
function(devices) {
var promises = devices
.map(
function(device) {
return client.shell(device.id, 'cat /proc/net/unix | grep -a remote')
.then(adb.util.readAll)
.then(
function(output) {
return [device.id, output.toString()];
}
);
}
);
return Q.all(promises).then(
function(results) {
var sockets = {};
for (var i = 0, l = results.length; i < l; i ++) {
var deviceId = results[i][0],
output = results[i][1];
var matches = output.match(/@[^\s]+/g) || [];
sockets[deviceId] = matches
.map(function(socket) {
return socket.replace(/^@/, 'localabstract:');
});
}
return sockets;
}
);
}
);
};
var forwardAndroidDevice = function(deviceId, abstractSocket) {
return getPort(8102)
.then(
function(port) {
return client.forward(deviceId, 'tcp:' + port, abstractSocket)
.then(
function() {
return 'http://localhost:' + port + '/json';
}
);
}
);
};
var removeForwarding = function() {
var deferred = Q.defer();
var proc = spawn(config.adbPath, ['forward', '--remove-all']);
adbProc.push(proc);
proc.on('exit', function(code, signal) {
if (code === 0) {
deferred.resolve();
}
else {
deferred.reject(ERROR_ADB);
}
});
proc.on('error', function() {
deferred.reject(ERROR_ADB);
});
return deferred.promise;
};
var launchAndroid = function(options) {
return removeForwarding()
.then(findAbstractSockets)
.then(
function(abstractSockets) {
var promises = [];
var deviceIds = Object.keys(abstractSockets);
if (deviceIds.length === 0) {
return Q.reject(ERROR_USB_CONNECTION);
}
for (var i = 0, l = deviceIds.length; i < l; i ++) {
var deviceId = deviceIds[i];
if (abstractSockets.hasOwnProperty(deviceId)) {
var sockets = abstractSockets[deviceId];
for (var j = 0, ll = sockets.length; j < ll; j ++) {
var abstractSocket = sockets[j];
promises.push(forwardAndroidDevice(deviceId, abstractSocket));
}
}
}
if (promises.length === 0) {
return Q.reject(ERROR_NO_PAGE);
}
return Q.all(promises);
}
)
.then(
function(urls) {
var promises = urls
.map(
function(url) {
return searchDeviceForUrl(url, options);
}
);
return firstSuccess(promises);
}
)
.then(startDevTools);
};
var launch = function(options) {
options = options || {};
switch (options.type) {
case 'ios':
return launchIOS(options);
case 'android':
return launchAndroid(options);
default:
return Q.reject(ERROR_NO_SUCH_DEVICE);
}
};
var initialize = function(configOptions) {
configOptions = configOptions || {};
for (var i in configOptions) {
if (config.hasOwnProperty(i)) {
config[i] = configOptions[i];
}
}
}
var onClose = function() {
try {
global.iosWebkitProxyProc.kill();
}
catch (e) {}
for (var i = 0, l = adbProc.length; i < l; i ++) {
var proc = adbProc[i];
try {
proc.kill();
}
catch (e) {}
}
};
process.on('exit', onClose);
process.on('uncaughtException', onClose);
process.on('SIGINT', onClose);
process.on('SIGTERM', onClose);
module.exports = {
initialize: initialize,
launch: launch,
startProxy: startProxy
};
})();