| /** |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| |
| var nodeVersion = process.version.replace(/^v|-.*$/g, '').split('.').map(Number); |
| var useOldKeepAlive = nodeVersion[0] < 1 && nodeVersion[1] <= 10; |
| |
| var http = require('http'); |
| var url = require('url'); |
| var fs = require('fs'); |
| var adbkit = require('adbkit'); |
| var Q = require('q'); |
| var AgentKeepAlive = useOldKeepAlive ? require('old-agentkeepalive') : require('agentkeepalive'); |
| var PushSession = require('./pushsession'); |
| |
| var httpAgent = new AgentKeepAlive({ |
| maxSockets: 1, // Must be set to 1 unless we refactor to allow multiple sockets through adbkit. |
| maxFreeSockets: 1, |
| keepAliveMsecs: 3000000 |
| }); |
| |
| // Client creation is cheap. No IO done until first command. |
| var adbClient = adbkit.createClient(); |
| |
| function HarnessClient(target) { |
| this.target = target; |
| } |
| |
| HarnessClient.detectAdbTargets = function() { |
| return adbClient.listDevices() |
| .then(function(devices) { |
| devices.sort(function(a,b) { |
| return a.type < b.type ? -1 : |
| a.type > b.type ? 1 : |
| 0; |
| }); |
| return devices.map(function(d) { return 'adb:' + d.id; }); |
| }, function() { |
| return Q.reject('Could not run adb command.'); |
| }); |
| }; |
| |
| HarnessClient.prototype.autoDetectTarget = function() { |
| return HarnessClient.detectAdbTargets() |
| .then(function(devices) { |
| if (devices.length === 0) { |
| return '127.0.0.1:2424'; |
| } |
| return devices[0]; |
| }, function() { |
| return Q.reject('Could not run adb command.'); |
| }); |
| }; |
| |
| HarnessClient.prototype.doRequest = function(method, action, options) { |
| var self = this; |
| return Q.when().then(function() { |
| return self.target || self.autoDetectTarget(); |
| }).catch(function() { |
| return '127.0.0.1:2424'; |
| }).then(function(target) { |
| self.target = target; |
| var targetParts = target.split(':'); |
| var host = targetParts[0]; |
| var port = +(targetParts[1] || 2424); |
| var deviceId = null; |
| if (host == 'adb') { |
| deviceId = targetParts[1]; |
| port = +(targetParts[2] || 2424); |
| host = 'adb_' + deviceId; |
| } |
| // If using adb, try to open a socket. |
| return Q.when().then(function() { |
| return deviceId && adbClient.openTcp(deviceId, port); |
| }).catch(function() { |
| return Q.reject('Could not connect to device. Make sure you have the Chrome App Developer Tool for Mobile open.'); |
| }).then(function(adbSocket) { |
| var deferred = Q.defer(); |
| |
| if (adbSocket) { |
| // Override the function that creates a socket, instead using the existing socket. |
| // This function is identical to the one found in agentkeepalive, except that it doesn't create a new socket. |
| httpAgent.createConnection = function() { |
| return adbSocket; |
| }; |
| } else { |
| // Revert to prototype implementation. |
| delete httpAgent.createConnection; |
| } |
| options = options || {}; |
| |
| var queryParams = {}; |
| if (options.query) { |
| Object.keys(options.query).forEach(function(k) { |
| queryParams[k] = options.query[k]; |
| }); |
| } |
| if (options.appId) { |
| queryParams['appId'] = options.appId; |
| } |
| if (options.appType) { |
| queryParams['appType'] = options.appType; |
| } |
| |
| var uri = url.format({ |
| protocol: 'http', |
| hostname: host, |
| port: port, |
| pathname: action, |
| query: queryParams |
| }); |
| var pathWithQuery = uri.replace(/^.*?\/\/.*?\//, '/'); |
| process.stdout.write(method + ' ' + uri); |
| |
| var headers = { |
| 'Connection': 'keep-alive' |
| }; |
| var body = null; |
| if (method == 'POST' || method == 'PUT') { |
| body = options.body || ''; |
| if (options.json) { |
| body = JSON.stringify(options.json); |
| headers['Content-Type'] = 'application/json'; |
| } |
| headers['Content-Length'] = body.length; |
| } |
| |
| var startTime = new Date(); |
| var req = http.request({ |
| hostname: host, |
| port: port, |
| method: method, |
| path: pathWithQuery, |
| headers: headers, |
| agent: httpAgent |
| }); |
| |
| var outStream = null; |
| var innerDeferred = null; |
| |
| if (options.saveResponseToFile) { |
| innerDeferred = Q.defer(); |
| deferred.promise.then(function() { |
| return innerDeferred.promise; |
| }, function(e) { |
| if (outStream) { |
| outStream.close(); |
| } |
| throw e; |
| }); |
| } |
| |
| req.on('error', function(e) { |
| deferred.reject(e); |
| }); |
| req.on('socket', function(socket) { |
| // Disable timeouts for packapk until we can optimize it :(. |
| socket.setTimeout(0); |
| // Check if for some reason our adb socket was not used. |
| if (adbSocket && adbSocket != socket) { |
| delete httpAgent.createConnection; |
| adbSocket.destroy(); |
| } |
| }); |
| |
| if (body) { |
| req.write(body); |
| } |
| req.end(); |
| req.on('response', function(res) { |
| process.stdout.write(' ==> ' + res.statusCode); |
| var body = ''; |
| var lastDotTime = Date.now(); |
| if (options.saveResponseToFile) { |
| outStream = fs.createWriteStream(options.saveResponseToFile); |
| res.on('data', function() { |
| var now = Date.now(); |
| if (now - lastDotTime > 1000) { |
| lastDotTime = now; |
| process.stdout.write('.'); |
| } |
| }); |
| res.pipe(outStream); |
| outStream.on('finish', function() { |
| outStream.close(innerDeferred.resolve); // close() is async, call cb after close completes. |
| }); |
| } else { |
| res.setEncoding('utf8'); |
| res.on('data', function(chunk) { |
| body += chunk; |
| }); |
| } |
| res.on('end', function() { |
| process.stdout.write(' (' + (new Date() - startTime) + ')\n'); |
| if (res.statusCode != 200) { |
| deferred.reject(new Error('Server returned status code: ' + res.statusCode + '\n\n' + body)); |
| return; |
| } |
| if (!options.saveResponseToFile) { |
| try { |
| body = options.expectJson ? JSON.parse(body) : body; |
| } catch (e) { |
| deferred.reject(new Error('Invalid JSON: ' + body.slice(500))); |
| return; |
| } |
| } |
| deferred.resolve({res:res, body:body}); |
| }); |
| }); |
| return deferred.promise; |
| }); |
| }); |
| }; |
| |
| HarnessClient.prototype.info = function() { |
| return this.doRequest('GET', '/info', { expectJson: true }); |
| }; |
| |
| HarnessClient.prototype.assetmanifest = function(/* optional */ appId) { |
| return this.doRequest('GET', '/assetmanifest', {expectJson: true, appId: appId}); |
| }; |
| |
| HarnessClient.prototype.menu = function() { |
| return this.doRequest('POST', '/menu'); |
| }; |
| |
| HarnessClient.prototype.quit = function() { |
| return this.doRequest('POST', '/quit'); |
| }; |
| |
| HarnessClient.prototype.evalJs = function(someJs) { |
| return this.doRequest('POST', '/exec', { query: {code: someJs} }); |
| }; |
| |
| HarnessClient.prototype.launch = function(/* optional */ appId) { |
| return this.doRequest('POST', '/launch', { appId: appId}); |
| }; |
| |
| HarnessClient.prototype.deleteAllApps = function() { |
| return this.doRequest('POST', '/deleteapp', { query: {'all': 1} }); |
| }; |
| |
| HarnessClient.prototype.deleteApp = function(/* optional */ appId) { |
| return this.doRequest('POST', '/deleteapp', { appId: appId}); |
| }; |
| |
| HarnessClient.prototype.pushZip = function(appId, appType, zipData, /* optional */ totalPushBytes, /* optional */ manifestEtag) { |
| var query = {}; |
| if (totalPushBytes) { |
| query['expectBytes'] = totalPushBytes; |
| } |
| if (manifestEtag) { |
| query['manifestEtag'] = manifestEtag; |
| } |
| return this.doRequest('POST', '/zippush', { |
| appId: appId, |
| appType: appType, |
| body: zipData, |
| query: query |
| }); |
| }; |
| |
| HarnessClient.prototype.pushFile = function(appId, appType, payload, etag, remotePath, /* optional */ totalPushBytes, /* optional */ manifestEtag) { |
| var query = { |
| 'path': remotePath, |
| 'etag': etag |
| }; |
| if (totalPushBytes) { |
| query['expectBytes'] = totalPushBytes; |
| } |
| if (manifestEtag) { |
| query['manifestEtag'] = manifestEtag; |
| } |
| return this.doRequest('PUT', '/putfile', { |
| appId: appId, |
| appType: appType, |
| body: payload, |
| query: query |
| }); |
| }; |
| |
| HarnessClient.prototype.deleteFiles = function(appId, deleteList, /* optional */ manifestEtag) { |
| var query = {}; |
| if (manifestEtag) { |
| query['manifestEtag'] = manifestEtag; |
| } |
| return this.doRequest('POST', 'deletefiles', { appId: appId, json: {'paths': deleteList}, query: query}); |
| }; |
| |
| HarnessClient.prototype.createPushSession = function(dir) { |
| return new PushSession(this, dir); |
| }; |
| |
| module.exports = HarnessClient; |
| |