Bug 1469054 - Adapt implementations for adb.start() and adb.stop() and relevant stuff. r=jdescottes
authorHiroyuki Ikezoe <hikezoe@mozilla.com>
Thu, 09 Aug 2018 14:27:55 +0900
changeset 430753 5bbec1fa2d0781b95234b619dc2aedbb6686035f
parent 430752 8b57faf0b3d1df90207232a39731e4be3a850125
child 430754 8e296137f5bf6416fd18dab3f340e6a13560ff18
push id34411
push usernerli@mozilla.com
push dateThu, 09 Aug 2018 21:30:25 +0000
treeherdermozilla-central@5ad7b28b1009 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1469054
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1469054 - Adapt implementations for adb.start() and adb.stop() and relevant stuff. r=jdescottes MozReview-Commit-ID: LF23OQtXxkj
devtools/shared/adb/adb-client.js
devtools/shared/adb/adb-running-checker.js
devtools/shared/adb/adb-socket.js
devtools/shared/adb/adb.js
devtools/shared/adb/moz.build
devtools/shared/adb/test/adb.py
devtools/shared/adb/test/test_adb.js
devtools/shared/adb/test/xpcshell.ini
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