Bug 1469054 - Adapt implementations for adb.start() and adb.stop() and relevant stuff. r?jdescottes draft
authorHiroyuki Ikezoe <hikezoe@mozilla.com>
Thu, 09 Aug 2018 14:27:55 +0900
changeset 827774 17955c7c342140cef7b0bfe1b570664fd58a03f6
parent 827773 2c10e2ce3468ade4257b1159b55514f1e7e84c9c
child 827775 91699dcd898d6618bb4c7deb59bec9e7820043f9
push id118583
push userhikezoe@mozilla.com
push dateThu, 09 Aug 2018 06:03:03 +0000
reviewersjdescottes
bugs1469054
milestone63.0a1
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