new file mode 100644
--- /dev/null
+++ b/devtools/shared/adb/adb-client.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/*
+ * A module to track device changes
+ * Adapted from adb.js at
+ * https://github.com/mozilla/adbhelper/tree/f44386c2d8cb7635a7d2c5a51191c89b886f8327
+ */
+
+"use strict";
+
+const { AdbSocket } = require("./adb-socket");
+
+const OKAY = 0x59414b4f;
+const FAIL = 0x4c494146;
+
+let _sockets = [ ];
+
+// Return buffer, which differs between Gecko versions
+function getBuffer(aPacket) {
+ return aPacket.buffer ? aPacket.buffer : aPacket;
+}
+
+// @param aPacket The packet to get the length from.
+// @param aIgnoreResponse True if this packet has no OKAY/FAIL.
+// @return A js object { length:...; data:... }
+function unpackPacket(aPacket, aIgnoreResponse) {
+ let buffer = getBuffer(aPacket);
+ console.debug("Len buffer: " + buffer.byteLength);
+ if (buffer.byteLength === 4 && !aIgnoreResponse) {
+ console.debug("Packet empty");
+ return { length: 0, data: "" };
+ }
+ let lengthView = new Uint8Array(buffer, aIgnoreResponse ? 0 : 4, 4);
+ let decoder = new TextDecoder();
+ let length = parseInt(decoder.decode(lengthView), 16);
+ let text = new Uint8Array(buffer, aIgnoreResponse ? 4 : 8, length);
+ return { length, data: decoder.decode(text) };
+}
+
+// Checks if the response is expected (defaults to OKAY).
+// @return true if response equals expected.
+function checkResponse(aPacket, aExpected = OKAY) {
+ let buffer = getBuffer(aPacket);
+ let view = new Uint32Array(buffer, 0, 1);
+ if (view[0] == FAIL) {
+ console.debug("Response: FAIL");
+ }
+ console.debug("view[0] = " + view[0]);
+ return view[0] == aExpected;
+}
+
+// @param aCommand A protocol-level command as described in
+// http://androidxref.com/4.0.4/xref/system/core/adb/OVERVIEW.TXT and
+// http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
+// @return A 8 bit typed array.
+function createRequest(aCommand) {
+ let length = aCommand.length.toString(16).toUpperCase();
+ while (length.length < 4) {
+ length = "0" + length;
+ }
+
+ let encoder = new TextEncoder();
+ console.debug("Created request: " + length + aCommand);
+ return encoder.encode(length + aCommand);
+}
+
+function close() {
+ _sockets.forEach(function(s) {
+ s.close();
+ });
+}
+
+function connect() {
+ let tmp = new AdbSocket();
+ _sockets.push(tmp);
+ return tmp;
+}
+
+let client = {
+ getBuffer,
+ unpackPacket,
+ checkResponse,
+ createRequest,
+ connect,
+ close
+};
+
+module.exports = client;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/adb/adb-running-checker.js
@@ -0,0 +1,75 @@
+/* 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/.
+ */
+
+/*
+ * Uses host:version service to detect if ADB is running
+ * Modified from adb-file-transfer from original ADB
+ */
+
+"use strict";
+
+const client = require("./adb-client");
+
+exports.check = async function check() {
+ let socket;
+ let state;
+
+ console.debug("Asking for host:version");
+
+ return new Promise(resolve => {
+ let runFSM = function runFSM(aData) {
+ console.debug("runFSM " + state);
+ switch (state) {
+ case "start":
+ let req = client.createRequest("host:version");
+ socket.send(req);
+ state = "wait-version";
+ break;
+ case "wait-version":
+ // TODO: Actually check the version number to make sure the daemon
+ // supports the commands we want to use
+ let { length, data } = client.unpackPacket(aData);
+ console.debug("length: ", length, "data: ", data);
+ socket.close();
+ let version = parseInt(data, "16");
+ if (version >= 31) {
+ resolve(true);
+ } else {
+ console.log("killing existing adb as we need version >= 31");
+ resolve(false);
+ }
+ break;
+ default:
+ console.debug("Unexpected State: " + state);
+ resolve(false);
+ }
+ };
+
+ let setupSocket = function() {
+ socket.s.onerror = function(aEvent) {
+ console.debug("running checker onerror");
+ resolve(false);
+ };
+
+ socket.s.onopen = function(aEvent) {
+ console.debug("running checker onopen");
+ state = "start";
+ runFSM();
+ };
+
+ socket.s.onclose = function(aEvent) {
+ console.debug("running checker onclose");
+ };
+
+ socket.s.ondata = function(aEvent) {
+ console.debug("running checker ondata");
+ runFSM(aEvent.data);
+ };
+ };
+
+ socket = client.connect();
+ setupSocket();
+ });
+};
new file mode 100644
--- /dev/null
+++ b/devtools/shared/adb/adb-socket.js
@@ -0,0 +1,73 @@
+/* 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/. */
+
+"use strict";
+
+const { Cu } = require("chrome");
+
+function createTCPSocket(location, port, options) {
+ // Starting with FF57, jsm share the same global and requires some special code
+ const { TCPSocket } =
+ Cu.getGlobalForObject(Cu.import("resource://gre/modules/Services.jsm", {}));
+
+ // Starting with FF43, TCPSocket is now exposed via WebIDL
+ return new TCPSocket(location, port, options);
+}
+
+// Creates a socket connected to the adb instance.
+// This instantiation is sync, and returns before we know if opening the
+// connection succeeds. Callers must attach handlers to the s field.
+class AdbSocket {
+ constructor() {
+ this.s = createTCPSocket("127.0.0.1", 5037, { binaryType: "arraybuffer" });
+ }
+
+ /**
+ * Dump the first few bytes of the given array to the console.
+ *
+ * @param {TypedArray} aArray
+ * the array to dump
+ */
+ _hexdump(aArray) {
+ let decoder = new TextDecoder("windows-1252");
+ let array = new Uint8Array(aArray.buffer);
+ let s = decoder.decode(array);
+ let len = array.length;
+ let dbg = "len=" + len + " ";
+ let l = len > 20 ? 20 : len;
+
+ for (let i = 0; i < l; i++) {
+ let c = array[i].toString(16);
+ if (c.length == 1)
+ c = "0" + c;
+ dbg += c;
+ }
+ dbg += " ";
+ for (let i = 0; i < l; i++) {
+ let c = array[i];
+ if (c < 32 || c > 127) {
+ dbg += ".";
+ } else {
+ dbg += s[i];
+ }
+ }
+ console.debug(dbg);
+ }
+
+ // debugging version of tcpsocket.send()
+ send(aArray) {
+ this._hexdump(aArray);
+
+ this.s.send(aArray.buffer, aArray.byteOffset, aArray.byteLength);
+ }
+
+ close() {
+ if (this.s.readyState === "open" ||
+ this.s.readyState === "connecting") {
+ this.s.close();
+ }
+ }
+}
+
+exports.AdbSocket = AdbSocket;
new file mode 100644
--- /dev/null
+++ b/devtools/shared/adb/adb.js
@@ -0,0 +1,932 @@
+/* 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/. */
+
+// Wrapper around the ADB utility.
+
+"use strict";
+
+const { Cc, Ci } = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const client = require("./adb-client");
+const { getFileForBinary } = require("./adb-binary");
+const { setTimeout } = require("resource://gre/modules/Timer.jsm");
+const { PromiseUtils } = require("resource://gre/modules/PromiseUtils.jsm");
+const { OS } = require("resource://gre/modules/osfile.jsm");
+const { Services } = require("resource://gre/modules/Services.jsm");
+
+let ready = false;
+let didRunInitially = false;
+
+const OKAY = 0x59414b4f;
+// const FAIL = 0x4c494146;
+// const STAT = 0x54415453;
+const DATA = 0x41544144;
+const DONE = 0x454e4f44;
+
+const ADB = {
+ get didRunInitially() {
+ return didRunInitially;
+ },
+ set didRunInitially(newVal) {
+ didRunInitially = newVal;
+ },
+
+ get ready() {
+ return ready;
+ },
+ set ready(newVal) {
+ ready = newVal;
+ },
+
+ get adbFilePromise() {
+ if (this._adbFilePromise) {
+ return this._adbFilePromise;
+ }
+ this._adbFilePromise = getFileForBinary();
+ return this._adbFilePromise;
+ },
+
+ // We startup by launching adb in server mode, and setting
+ // the tcp socket preference to |true|
+ start() {
+ return new Promise(async (resolve, reject) => {
+ let onSuccessfulStart = () => {
+ Services.obs.notifyObservers(null, "adb-ready");
+ this.ready = true;
+ resolve();
+ };
+
+ let isAdbRunning = await require("./adb-running-checker").check();
+ if (isAdbRunning) {
+ this.didRunInitially = false;
+ console.log("Found ADB process running, not restarting");
+ onSuccessfulStart();
+ return;
+ }
+ console.log("Didn't find ADB process running, restarting");
+
+ this.didRunInitially = true;
+ let process = Cc["@mozilla.org/process/util;1"]
+ .createInstance(Ci.nsIProcess);
+ // FIXME: Bug 1481691 - We should avoid extracting files every time.
+ let adbFile = await this.adbFilePromise;
+ process.init(adbFile);
+ // Hide command prompt window on Windows
+ try {
+ // startHidden is 55+
+ process.startHidden = true;
+ // noShell attribute is 58+
+ process.noShell = true;
+ } catch (e) {
+ }
+ let params = ["start-server"];
+ let self = this;
+ process.runAsync(params, params.length, {
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "process-finished":
+ onSuccessfulStart();
+ break;
+ case "process-failed":
+ self.ready = false;
+ reject();
+ break;
+ }
+ }
+ }, false);
+ });
+ },
+
+ /**
+ * Stop the ADB server, but only if we started it. If it was started before
+ * us, we return immediately.
+ *
+ * @param boolean sync
+ * In case, we do need to kill the server, this param is passed through
+ * to kill to determine whether it's a sync operation.
+ */
+ async stop(sync) {
+ if (!this.didRunInitially) {
+ return; // We didn't start the server, nothing to do
+ }
+ await this.kill(sync);
+ },
+
+ /**
+ * Kill the ADB server. We do this by running ADB again, passing it
+ * the "kill-server" argument.
+ *
+ * @param {Boolean} sync
+ * Whether or not to kill the server synchronously. In general,
+ * this should be false. But on Windows, an add-on may fail to update
+ * if its copy of ADB is running when Firefox tries to update it.
+ * So add-ons who observe their own updates and kill the ADB server
+ * beforehand should do so synchronously on Windows to make sure
+ * the update doesn't race the killing.
+ */
+ async kill(sync) {
+ let process = Cc["@mozilla.org/process/util;1"]
+ .createInstance(Ci.nsIProcess);
+ let adbFile = await this.adbFilePromise;
+ process.init(adbFile);
+ // Hide command prompt window on Windows
+ try {
+ // startHidden is 55+
+ process.startHidden = true;
+ // noShell attribute is 58+
+ process.noShell = true;
+ } catch (e) {
+ }
+ let params = ["kill-server"];
+
+ if (sync) {
+ process.run(true, params, params.length);
+ console.log("adb kill-server: " + process.exitValue);
+ this.ready = false;
+ this.didRunInitially = false;
+ } else {
+ let self = this;
+ process.runAsync(params, params.length, {
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "process-finished":
+ console.log("adb kill-server: " + process.exitValue);
+ Services.obs.notifyObservers(null, "adb-killed");
+ self.ready = false;
+ self.didRunInitially = false;
+ break;
+ case "process-failed":
+ console.log("adb kill-server failure: " + process.exitValue);
+ // It's hard to say whether or not ADB is ready at this point,
+ // but it seems safer to assume that it isn't, so code that wants
+ // to use it later will try to restart it.
+ Services.obs.notifyObservers(null, "adb-killed");
+ self.ready = false;
+ self.didRunInitially = false;
+ break;
+ }
+ }
+ }, false);
+ }
+ },
+
+ // Start tracking devices connecting and disconnecting from the host.
+ // We can't reuse runCommand here because we keep the socket alive.
+ // @return The socket used.
+ trackDevices: function adb_trackDevices() {
+ console.log("trackDevices");
+ let socket = client.connect();
+ let waitForFirst = true;
+ let devices = {};
+
+ socket.s.onopen = function() {
+ console.log("trackDevices onopen");
+ Services.obs.notifyObservers(null, "adb-track-devices-start");
+ let req = client.createRequest("host:track-devices");
+ socket.send(req);
+
+ };
+
+ socket.s.onerror = function(event) {
+ console.log("trackDevices onerror: " + event);
+ Services.obs.notifyObservers(null, "adb-track-devices-stop");
+ };
+
+ socket.s.onclose = function() {
+ console.log("trackDevices onclose");
+
+ // Report all devices as disconnected
+ for (let dev in devices) {
+ devices[dev] = false;
+ EventEmitter.emit(ADB, "device-disconnected", dev);
+ }
+
+ Services.obs.notifyObservers(null, "adb-track-devices-stop");
+
+ // When we lose connection to the server,
+ // and the adb is still on, we most likely got our server killed
+ // by local adb. So we do try to reconnect to it.
+ setTimeout(function() { // Give some time to the new adb to start
+ if (ADB.ready) { // Only try to reconnect/restart if the add-on is still enabled
+ ADB.start().then(function() { // try to connect to the new local adb server
+ // or, spawn a new one
+ ADB.trackDevices(); // Re-track devices
+ });
+ }
+ }, 2000);
+ };
+
+ socket.s.ondata = function(aEvent) {
+ console.log("trackDevices ondata");
+ let data = aEvent.data;
+ console.log("length=" + data.byteLength);
+ let dec = new TextDecoder();
+ console.log(dec.decode(new Uint8Array(data)).trim());
+
+ // check the OKAY or FAIL on first packet.
+ if (waitForFirst) {
+ if (!client.checkResponse(data, OKAY)) {
+ socket.close();
+ return;
+ }
+ }
+
+ let packet = client.unpackPacket(data, !waitForFirst);
+ waitForFirst = false;
+
+ if (packet.data == "") {
+ // All devices got disconnected.
+ for (let dev in devices) {
+ devices[dev] = false;
+ EventEmitter.emit(ADB, "device-disconnected", dev);
+ }
+ } else {
+ // One line per device, each line being $DEVICE\t(offline|device)
+ let lines = packet.data.split("\n");
+ let newDev = {};
+ lines.forEach(function(aLine) {
+ if (aLine.length == 0) {
+ return;
+ }
+
+ let [dev, status] = aLine.split("\t");
+ newDev[dev] = status !== "offline";
+ });
+ // Check which device changed state.
+ for (let dev in newDev) {
+ if (devices[dev] != newDev[dev]) {
+ if (dev in devices || newDev[dev]) {
+ let topic = newDev[dev] ? "device-connected"
+ : "device-disconnected";
+ EventEmitter.emit(ADB, topic, dev);
+ }
+ devices[dev] = newDev[dev];
+ }
+ }
+ }
+ };
+ },
+
+ // Sends back an array of device names.
+ listDevices: function adb_listDevices() {
+ console.log("listDevices");
+
+ return this.runCommand("host:devices").then(
+ function onSuccess(data) {
+ let lines = data.split("\n");
+ let res = [];
+ lines.forEach(function(aLine) {
+ if (aLine.length == 0) {
+ return;
+ }
+ let [ device ] = aLine.split("\t");
+ res.push(device);
+ });
+ return res;
+ }
+ );
+ },
+
+ // sends adb forward aLocalPort aDevicePort
+ forwardPort: function adb_forwardPort(aLocalPort, aDevicePort) {
+ console.log("forwardPort " + aLocalPort + " -- " + aDevicePort);
+ // <host-prefix>:forward:<local>;<remote>
+
+ return this.runCommand("host:forward:" + aLocalPort + ";" + aDevicePort)
+ .then(function onSuccess(data) {
+ return data;
+ });
+ },
+
+ // pulls a file from the device.
+ // send "host:transport-any" why??
+ // if !OKAY, return
+ // send "sync:"
+ // if !OKAY, return
+ // send STAT + hex4(path.length) + path
+ // recv STAT + 12 bytes (3 x 32 bits: mode, size, time)
+ // send RECV + hex4(path.length) + path
+ // while(needs data):
+ // recv DATA + hex4 + data
+ // recv DONE + hex4(0)
+ // send QUIT + hex4(0)
+ pull: function adb_pull(aFrom, aDest) {
+ let deferred = PromiseUtils.defer();
+ let socket;
+ let state;
+ let fileData = null;
+ let currentPos = 0;
+ let chunkSize = 0;
+ let pkgData;
+ let headerArray = new Uint32Array(2);
+ let currentHeaderLength = 0;
+
+ let encoder = new TextEncoder();
+ let infoLengthPacket;
+
+ console.log("pulling " + aFrom + " -> " + aDest);
+
+ let shutdown = function() {
+ console.log("pull shutdown");
+ socket.close();
+ deferred.reject("BAD_RESPONSE");
+ };
+
+ // extract chunk data header info. to headerArray.
+ let extractChunkDataHeader = function(data) {
+ let tmpArray = new Uint8Array(headerArray.buffer);
+ for (let i = 0; i < 8 - currentHeaderLength; i++) {
+ tmpArray[currentHeaderLength + i] = data[i];
+ }
+ };
+
+ // chunk data header is 8 bytes length,
+ // the first 4 bytes: hex4("DATA"), and
+ // the second 4 bytes: hex4(chunk size)
+ let checkChunkDataHeader = function(data) {
+ if (data.length + currentHeaderLength >= 8) {
+ extractChunkDataHeader(data);
+
+ if (headerArray[0] != DATA) {
+ shutdown();
+ return false;
+ }
+ // remove header info. from socket package data
+ pkgData = data.subarray(8 - currentHeaderLength, data.length);
+ chunkSize = headerArray[1];
+ currentHeaderLength = 0;
+ return true;
+ }
+
+ // If chunk data header info. is separated into more than one
+ // socket package, keep partial header info. in headerArray.
+ let tmpArray = new Uint8Array(headerArray.buffer);
+ for (let i = 0; i < data.length; i++) {
+ tmpArray[currentHeaderLength + i] = data[i];
+ }
+ currentHeaderLength += data.length;
+ return true;
+ };
+
+ // The last remaining package data contains 8 bytes,
+ // they are "DONE(0x454e4f44)" and 0x0000.
+ let checkDone = function(data) {
+ if (data.length != 8) {
+ return false;
+ }
+
+ let doneFlagArray = new Uint32Array(1);
+ let tmpArray = new Uint8Array(doneFlagArray.buffer);
+ for (let i = 0; i < 4; i++) {
+ tmpArray[i] = data[i];
+ }
+ // Check DONE flag
+ if (doneFlagArray[0] == DONE) {
+ return true;
+ }
+ return false;
+ };
+
+ let runFSM = function runFSM(aData) {
+ console.log("runFSM " + state);
+ let req;
+ switch (state) {
+ case "start":
+ state = "send-transport";
+ runFSM();
+ break;
+ case "send-transport":
+ req = client.createRequest("host:transport-any");
+ socket.send(req);
+ state = "wait-transport";
+ break;
+ case "wait-transport":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ console.log("transport: OK");
+ state = "send-sync";
+ runFSM();
+ break;
+ case "send-sync":
+ req = client.createRequest("sync:");
+ socket.send(req);
+ state = "wait-sync";
+ break;
+ case "wait-sync":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ console.log("sync: OK");
+ state = "send-recv";
+ runFSM();
+ break;
+ case "send-recv":
+ infoLengthPacket = new Uint32Array(1);
+ infoLengthPacket[0] = aFrom.length;
+ socket.send(encoder.encode("RECV"));
+ socket.send(infoLengthPacket);
+ socket.send(encoder.encode(aFrom));
+
+ state = "wait-recv";
+ break;
+ case "wait-recv":
+ // After sending "RECV" command, adb server will send chunks data back,
+ // Handle every single socket package here.
+ // Note: One socket package maybe contain many chunks, and often
+ // partial chunk at the end.
+ pkgData = new Uint8Array(client.getBuffer(aData));
+
+ // Handle all data in a single socket package.
+ while (pkgData.length > 0) {
+ if (chunkSize == 0 && checkDone(pkgData)) {
+ OS.File.writeAtomic(aDest, fileData, {}).then(
+ function onSuccess(number) {
+ console.log(number);
+ deferred.resolve("SUCCESS");
+ },
+ function onFailure(reason) {
+ console.log(reason);
+ deferred.reject("CANT_ACCESS_FILE");
+ }
+ );
+
+ state = "send-quit";
+ runFSM();
+ return;
+ }
+ if (chunkSize == 0 && !checkChunkDataHeader(pkgData)) {
+ shutdown();
+ return;
+ }
+ // handle full chunk
+ if (chunkSize > 0 && pkgData.length >= chunkSize) {
+ let chunkData = pkgData.subarray(0, chunkSize);
+ let tmpData = new Uint8Array(currentPos + chunkSize);
+ if (fileData) {
+ tmpData.set(fileData, 0);
+ }
+ tmpData.set(chunkData, currentPos);
+ fileData = tmpData;
+ pkgData = pkgData.subarray(chunkSize, pkgData.length);
+ currentPos += chunkSize;
+ chunkSize = 0;
+ }
+ // handle partial chunk at the end of socket package
+ if (chunkSize > 0 && pkgData.length > 0 && pkgData.length < chunkSize) {
+ let tmpData = new Uint8Array(currentPos + pkgData.length);
+ if (fileData) {
+ tmpData.set(fileData, 0);
+ }
+ tmpData.set(pkgData, currentPos);
+ fileData = tmpData;
+ currentPos += pkgData.length;
+ chunkSize -= pkgData.length;
+ break; // Break while loop.
+ }
+ }
+
+ break;
+ case "send-quit":
+ infoLengthPacket = new Uint32Array(1);
+ infoLengthPacket[0] = 0;
+ socket.send(encoder.encode("QUIT"));
+ socket.send(infoLengthPacket);
+
+ state = "end";
+ runFSM();
+ break;
+ case "end":
+ socket.close();
+ break;
+ default:
+ console.log("pull Unexpected State: " + state);
+ deferred.reject("UNEXPECTED_STATE");
+ }
+ };
+
+ let setupSocket = function() {
+ socket.s.onerror = function(aEvent) {
+ console.log("pull onerror");
+ deferred.reject("SOCKET_ERROR");
+ };
+
+ socket.s.onopen = function(aEvent) {
+ console.log("pull onopen");
+ state = "start";
+ runFSM();
+ };
+
+ socket.s.onclose = function(aEvent) {
+ console.log("pull onclose");
+ };
+
+ socket.s.ondata = function(aEvent) {
+ console.log("pull ondata:");
+ runFSM(aEvent.data);
+ };
+ };
+
+ socket = client.connect();
+ setupSocket();
+
+ return deferred.promise;
+ },
+
+ // pushes a file to the device.
+ // aFrom and aDest are full paths.
+ // XXX we should STAT the remote path before sending.
+ push: function adb_push(aFrom, aDest) {
+ let deferred = PromiseUtils.defer();
+ let socket;
+ let state;
+ let fileSize;
+ let fileData;
+ let remaining;
+ let currentPos = 0;
+ let fileTime;
+
+ console.log("pushing " + aFrom + " -> " + aDest);
+
+ let shutdown = function() {
+ console.log("push shutdown");
+ socket.close();
+ deferred.reject("BAD_RESPONSE");
+ };
+
+ let runFSM = function runFSM(aData) {
+ console.log("runFSM " + state);
+ let req;
+ switch (state) {
+ case "start":
+ state = "send-transport";
+ runFSM();
+ break;
+ case "send-transport":
+ req = client.createRequest("host:transport-any");
+ socket.send(req);
+ state = "wait-transport";
+ break;
+ case "wait-transport":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ console.log("transport: OK");
+ state = "send-sync";
+ runFSM();
+ break;
+ case "send-sync":
+ req = client.createRequest("sync:");
+ socket.send(req);
+ state = "wait-sync";
+ break;
+ case "wait-sync":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ console.log("sync: OK");
+ state = "send-send";
+ runFSM();
+ break;
+ case "send-send":
+ // need to send SEND + length($aDest,$fileMode)
+ // $fileMode is not the octal one there.
+ let encoder = new TextEncoder();
+
+ let infoLengthPacket = new Uint32Array(1), info = aDest + ",33204";
+ infoLengthPacket[0] = info.length;
+ socket.send(encoder.encode("SEND"));
+ socket.send(infoLengthPacket);
+ socket.send(encoder.encode(info));
+
+ // now sending file data.
+ while (remaining > 0) {
+ let toSend = remaining > 65536 ? 65536 : remaining;
+ console.log("Sending " + toSend + " bytes");
+
+ let dataLengthPacket = new Uint32Array(1);
+ // We have to create a new ArrayBuffer for the fileData slice
+ // because nsIDOMTCPSocket (or ArrayBufferInputStream) chokes on
+ // reused buffers, even when we don't modify their contents.
+ let dataPacket = new Uint8Array(new ArrayBuffer(toSend));
+ dataPacket.set(new Uint8Array(fileData.buffer, currentPos, toSend));
+ dataLengthPacket[0] = toSend;
+ socket.send(encoder.encode("DATA"));
+ socket.send(dataLengthPacket);
+ socket.send(dataPacket);
+
+ currentPos += toSend;
+ remaining -= toSend;
+ }
+
+ // Ending up with DONE + mtime (wtf???)
+ let fileTimePacket = new Uint32Array(1);
+ fileTimePacket[0] = fileTime;
+ socket.send(encoder.encode("DONE"));
+ socket.send(fileTimePacket);
+
+ state = "wait-done";
+ break;
+ case "wait-done":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ console.log("DONE: OK");
+ state = "end";
+ runFSM();
+ break;
+ case "end":
+ socket.close();
+ deferred.resolve("SUCCESS");
+ break;
+ default:
+ console.log("push Unexpected State: " + state);
+ deferred.reject("UNEXPECTED_STATE");
+ }
+ };
+
+ let setupSocket = function() {
+ socket.s.onerror = function(aEvent) {
+ console.log("push onerror");
+ deferred.reject("SOCKET_ERROR");
+ };
+
+ socket.s.onopen = function(aEvent) {
+ console.log("push onopen");
+ state = "start";
+ runFSM();
+ };
+
+ socket.s.onclose = function(aEvent) {
+ console.log("push onclose");
+ };
+
+ socket.s.ondata = function(aEvent) {
+ console.log("push ondata");
+ runFSM(aEvent.data);
+ };
+ };
+ // Stat the file, get its size.
+ OS.File.stat(aFrom).then(
+ function onSuccess(stat) {
+ if (stat.isDir) {
+ // The path represents a directory
+ deferred.reject("CANT_PUSH_DIR");
+ } else {
+ // The path represents a file, not a directory
+ fileSize = stat.size;
+ // We want seconds since epoch
+ fileTime = stat.lastModificationDate.getTime() / 1000;
+ remaining = fileSize;
+ console.log(aFrom + " size is " + fileSize);
+ let readPromise = OS.File.read(aFrom);
+ readPromise.then(
+ function readSuccess(aData) {
+ fileData = aData;
+ socket = client.connect();
+ setupSocket();
+ },
+ function readError() {
+ deferred.reject("READ_FAILED");
+ }
+ );
+ }
+ },
+ function onFailure(reason) {
+ console.log(reason);
+ deferred.reject("CANT_ACCESS_FILE");
+ }
+ );
+
+ return deferred.promise;
+ },
+
+ // Run a shell command
+ shell: function adb_shell(aCommand) {
+ let deferred = PromiseUtils.defer();
+ let socket;
+ let state;
+ let stdout = "";
+
+ console.log("shell " + aCommand);
+
+ let shutdown = function() {
+ console.log("shell shutdown");
+ socket.close();
+ deferred.reject("BAD_RESPONSE");
+ };
+
+ let runFSM = function runFSM(aData) {
+ console.log("runFSM " + state);
+ let req;
+ let ignoreResponseCode = false;
+ switch (state) {
+ case "start":
+ state = "send-transport";
+ runFSM();
+ break;
+ case "send-transport":
+ req = client.createRequest("host:transport-any");
+ socket.send(req);
+ state = "wait-transport";
+ break;
+ case "wait-transport":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ state = "send-shell";
+ runFSM();
+ break;
+ case "send-shell":
+ req = client.createRequest("shell:" + aCommand);
+ socket.send(req);
+ state = "rec-shell";
+ break;
+ case "rec-shell":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ state = "decode-shell";
+ if (client.getBuffer(aData).byteLength == 4) {
+ break;
+ }
+ ignoreResponseCode = true;
+ case "decode-shell":
+ let decoder = new TextDecoder();
+ let text = new Uint8Array(client.getBuffer(aData), ignoreResponseCode ? 4 : 0);
+ stdout += decoder.decode(text);
+ break;
+ default:
+ console.log("shell Unexpected State: " + state);
+ deferred.reject("UNEXPECTED_STATE");
+ }
+ };
+
+ socket = client.connect();
+ socket.s.onerror = function(aEvent) {
+ console.log("shell onerror");
+ deferred.reject("SOCKET_ERROR");
+ };
+
+ socket.s.onopen = function(aEvent) {
+ console.log("shell onopen");
+ state = "start";
+ runFSM();
+ };
+
+ socket.s.onclose = function(aEvent) {
+ deferred.resolve(stdout);
+ console.log("shell onclose");
+ };
+
+ socket.s.ondata = function(aEvent) {
+ console.log("shell ondata");
+ runFSM(aEvent.data);
+ };
+
+ return deferred.promise;
+ },
+
+ reboot: function adb_reboot() {
+ return this.shell("reboot");
+ },
+
+ rebootRecovery: function adb_rebootRecovery() {
+ return this.shell("reboot recovery");
+ },
+
+ rebootBootloader: function adb_rebootBootloader() {
+ return this.shell("reboot bootloader");
+ },
+
+ root: function adb_root() {
+ let deferred = PromiseUtils.defer();
+ let socket;
+ let state;
+
+ console.log("root");
+
+ let shutdown = function() {
+ console.log("root shutdown");
+ socket.close();
+ deferred.reject("BAD_RESPONSE");
+ };
+
+ let runFSM = function runFSM(aData) {
+ console.log("runFSM " + state);
+ let req;
+ switch (state) {
+ case "start":
+ state = "send-transport";
+ runFSM();
+ break;
+ case "send-transport":
+ req = client.createRequest("host:transport-any");
+ socket.send(req);
+ state = "wait-transport";
+ break;
+ case "wait-transport":
+ if (!client.checkResponse(aData, OKAY)) {
+ shutdown();
+ return;
+ }
+ state = "send-root";
+ runFSM();
+ break;
+ case "send-root":
+ req = client.createRequest("root:");
+ socket.send(req);
+ state = "rec-root";
+ break;
+ case "rec-root":
+ // Nothing to do
+ break;
+ default:
+ console.log("root Unexpected State: " + state);
+ deferred.reject("UNEXPECTED_STATE");
+ }
+ };
+
+ socket = client.connect();
+ socket.s.onerror = function(aEvent) {
+ console.log("root onerror");
+ deferred.reject("SOCKET_ERROR");
+ };
+
+ socket.s.onopen = function(aEvent) {
+ console.log("root onopen");
+ state = "start";
+ runFSM();
+ };
+
+ socket.s.onclose = function(aEvent) {
+ deferred.resolve();
+ console.log("root onclose");
+ };
+
+ socket.s.ondata = function(aEvent) {
+ console.log("root ondata");
+ runFSM(aEvent.data);
+ };
+
+ return deferred.promise;
+ },
+
+ // Asynchronously runs an adb command.
+ // @param aCommand The command as documented in
+ // http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
+ runCommand: function adb_runCommand(aCommand) {
+ console.log("runCommand " + aCommand);
+ let deferred = PromiseUtils.defer();
+ if (!this.ready) {
+ setTimeout(function() { deferred.reject("ADB_NOT_READY"); });
+ return deferred.promise;
+ }
+
+ let socket = client.connect();
+
+ socket.s.onopen = function() {
+ console.log("runCommand onopen");
+ let req = client.createRequest(aCommand);
+ socket.send(req);
+
+ };
+
+ socket.s.onerror = function() {
+ console.log("runCommand onerror");
+ deferred.reject("NETWORK_ERROR");
+ };
+
+ socket.s.onclose = function() {
+ console.log("runCommand onclose");
+ };
+
+ socket.s.ondata = function(aEvent) {
+ console.log("runCommand ondata");
+ let data = aEvent.data;
+
+ let packet = client.unpackPacket(data, false);
+ if (!client.checkResponse(data, OKAY)) {
+ socket.close();
+ console.log("Error: " + packet.data);
+ deferred.reject("PROTOCOL_ERROR");
+ return;
+ }
+
+ deferred.resolve(packet.data);
+ };
+
+
+ return deferred.promise;
+ }
+};
+
+exports.ADB = ADB;
--- a/devtools/shared/adb/moz.build
+++ b/devtools/shared/adb/moz.build
@@ -1,12 +1,16 @@
# 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/.
DevToolsModules(
'adb-binary.js',
+ 'adb-client.js',
+ 'adb-running-checker.js',
+ 'adb-socket.js',
+ 'adb.js',
)
with Files('**'):
BUG_COMPONENT = ('DevTools', 'about:debugging')
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/shared/adb/test/adb.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# 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/.
+
+"""
+A fake ADB binary
+"""
+
+from __future__ import absolute_import
+
+import os
+import socket
+import SocketServer
+import sys
+import thread
+
+HOST = '127.0.0.1'
+PORT = 5037
+
+class ADBServer(SocketServer.BaseRequestHandler):
+ def sendData(self, data):
+ self.request.send('OKAY')
+ self.request.send('%04x' % len(data))
+ self.request.send(data)
+
+ def handle(self):
+ while True:
+ data = self.request.recv(4096)
+ if 'kill-server' in data:
+ def shutdown(server):
+ server.shutdown()
+ thread.exit()
+ thread.start_new_thread(shutdown, (server, ))
+ self.request.close()
+ break
+ elif 'host:version' in data:
+ self.sendData('001F')
+ self.request.close()
+ break
+ elif 'host:track-devices' in data:
+ self.sendData('1234567890\tdevice')
+ break
+
+if len(sys.argv) == 2:
+ if sys.argv[1] == 'start-server':
+ # daemonize
+ if os.fork() > 0:
+ sys.exit(0)
+ os.setsid()
+ if os.fork() > 0:
+ sys.exit(0)
+
+ # Create a SocketServer with 'False' for bind_and_activate to set
+ # allow_reuse_address before binding.
+ server = SocketServer.TCPServer((HOST, PORT), ADBServer, False)
+ server.allow_reuse_address = True
+ server.server_bind()
+ server.server_activate()
+ server.serve_forever()
+ elif sys.argv[1] == 'kill-server':
+ sock = socket.socket()
+ sock.connect((HOST, PORT))
+ sock.send('kill-server')
+ sock.shutdown(socket.SHUT_RDWR)
+ sock.close()
--- a/devtools/shared/adb/test/test_adb.js
+++ b/devtools/shared/adb/test/test_adb.js
@@ -1,12 +1,16 @@
"use strict";
+const EventEmitter = require("devtools/shared/event-emitter");
const { ExtensionTestUtils } = ChromeUtils.import("resource://testing-common/ExtensionXPCShellUtils.jsm", {});
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
const { getFileForBinary } = require("devtools/shared/adb/adb-binary");
+const { check } = require("devtools/shared/adb/adb-running-checker");
+const { ADB } = require("devtools/shared/adb/adb");
const ADB_JSON = {
"Linux": {
"x86": [
"linux/adb"
],
"x86_64": [
"linux64/adb"
@@ -28,21 +32,41 @@ const ADB_JSON = {
"win32/AdbWinApi.dll",
"win32/AdbWinUsbApi.dll"
]
}
};
ExtensionTestUtils.init(this);
+function readAdbMockContent() {
+ const adbMockFile = do_get_file("adb.py", false);
+ const s = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ s.init(adbMockFile, -1, -1, false);
+ try {
+ return NetUtil.readInputStreamToString(s, s.available());
+ } finally {
+ s.close();
+ }
+}
+
+const adbMock = readAdbMockContent();
+
add_task(async function setup() {
// Prepare the profile directory where the adb extension will be installed.
do_get_profile();
});
+add_task(async function testAdbIsNotRunningInitially() {
+ const isAdbRunning = await check();
+ // Assume that no adb server running.
+ ok(!isAdbRunning, "adb is not running initially");
+});
+
add_task(async function testNoAdbExtension() {
const extension = ExtensionTestUtils.loadExtension({
manifest: {
version: "1.0",
applications: {
gecko: { id: "not-adb@mozilla.org" }
}
},
@@ -56,16 +80,18 @@ add_task(async function testNoAdbExtensi
await extension.unload();
});
add_task(async function testNoAdbJSON() {
const extension = ExtensionTestUtils.loadExtension({
manifest: {
version: "1.0",
applications: {
+ // The extension id here and in later test cases should match the
+ // corresponding prefrece value.
gecko: { id: "adb@mozilla.org" }
}
},
});
await extension.startup();
const adbBinary = await getFileForBinary();
@@ -117,8 +143,40 @@ add_task(async function testExtract() {
await extension.startup();
const adbBinary = await getFileForBinary();
ok(await adbBinary.exists);
await extension.unload();
});
+add_task(async function testStartAndStop() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ version: "1.0",
+ applications: {
+ gecko: { id: "adb@mozilla.org" }
+ }
+ },
+ files: {
+ "adb.json": JSON.stringify(ADB_JSON),
+ "linux/adb": adbMock,
+ "linux64/adb": adbMock,
+ "mac64/adb": adbMock,
+ "win32/adb.exe": adbMock,
+ "win32/AdbWinApi.dll": "dummy",
+ "win32/AdbWinUsbApi.dll": "dummy"
+ },
+ });
+
+ await extension.startup();
+
+ await ADB.start();
+ ok(ADB.ready);
+
+ ok(await check(), "adb is now running");
+
+ await ADB.stop(true /* sync */);
+ ok(!ADB.ready);
+
+ await extension.unload();
+});
+
--- a/devtools/shared/adb/test/xpcshell.ini
+++ b/devtools/shared/adb/test/xpcshell.ini
@@ -1,8 +1,10 @@
[DEFAULT]
tags = devtools
head = xpcshell-head.js
firefox-appdir = browser
skip-if = toolkit == 'android'
+support-files =
+ adb.py
[test_adb.js]
run-sequentially = An extension having the same id is installed/uninstalled in different tests