summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/remote-debugging
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/remote-debugging')
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-addon.js186
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-binary.js244
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-client.js82
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-device.js53
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-process.js155
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-running-checker.js91
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-runtime.js129
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb-socket.js72
-rw-r--r--devtools/client/shared/remote-debugging/adb/adb.js176
-rw-r--r--devtools/client/shared/remote-debugging/adb/commands/index.js29
-rw-r--r--devtools/client/shared/remote-debugging/adb/commands/list-devices.js29
-rw-r--r--devtools/client/shared/remote-debugging/adb/commands/moz.build12
-rw-r--r--devtools/client/shared/remote-debugging/adb/commands/prepare-tcp-connection.js46
-rw-r--r--devtools/client/shared/remote-debugging/adb/commands/run-command.js66
-rw-r--r--devtools/client/shared/remote-debugging/adb/commands/shell.js107
-rw-r--r--devtools/client/shared/remote-debugging/adb/commands/track-devices.js163
-rw-r--r--devtools/client/shared/remote-debugging/adb/moz.build24
-rw-r--r--devtools/client/shared/remote-debugging/adb/xpcshell/.eslintrc.js9
-rw-r--r--devtools/client/shared/remote-debugging/adb/xpcshell/adb.py72
-rw-r--r--devtools/client/shared/remote-debugging/adb/xpcshell/test_adb.js245
-rw-r--r--devtools/client/shared/remote-debugging/adb/xpcshell/test_prepare-tcp-connection.js78
-rw-r--r--devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell-head.js10
-rw-r--r--devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell.ini11
-rw-r--r--devtools/client/shared/remote-debugging/constants.js24
-rw-r--r--devtools/client/shared/remote-debugging/moz.build20
-rw-r--r--devtools/client/shared/remote-debugging/remote-client-manager.js146
-rw-r--r--devtools/client/shared/remote-debugging/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/shared/remote-debugging/test/xpcshell/test_remote_client_manager.js153
-rw-r--r--devtools/client/shared/remote-debugging/test/xpcshell/test_version_checker.js159
-rw-r--r--devtools/client/shared/remote-debugging/test/xpcshell/xpcshell-head.js10
-rw-r--r--devtools/client/shared/remote-debugging/test/xpcshell/xpcshell.ini8
-rw-r--r--devtools/client/shared/remote-debugging/version-checker.js154
32 files changed, 2769 insertions, 0 deletions
diff --git a/devtools/client/shared/remote-debugging/adb/adb-addon.js b/devtools/client/shared/remote-debugging/adb/adb-addon.js
new file mode 100644
index 0000000000..ddce411cb3
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-addon.js
@@ -0,0 +1,186 @@
+/* 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 { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs",
+ // AddonManager is a singleton, never create two instances of it.
+ { loadInDevToolsLoader: false }
+);
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const PREF_ADB_EXTENSION_URL = "devtools.remote.adb.extensionURL";
+const PREF_ADB_EXTENSION_ID = "devtools.remote.adb.extensionID";
+
+const ADB_ADDON_STATES = {
+ DOWNLOADING: "downloading",
+ INSTALLED: "installed",
+ INSTALLING: "installing",
+ PREPARING: "preparing",
+ UNINSTALLED: "uninstalled",
+ UNKNOWN: "unknown",
+};
+exports.ADB_ADDON_STATES = ADB_ADDON_STATES;
+
+/**
+ * Wrapper around the ADB Extension providing ADB binaries for devtools remote debugging.
+ * Fires the following events:
+ * - "update": the status of the addon was updated
+ * - "failure": addon installation failed
+ * - "progress": addon download in progress
+ *
+ * AdbAddon::state can take any of the values from ADB_ADDON_STATES.
+ */
+class ADBAddon extends EventEmitter {
+ constructor() {
+ super();
+
+ this._status = ADB_ADDON_STATES.UNKNOWN;
+
+ const addonsListener = {};
+ addonsListener.onEnabled =
+ addonsListener.onDisabled =
+ addonsListener.onInstalled =
+ addonsListener.onUninstalled =
+ () => this.updateInstallStatus();
+ AddonManager.addAddonListener(addonsListener);
+
+ this.updateInstallStatus();
+ }
+
+ set status(value) {
+ if (this._status != value) {
+ this._status = value;
+ this.emit("update");
+ }
+ }
+
+ get status() {
+ return this._status;
+ }
+
+ async _getAddon() {
+ const addonId = Services.prefs.getCharPref(PREF_ADB_EXTENSION_ID);
+ return AddonManager.getAddonByID(addonId);
+ }
+
+ async updateInstallStatus() {
+ const addon = await this._getAddon();
+ if (addon && !addon.userDisabled) {
+ this.status = ADB_ADDON_STATES.INSTALLED;
+ } else {
+ this.status = ADB_ADDON_STATES.UNINSTALLED;
+ }
+ }
+
+ /**
+ * Returns the platform specific download link for the ADB extension.
+ */
+ _getXpiLink() {
+ const platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+ let OS = "";
+ if (platform.includes("Win")) {
+ OS = "win32";
+ } else if (platform.includes("Mac")) {
+ OS = "mac64";
+ } else if (platform.includes("Linux")) {
+ if (platform.includes("x86_64")) {
+ OS = "linux64";
+ } else {
+ OS = "linux";
+ }
+ }
+
+ const xpiLink = Services.prefs.getCharPref(PREF_ADB_EXTENSION_URL);
+ return xpiLink.replace(/#OS#/g, OS);
+ }
+
+ /**
+ * Install and enable the adb extension. Returns a promise that resolves when ADB is
+ * enabled.
+ *
+ * @param {String} source
+ * String passed to the AddonManager for telemetry.
+ */
+ async install(source) {
+ if (!source) {
+ throw new Error(
+ "Missing mandatory `source` parameter for adb-addon.install"
+ );
+ }
+
+ const addon = await this._getAddon();
+ if (addon && !addon.userDisabled) {
+ this.status = ADB_ADDON_STATES.INSTALLED;
+ return;
+ }
+ this.status = ADB_ADDON_STATES.PREPARING;
+ if (addon?.userDisabled) {
+ await addon.enable();
+ } else {
+ const install = await AddonManager.getInstallForURL(this._getXpiLink(), {
+ telemetryInfo: { source },
+ });
+ install.addListener(this);
+ install.install();
+ }
+ }
+
+ async uninstall() {
+ const addon = await this._getAddon();
+ addon.uninstall();
+ }
+
+ installFailureHandler(install, message) {
+ this.status = ADB_ADDON_STATES.UNINSTALLED;
+ this.emit("failure", message);
+ }
+
+ // Expected AddonManager install listener.
+ onDownloadStarted() {
+ this.status = ADB_ADDON_STATES.DOWNLOADING;
+ }
+
+ // Expected AddonManager install listener.
+ onDownloadProgress(install) {
+ if (install.maxProgress == -1) {
+ this.emit("progress", -1);
+ } else {
+ this.emit("progress", install.progress / install.maxProgress);
+ }
+ }
+
+ // Expected AddonManager install listener.
+ onDownloadCancelled(install) {
+ this.installFailureHandler(install, "Download cancelled");
+ }
+
+ // Expected AddonManager install listener.
+ onDownloadFailed(install) {
+ this.installFailureHandler(install, "Download failed");
+ }
+
+ // Expected AddonManager install listener.
+ onInstallStarted() {
+ this.status = ADB_ADDON_STATES.INSTALLING;
+ }
+
+ // Expected AddonManager install listener.
+ onInstallCancelled(install) {
+ this.installFailureHandler(install, "Install cancelled");
+ }
+
+ // Expected AddonManager install listener.
+ onInstallFailed(install) {
+ this.installFailureHandler(install, "Install failed");
+ }
+
+ // Expected AddonManager install listener.
+ onInstallEnded({ addon }) {
+ addon.enable();
+ }
+}
+
+exports.adbAddon = new ADBAddon();
diff --git a/devtools/client/shared/remote-debugging/adb/adb-binary.js b/devtools/client/shared/remote-debugging/adb/adb-binary.js
new file mode 100644
index 0000000000..4b73dbbee7
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-binary.js
@@ -0,0 +1,244 @@
+/* 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 { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "NetUtil",
+ "resource://gre/modules/NetUtil.jsm"
+);
+loader.lazyGetter(this, "UNPACKED_ROOT_PATH", () => {
+ return PathUtils.join(PathUtils.localProfileDir, "adb");
+});
+loader.lazyGetter(this, "EXTENSION_ID", () => {
+ return Services.prefs.getCharPref("devtools.remote.adb.extensionID");
+});
+loader.lazyGetter(this, "ADB_BINARY_PATH", () => {
+ let adbBinaryPath = PathUtils.join(UNPACKED_ROOT_PATH, "adb");
+ if (Services.appinfo.OS === "WINNT") {
+ adbBinaryPath += ".exe";
+ }
+ return adbBinaryPath;
+});
+
+const MANIFEST = "manifest.json";
+
+/**
+ * Read contents from a given uri in the devtools-adb-extension and parse the
+ * contents as JSON.
+ */
+async function readFromExtension(fileUri) {
+ return new Promise(resolve => {
+ lazy.NetUtil.asyncFetch(
+ {
+ uri: fileUri,
+ loadUsingSystemPrincipal: true,
+ },
+ input => {
+ try {
+ const string = lazy.NetUtil.readInputStreamToString(
+ input,
+ input.available()
+ );
+ resolve(JSON.parse(string));
+ } catch (e) {
+ dumpn(`Could not read ${fileUri} in the extension: ${e}`);
+ resolve(null);
+ }
+ }
+ );
+ });
+}
+
+/**
+ * Unpack file from the extension.
+ * Uses NetUtil to read and write, since it's required for reading.
+ *
+ * @param {string} file
+ * The path name of the file in the extension.
+ */
+async function unpackFile(file) {
+ const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(EXTENSION_ID);
+ if (!policy) {
+ return;
+ }
+
+ // Assumes that destination dir already exists.
+ const basePath = file.substring(file.lastIndexOf("/") + 1);
+ const filePath = PathUtils.join(UNPACKED_ROOT_PATH, basePath);
+ await new Promise((resolve, reject) => {
+ lazy.NetUtil.asyncFetch(
+ {
+ uri: policy.getURL(file),
+ loadUsingSystemPrincipal: true,
+ },
+ input => {
+ try {
+ // Since we have to use NetUtil to read, probably it's okay to use for
+ // writing, rather than bouncing to IOUtils...?
+ const outputFile = new lazy.FileUtils.File(filePath);
+ const output = lazy.FileUtils.openAtomicFileOutputStream(outputFile);
+ lazy.NetUtil.asyncCopy(input, output, resolve);
+ } catch (e) {
+ dumpn(`Could not unpack file ${file} in the extension: ${e}`);
+ reject(e);
+ }
+ }
+ );
+ });
+ // Mark binaries as executable.
+ await IOUtils.setPermissions(filePath, 0o744);
+}
+
+/**
+ * Extract files in the extension into local profile directory and returns
+ * if it fails.
+ */
+async function extractFiles() {
+ const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(EXTENSION_ID);
+ if (!policy) {
+ return false;
+ }
+ const uri = policy.getURL("adb.json");
+ const adbInfo = await readFromExtension(uri);
+ if (!adbInfo) {
+ return false;
+ }
+
+ let filesForAdb;
+ try {
+ // The adbInfo is an object looks like this;
+ //
+ // {
+ // "Linux": {
+ // "x86": [
+ // "linux/adb"
+ // ],
+ // "x86_64": [
+ // "linux64/adb"
+ // ]
+ // },
+ // ...
+
+ // XPCOMABI looks this; x86_64-gcc3, so drop the compiler name.
+ let architecture = Services.appinfo.XPCOMABI.split("-")[0];
+ if (architecture === "aarch64") {
+ // Fallback on x86 or x86_64 binaries for aarch64 - See Bug 1522149
+ const hasX86Binary = !!adbInfo[Services.appinfo.OS].x86;
+ architecture = hasX86Binary ? "x86" : "x86_64";
+ }
+ filesForAdb = adbInfo[Services.appinfo.OS][architecture];
+ } catch (e) {
+ return false;
+ }
+
+ // manifest.json isn't in adb.json but has to be unpacked for version
+ // comparison
+ filesForAdb.push(MANIFEST);
+
+ await IOUtils.makeDirectory(UNPACKED_ROOT_PATH);
+
+ for (const file of filesForAdb) {
+ try {
+ await unpackFile(file);
+ } catch (e) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Read the manifest from inside the devtools-adb-extension.
+ * Uses NetUtil since data is packed inside the extension, not a local file.
+ */
+async function getManifestFromExtension() {
+ const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(EXTENSION_ID);
+ if (!policy) {
+ return null;
+ }
+
+ const manifestUri = policy.getURL(MANIFEST);
+ return readFromExtension(manifestUri);
+}
+
+/**
+ * Returns whether manifest.json has already been unpacked.
+ */
+async function isManifestUnpacked() {
+ const manifestPath = PathUtils.join(UNPACKED_ROOT_PATH, MANIFEST);
+ return IOUtils.exists(manifestPath);
+}
+
+/**
+ * Read the manifest from the unpacked binary directory.
+ * Uses IOUtils since this is a local file.
+ */
+async function getManifestFromUnpacked() {
+ if (!(await isManifestUnpacked())) {
+ throw new Error("Manifest doesn't exist at unpacked path");
+ }
+
+ const manifestPath = PathUtils.join(UNPACKED_ROOT_PATH, MANIFEST);
+ const binary = await IOUtils.read(manifestPath);
+ const json = new TextDecoder().decode(binary);
+ let data;
+ try {
+ data = JSON.parse(json);
+ } catch (e) {}
+ return data;
+}
+
+/**
+ * Check state of binary unpacking, including the location and manifest.
+ */
+async function isUnpacked() {
+ if (!(await isManifestUnpacked())) {
+ dumpn("Needs unpacking, no manifest found");
+ return false;
+ }
+
+ const manifestInExtension = await getManifestFromExtension();
+ const unpackedManifest = await getManifestFromUnpacked();
+ if (manifestInExtension.version != unpackedManifest.version) {
+ dumpn(
+ `Needs unpacking, extension version ${manifestInExtension.version} != ` +
+ `unpacked version ${unpackedManifest.version}`
+ );
+ return false;
+ }
+ dumpn("Already unpacked");
+ return true;
+}
+
+/**
+ * Get a file object for the adb binary from the 'adb@mozilla.org' extension
+ * which has been already installed.
+ *
+ * @return {nsIFile}
+ * File object for the binary.
+ */
+async function getFileForBinary() {
+ if (!(await isUnpacked()) && !(await extractFiles())) {
+ return null;
+ }
+
+ const file = new lazy.FileUtils.File(ADB_BINARY_PATH);
+ if (!file.exists()) {
+ return null;
+ }
+ return file;
+}
+
+exports.getFileForBinary = getFileForBinary;
diff --git a/devtools/client/shared/remote-debugging/adb/adb-client.js b/devtools/client/shared/remote-debugging/adb/adb-client.js
new file mode 100644
index 0000000000..ff238d6392
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-client.js
@@ -0,0 +1,82 @@
+/* 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("resource://devtools/client/shared/remote-debugging/adb/adb-socket.js");
+const { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+
+const OKAY = 0x59414b4f;
+const FAIL = 0x4c494146;
+
+// Return buffer, which differs between Gecko versions
+function getBuffer(packet) {
+ return packet.buffer ? packet.buffer : packet;
+}
+
+// @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(packet, ignoreResponse) {
+ const buffer = getBuffer(packet);
+ dumpn("Len buffer: " + buffer.byteLength);
+ if (buffer.byteLength === 4 && !ignoreResponse) {
+ dumpn("Packet empty");
+ return { length: 0, data: "" };
+ }
+ const lengthView = new Uint8Array(buffer, ignoreResponse ? 0 : 4, 4);
+ const decoder = new TextDecoder();
+ const length = parseInt(decoder.decode(lengthView), 16);
+ const text = new Uint8Array(buffer, ignoreResponse ? 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(packet, expected = OKAY) {
+ const buffer = getBuffer(packet);
+ const view = new Uint32Array(buffer, 0, 1);
+ if (view[0] == FAIL) {
+ dumpn("Response: FAIL");
+ }
+ dumpn("view[0] = " + view[0]);
+ return view[0] == expected;
+}
+
+// @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(command) {
+ let length = command.length.toString(16).toUpperCase();
+ while (length.length < 4) {
+ length = "0" + length;
+ }
+
+ const encoder = new TextEncoder();
+ dumpn("Created request: " + length + command);
+ return encoder.encode(length + command);
+}
+
+function connect() {
+ return new AdbSocket();
+}
+
+const client = {
+ getBuffer,
+ unpackPacket,
+ checkResponse,
+ createRequest,
+ connect,
+};
+
+module.exports = client;
diff --git a/devtools/client/shared/remote-debugging/adb/adb-device.js b/devtools/client/shared/remote-debugging/adb/adb-device.js
new file mode 100644
index 0000000000..def22f27c3
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-device.js
@@ -0,0 +1,53 @@
+/* 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 {
+ shell,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/index.js");
+
+/**
+ * A Device instance is created and registered with the Devices module whenever
+ * ADB notices a new device is connected.
+ */
+class AdbDevice {
+ constructor(id) {
+ this.id = id;
+ }
+
+ async initialize() {
+ const model = await shell(this.id, "getprop ro.product.model");
+ this.model = model.trim();
+ }
+
+ get name() {
+ return this.model || this.id;
+ }
+
+ async getRuntimeSocketPaths() {
+ // A matching entry looks like:
+ // 00000000: 00000002 00000000 00010000 0001 01 6551588
+ // /data/data/org.mozilla.fennec/firefox-debugger-socket
+ const query = "cat /proc/net/unix";
+ const rawSocketInfo = await shell(this.id, query);
+
+ // Filter to lines with "firefox-debugger-socket"
+ let socketInfos = rawSocketInfo.split(/\r?\n/);
+ socketInfos = socketInfos.filter(l =>
+ l.includes("firefox-debugger-socket")
+ );
+
+ // It's possible to have multiple lines with the same path, so de-dupe them
+ const socketPaths = new Set();
+ for (const socketInfo of socketInfos) {
+ const socketPath = socketInfo.split(" ").pop();
+ socketPaths.add(socketPath);
+ }
+
+ return socketPaths;
+ }
+}
+
+module.exports = AdbDevice;
diff --git a/devtools/client/shared/remote-debugging/adb/adb-process.js b/devtools/client/shared/remote-debugging/adb/adb-process.js
new file mode 100644
index 0000000000..ade509125e
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-process.js
@@ -0,0 +1,155 @@
+/* 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 { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const {
+ getFileForBinary,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-binary.js");
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+loader.lazyRequireGetter(
+ this,
+ "runCommand",
+ "resource://devtools/client/shared/remote-debugging/adb/commands/index.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "check",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-running-checker.js",
+ true
+);
+
+// Waits until a predicate returns true or re-tries the predicate calls
+// |retry| times, we wait for 100ms between each calls.
+async function waitUntil(predicate, retry = 20) {
+ let count = 0;
+ while (count++ < retry) {
+ if (await predicate()) {
+ return true;
+ }
+ // Wait for 100 milliseconds.
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ // Timed out after trying too many times.
+ return false;
+}
+
+// Class representing the ADB process.
+class AdbProcess extends EventEmitter {
+ constructor() {
+ super();
+
+ this._ready = false;
+ this._didRunInitially = false;
+ }
+
+ get ready() {
+ return this._ready;
+ }
+
+ _getAdbFile() {
+ if (this._adbFilePromise) {
+ return this._adbFilePromise;
+ }
+ this._adbFilePromise = getFileForBinary();
+ return this._adbFilePromise;
+ }
+
+ async _runProcess(process, params) {
+ return new Promise((resolve, reject) => {
+ process.runAsync(
+ params,
+ params.length,
+ {
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "process-finished":
+ resolve();
+ break;
+ case "process-failed":
+ reject();
+ break;
+ }
+ },
+ },
+ false
+ );
+ });
+ }
+
+ // We startup by launching adb in server mode, and setting
+ // the tcp socket preference to |true|
+ async start() {
+ const onSuccessfulStart = () => {
+ this._ready = true;
+ this.emit("adb-ready");
+ };
+
+ const isAdbRunning = await check();
+ if (isAdbRunning) {
+ dumpn("Found ADB process running, not restarting");
+ onSuccessfulStart();
+ return;
+ }
+ dumpn("Didn't find ADB process running, restarting");
+
+ this._didRunInitially = true;
+ const process = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+
+ // FIXME: Bug 1481691 - We should avoid extracting files every time.
+ const adbFile = await this._getAdbFile();
+ process.init(adbFile);
+ // Hide command prompt window on Windows
+ process.startHidden = true;
+ process.noShell = true;
+ const params = ["start-server"];
+ let isStarted = false;
+ try {
+ await this._runProcess(process, params);
+ isStarted = await waitUntil(check);
+ } catch (e) {}
+
+ if (isStarted) {
+ onSuccessfulStart();
+ } else {
+ this._ready = false;
+ throw new Error("ADB Process didn't start");
+ }
+ }
+
+ /**
+ * Stop the ADB server, but only if we started it. If it was started before
+ * us, we return immediately.
+ */
+ async stop() {
+ if (!this._didRunInitially) {
+ return; // We didn't start the server, nothing to do
+ }
+ await this.kill();
+ }
+
+ /**
+ * Kill the ADB server.
+ */
+ async kill() {
+ try {
+ await runCommand("host:kill");
+ } catch (e) {
+ dumpn("Failed to send host:kill command");
+ }
+ dumpn("adb server was terminated by host:kill");
+ this._ready = false;
+ this._didRunInitially = false;
+ }
+}
+
+exports.adbProcess = new AdbProcess();
diff --git a/devtools/client/shared/remote-debugging/adb/adb-running-checker.js b/devtools/client/shared/remote-debugging/adb/adb-running-checker.js
new file mode 100644
index 0000000000..7f952ca39b
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-running-checker.js
@@ -0,0 +1,91 @@
+/* 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("resource://devtools/client/shared/remote-debugging/adb/adb-client.js");
+const { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+
+exports.check = async function check() {
+ let socket;
+ let state;
+ let timerID;
+ const TIMEOUT_TIME = 1000;
+
+ dumpn("Asking for host:version");
+
+ return new Promise(resolve => {
+ // On MacOSX connecting to a port which is not started listening gets
+ // stuck (bug 1481963), to avoid the stuck, we do forcibly fail the
+ // connection after |TIMEOUT_TIME| elapsed.
+ timerID = setTimeout(() => {
+ socket.close();
+ resolve(false);
+ }, TIMEOUT_TIME);
+
+ function finish(returnValue) {
+ clearTimeout(timerID);
+ resolve(returnValue);
+ }
+
+ const runFSM = function runFSM(packetData) {
+ dumpn("runFSM " + state);
+ switch (state) {
+ case "start":
+ const 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
+ const { length, data } = client.unpackPacket(packetData);
+ dumpn("length: ", length, "data: ", data);
+ socket.close();
+ const version = parseInt(data, 16);
+ if (version >= 31) {
+ finish(true);
+ } else {
+ dumpn("killing existing adb as we need version >= 31");
+ finish(false);
+ }
+ break;
+ default:
+ dumpn("Unexpected State: " + state);
+ finish(false);
+ }
+ };
+
+ const setupSocket = function () {
+ socket.s.onerror = function (event) {
+ dumpn("running checker onerror");
+ finish(false);
+ };
+
+ socket.s.onopen = function (event) {
+ dumpn("running checker onopen");
+ state = "start";
+ runFSM();
+ };
+
+ socket.s.onclose = function (event) {
+ dumpn("running checker onclose");
+ };
+
+ socket.s.ondata = function (event) {
+ dumpn("running checker ondata");
+ runFSM(event.data);
+ };
+ };
+
+ socket = client.connect();
+ setupSocket();
+ });
+};
diff --git a/devtools/client/shared/remote-debugging/adb/adb-runtime.js b/devtools/client/shared/remote-debugging/adb/adb-runtime.js
new file mode 100644
index 0000000000..77d39d643e
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-runtime.js
@@ -0,0 +1,129 @@
+/* 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 {
+ prepareTCPConnection,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/index.js");
+const {
+ shell,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/index.js");
+
+class AdbRuntime {
+ constructor(adbDevice, socketPath) {
+ this._adbDevice = adbDevice;
+ this._socketPath = socketPath;
+ // Set a default version name in case versionName cannot be parsed.
+ this._versionName = "";
+ }
+
+ async init() {
+ const packageName = this._packageName();
+ const query = `dumpsys package ${packageName} | grep versionName`;
+ const versionNameString = await shell(this._adbDevice.id, query);
+
+ // The versionName can have different formats depending on the channel
+ // - `versionName=Nightly 191016 06:01\n` on Nightly
+ // - `versionName=2.1.0\n` on Release
+ // We use a very flexible regular expression to accommodate for those
+ // different formats.
+ const matches = versionNameString.match(/versionName=(.*)\n/);
+ if (matches?.[1]) {
+ this._versionName = matches[1];
+ }
+ }
+
+ get id() {
+ return this._adbDevice.id + "|" + this._socketPath;
+ }
+
+ get isFenix() {
+ // Firefox Release uses "org.mozilla.firefox"
+ // Firefox Beta uses "org.mozilla.firefox_beta"
+ // Firefox Nightly uses "org.mozilla.fenix"
+ const isFirefox =
+ this._packageName().includes("org.mozilla.firefox") ||
+ this._packageName().includes("org.mozilla.fenix");
+
+ if (!isFirefox) {
+ return false;
+ }
+
+ // Firefox Release (based on Fenix) is not released in all regions yet, so
+ // we should still check for Fennec using the version number.
+ // Note that Fennec's versionName followed Firefox versions (eg "68.11.0").
+ // We can find the main version number in it. Fenix on the other hand has
+ // version names such as "Nightly 200730 06:21".
+ const mainVersion = Number(this.versionName.split(".")[0]);
+ const isFennec = mainVersion === 68;
+
+ // Application is Fenix if this is a Firefox application with a version
+ // different from the Fennec version.
+ return !isFennec;
+ }
+
+ get deviceId() {
+ return this._adbDevice.id;
+ }
+
+ get deviceName() {
+ return this._adbDevice.name;
+ }
+
+ get versionName() {
+ return this._versionName;
+ }
+
+ get shortName() {
+ const packageName = this._packageName();
+
+ switch (packageName) {
+ case "org.mozilla.firefox":
+ if (!this.isFenix) {
+ // Old Fennec release
+ return "Firefox (Fennec)";
+ }
+ // Official Firefox app, based on Fenix
+ return "Firefox";
+ case "org.mozilla.firefox_beta":
+ // Official Firefox Beta app, based on Fenix
+ return "Firefox Beta";
+ case "org.mozilla.fenix":
+ case "org.mozilla.fenix.nightly":
+ // Official Firefox Nightly app, based on Fenix
+ return "Firefox Nightly";
+ default:
+ // Unknown package name
+ return `Firefox (${packageName})`;
+ }
+ }
+
+ get socketPath() {
+ return this._socketPath;
+ }
+
+ get name() {
+ return `${this.shortName} on Android (${this.deviceName})`;
+ }
+
+ connect(connection) {
+ return prepareTCPConnection(this.deviceId, this._socketPath).then(port => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.connect();
+ });
+ }
+
+ _packageName() {
+ // If using abstract socket address, it is "@org.mozilla.firefox/..."
+ // If using path base socket, it is "/data/data/<package>...""
+ // Until Fennec 62 only supports path based UNIX domain socket, but
+ // Fennec 63+ supports both path based and abstract socket.
+ return this._socketPath.startsWith("@")
+ ? this._socketPath.substr(1).split("/")[0]
+ : this._socketPath.split("/")[3];
+ }
+}
+exports.AdbRuntime = AdbRuntime;
diff --git a/devtools/client/shared/remote-debugging/adb/adb-socket.js b/devtools/client/shared/remote-debugging/adb/adb-socket.js
new file mode 100644
index 0000000000..6b2a6d7a1c
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb-socket.js
@@ -0,0 +1,72 @@
+/* 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 { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+
+function createTCPSocket(location, port, options) {
+ const { TCPSocket } = Cu.getGlobalForObject(
+ ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs")
+ );
+
+ 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} inputArray
+ * the array to dump
+ */
+ _hexdump(inputArray) {
+ const decoder = new TextDecoder("windows-1252");
+ const array = new Uint8Array(inputArray.buffer);
+ const s = decoder.decode(array);
+ const len = array.length;
+ let dbg = "len=" + len + " ";
+ const 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++) {
+ const c = array[i];
+ if (c < 32 || c > 127) {
+ dbg += ".";
+ } else {
+ dbg += s[i];
+ }
+ }
+ dumpn(dbg);
+ }
+
+ // debugging version of tcpsocket.send()
+ send(array) {
+ this._hexdump(array);
+
+ this.s.send(array.buffer, array.byteOffset, array.byteLength);
+ }
+
+ close() {
+ if (this.s.readyState === "open" || this.s.readyState === "connecting") {
+ this.s.close();
+ }
+ }
+}
+
+exports.AdbSocket = AdbSocket;
diff --git a/devtools/client/shared/remote-debugging/adb/adb.js b/devtools/client/shared/remote-debugging/adb/adb.js
new file mode 100644
index 0000000000..b4c2a9377f
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/adb.js
@@ -0,0 +1,176 @@
+/* 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 { clearInterval, setInterval } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const {
+ adbProcess,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js");
+const {
+ adbAddon,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+const AdbDevice = require("resource://devtools/client/shared/remote-debugging/adb/adb-device.js");
+const {
+ AdbRuntime,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-runtime.js");
+const {
+ TrackDevicesCommand,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/track-devices.js");
+loader.lazyRequireGetter(
+ this,
+ "check",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-running-checker.js",
+ true
+);
+
+// Duration in milliseconds of the runtime polling. We resort to polling here because we
+// have no event to know when a runtime started on an already discovered ADB device.
+const UPDATE_RUNTIMES_INTERVAL = 3000;
+
+class Adb extends EventEmitter {
+ constructor() {
+ super();
+
+ this._trackDevicesCommand = new TrackDevicesCommand();
+
+ this._isTrackingDevices = false;
+ this._isUpdatingRuntimes = false;
+
+ this._listeners = new Set();
+ this._devices = new Map();
+ this._runtimes = [];
+
+ this._updateAdbProcess = this._updateAdbProcess.bind(this);
+ this._onDeviceConnected = this._onDeviceConnected.bind(this);
+ this._onDeviceDisconnected = this._onDeviceDisconnected.bind(this);
+ this._onNoDevicesDetected = this._onNoDevicesDetected.bind(this);
+
+ this._trackDevicesCommand.on("device-connected", this._onDeviceConnected);
+ this._trackDevicesCommand.on(
+ "device-disconnected",
+ this._onDeviceDisconnected
+ );
+ this._trackDevicesCommand.on(
+ "no-devices-detected",
+ this._onNoDevicesDetected
+ );
+ adbAddon.on("update", this._updateAdbProcess);
+ }
+
+ registerListener(listener) {
+ this._listeners.add(listener);
+ this.on("runtime-list-updated", listener);
+ this._updateAdbProcess();
+ }
+
+ unregisterListener(listener) {
+ this._listeners.delete(listener);
+ this.off("runtime-list-updated", listener);
+ this._updateAdbProcess();
+ }
+
+ async updateRuntimes() {
+ try {
+ const devices = [...this._devices.values()];
+ const promises = devices.map(d => this._getDeviceRuntimes(d));
+ const allRuntimes = await Promise.all(promises);
+
+ this._runtimes = allRuntimes.flat();
+ this.emit("runtime-list-updated");
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ getRuntimes() {
+ return this._runtimes;
+ }
+
+ getDevices() {
+ return [...this._devices.values()];
+ }
+
+ async isProcessStarted() {
+ return check();
+ }
+
+ async _startTracking() {
+ this._isTrackingDevices = true;
+ await adbProcess.start();
+
+ this._trackDevicesCommand.run();
+
+ // Device runtimes are detected by running a shell command and checking for
+ // "firefox-debugger-socket" in the list of currently running processes.
+ this._timer = setInterval(
+ this.updateRuntimes.bind(this),
+ UPDATE_RUNTIMES_INTERVAL
+ );
+ }
+
+ async _stopTracking() {
+ clearInterval(this._timer);
+ this._isTrackingDevices = false;
+ this._trackDevicesCommand.stop();
+
+ this._devices = new Map();
+ this._runtimes = [];
+ this.updateRuntimes();
+ }
+
+ _shouldTrack() {
+ return adbAddon.status === "installed" && this._listeners.size > 0;
+ }
+
+ /**
+ * This method will emit "runtime-list-ready" to notify the consumer that the list of
+ * runtimes is ready to be retrieved.
+ */
+ async _updateAdbProcess() {
+ if (!this._isTrackingDevices && this._shouldTrack()) {
+ const onRuntimesUpdated = this.once("runtime-list-updated");
+ this._startTracking();
+ // If we are starting to track runtimes, the list of runtimes will only be ready
+ // once the first "runtime-list-updated" event has been processed.
+ await onRuntimesUpdated;
+ } else if (this._isTrackingDevices && !this._shouldTrack()) {
+ this._stopTracking();
+ }
+ this.emit("runtime-list-ready");
+ }
+
+ async _onDeviceConnected(deviceId) {
+ const adbDevice = new AdbDevice(deviceId);
+ await adbDevice.initialize();
+ this._devices.set(deviceId, adbDevice);
+ this.updateRuntimes();
+ }
+
+ _onDeviceDisconnected(deviceId) {
+ this._devices.delete(deviceId);
+ this.updateRuntimes();
+ }
+
+ _onNoDevicesDetected() {
+ this.updateRuntimes();
+ }
+
+ async _getDeviceRuntimes(device) {
+ const socketPaths = [...(await device.getRuntimeSocketPaths())];
+ const runtimes = [];
+ for (const socketPath of socketPaths) {
+ const runtime = new AdbRuntime(device, socketPath);
+ await runtime.init();
+ runtimes.push(runtime);
+ }
+ return runtimes;
+ }
+}
+
+exports.adb = new Adb();
diff --git a/devtools/client/shared/remote-debugging/adb/commands/index.js b/devtools/client/shared/remote-debugging/adb/commands/index.js
new file mode 100644
index 0000000000..1032f1a4b8
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/commands/index.js
@@ -0,0 +1,29 @@
+/* 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 {
+ listDevices,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/list-devices.js");
+const {
+ prepareTCPConnection,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/prepare-tcp-connection.js");
+const {
+ runCommand,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/run-command.js");
+const {
+ shell,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/shell.js");
+const {
+ TrackDevicesCommand,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/track-devices.js");
+
+module.exports = {
+ listDevices,
+ prepareTCPConnection,
+ runCommand,
+ shell,
+ TrackDevicesCommand,
+};
diff --git a/devtools/client/shared/remote-debugging/adb/commands/list-devices.js b/devtools/client/shared/remote-debugging/adb/commands/list-devices.js
new file mode 100644
index 0000000000..c04ba9eb1e
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/commands/list-devices.js
@@ -0,0 +1,29 @@
+/* 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 { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+/**
+ * The listDevices command is currently unused in DevTools. We are keeping it while
+ * working on RemoteDebugging NG, in case it becomes needed later. We will remove it from
+ * the codebase if unused at the end of the project. See Bug 1511779.
+ */
+const listDevices = function () {
+ dumpn("listDevices");
+
+ return this.runCommand("host:devices").then(function onSuccess(data) {
+ const lines = data.split("\n");
+ const res = [];
+ lines.forEach(function (line) {
+ if (!line.length) {
+ return;
+ }
+ const [device] = line.split("\t");
+ res.push(device);
+ });
+ return res;
+ });
+};
+exports.listDevices = listDevices;
diff --git a/devtools/client/shared/remote-debugging/adb/commands/moz.build b/devtools/client/shared/remote-debugging/adb/commands/moz.build
new file mode 100644
index 0000000000..ecd3428a1b
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/commands/moz.build
@@ -0,0 +1,12 @@
+# 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(
+ "index.js",
+ "list-devices.js",
+ "prepare-tcp-connection.js",
+ "run-command.js",
+ "shell.js",
+ "track-devices.js",
+)
diff --git a/devtools/client/shared/remote-debugging/adb/commands/prepare-tcp-connection.js b/devtools/client/shared/remote-debugging/adb/commands/prepare-tcp-connection.js
new file mode 100644
index 0000000000..dc9103f56b
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/commands/prepare-tcp-connection.js
@@ -0,0 +1,46 @@
+/* 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 { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+const {
+ runCommand,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/run-command.js");
+
+// sends adb forward deviceId, localPort and devicePort
+const forwardPort = function (deviceId, localPort, devicePort) {
+ dumpn("forwardPort " + localPort + " -- " + devicePort);
+ // Send "host-serial:<serial-number>:<request>",
+ // with <request> set to "forward:<local>;<remote>"
+ // See https://android.googlesource.com/platform/system/core/+/jb-dev/adb/SERVICES.TXT
+ return runCommand(
+ `host-serial:${deviceId}:forward:${localPort};${devicePort}`
+ ).then(function onSuccess(data) {
+ return data;
+ });
+};
+
+const getFreeTCPPort = function () {
+ const serv = Cc["@mozilla.org/network/server-socket;1"].createInstance(
+ Ci.nsIServerSocket
+ );
+ serv.init(-1, true, -1);
+ const port = serv.port;
+ serv.close();
+ return port;
+};
+
+// Prepare TCP connection for provided device id and socket path.
+// The returned value is a port number of localhost for the connection.
+const prepareTCPConnection = async function (deviceId, socketPath) {
+ const port = getFreeTCPPort();
+ const local = `tcp:${port}`;
+ const remote = socketPath.startsWith("@")
+ ? `localabstract:${socketPath.substring(1)}`
+ : `localfilesystem:${socketPath}`;
+ await forwardPort(deviceId, local, remote);
+ return port;
+};
+exports.prepareTCPConnection = prepareTCPConnection;
diff --git a/devtools/client/shared/remote-debugging/adb/commands/run-command.js b/devtools/client/shared/remote-debugging/adb/commands/run-command.js
new file mode 100644
index 0000000000..5fc521b5e7
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/commands/run-command.js
@@ -0,0 +1,66 @@
+/* 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 { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const {
+ adbProcess,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js");
+const client = require("resource://devtools/client/shared/remote-debugging/adb/adb-client.js");
+
+const OKAY = 0x59414b4f;
+
+// Asynchronously runs an adb command.
+// @param command The command as documented in
+// http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
+const runCommand = function (command) {
+ dumpn("runCommand " + command);
+ return new Promise((resolve, reject) => {
+ if (!adbProcess.ready) {
+ setTimeout(function () {
+ reject("ADB_NOT_READY");
+ });
+ return;
+ }
+
+ const socket = client.connect();
+
+ socket.s.onopen = function () {
+ dumpn("runCommand onopen");
+ const req = client.createRequest(command);
+ socket.send(req);
+ };
+
+ socket.s.onerror = function () {
+ dumpn("runCommand onerror");
+ reject("NETWORK_ERROR");
+ };
+
+ socket.s.onclose = function () {
+ dumpn("runCommand onclose");
+ };
+
+ socket.s.ondata = function (event) {
+ dumpn("runCommand ondata");
+ const data = event.data;
+
+ const packet = client.unpackPacket(data, false);
+ if (!client.checkResponse(data, OKAY)) {
+ socket.close();
+ dumpn("Error: " + packet.data);
+ reject("PROTOCOL_ERROR");
+ return;
+ }
+
+ resolve(packet.data);
+ };
+ });
+};
+exports.runCommand = runCommand;
diff --git a/devtools/client/shared/remote-debugging/adb/commands/shell.js b/devtools/client/shared/remote-debugging/adb/commands/shell.js
new file mode 100644
index 0000000000..03f4bfcf78
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/commands/shell.js
@@ -0,0 +1,107 @@
+/* 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 { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+const client = require("resource://devtools/client/shared/remote-debugging/adb/adb-client.js");
+
+const OKAY = 0x59414b4f;
+
+const shell = async function (deviceId, command) {
+ if (!deviceId) {
+ throw new Error("ADB shell command needs the device id");
+ }
+
+ let state;
+ let stdout = "";
+
+ dumpn("shell " + command + " on " + deviceId);
+
+ return new Promise((resolve, reject) => {
+ const shutdown = function () {
+ dumpn("shell shutdown");
+ socket.close();
+ reject("BAD_RESPONSE");
+ };
+
+ const runFSM = function runFSM(data) {
+ dumpn("runFSM " + state);
+ let req;
+ let ignoreResponseCode = false;
+ switch (state) {
+ case "start":
+ state = "send-transport";
+ runFSM();
+ break;
+ case "send-transport":
+ req = client.createRequest("host:transport:" + deviceId);
+ socket.send(req);
+ state = "wait-transport";
+ break;
+ case "wait-transport":
+ if (!client.checkResponse(data, OKAY)) {
+ shutdown();
+ return;
+ }
+ state = "send-shell";
+ runFSM();
+ break;
+ case "send-shell":
+ req = client.createRequest("shell:" + command);
+ socket.send(req);
+ state = "rec-shell";
+ break;
+ case "rec-shell":
+ if (!client.checkResponse(data, OKAY)) {
+ shutdown();
+ return;
+ }
+ state = "decode-shell";
+ if (client.getBuffer(data).byteLength == 4) {
+ break;
+ }
+ ignoreResponseCode = true;
+ // eslint-disable-next-lined no-fallthrough
+ case "decode-shell":
+ const decoder = new TextDecoder();
+ const text = new Uint8Array(
+ client.getBuffer(data),
+ ignoreResponseCode ? 4 : 0
+ );
+ stdout += decoder.decode(text);
+ break;
+ default:
+ dumpn("shell Unexpected State: " + state);
+ reject("UNEXPECTED_STATE");
+ }
+ };
+
+ const socket = client.connect();
+ socket.s.onerror = function (event) {
+ dumpn("shell onerror");
+ reject("SOCKET_ERROR");
+ };
+
+ socket.s.onopen = function (event) {
+ dumpn("shell onopen");
+ state = "start";
+ runFSM();
+ };
+
+ socket.s.onclose = function (event) {
+ resolve(stdout);
+ dumpn("shell onclose");
+ };
+
+ socket.s.ondata = function (event) {
+ dumpn("shell ondata");
+ runFSM(event.data);
+ };
+ });
+};
+
+exports.shell = shell;
diff --git a/devtools/client/shared/remote-debugging/adb/commands/track-devices.js b/devtools/client/shared/remote-debugging/adb/commands/track-devices.js
new file mode 100644
index 0000000000..2d796668ea
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/commands/track-devices.js
@@ -0,0 +1,163 @@
+/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const {
+ adbProcess,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js");
+const client = require("resource://devtools/client/shared/remote-debugging/adb/adb-client.js");
+
+const ADB_STATUS_OFFLINE = "offline";
+const OKAY = 0x59414b4f;
+
+// Start tracking devices connecting and disconnecting from the host.
+// We can't reuse runCommand here because we keep the socket alive.
+class TrackDevicesCommand extends EventEmitter {
+ run() {
+ this._waitForFirst = true;
+ // Hold device statuses. key: device id, value: status.
+ this._devices = new Map();
+ this._socket = client.connect();
+
+ this._socket.s.onopen = this._onOpen.bind(this);
+ this._socket.s.onerror = this._onError.bind(this);
+ this._socket.s.onclose = this._onClose.bind(this);
+ this._socket.s.ondata = this._onData.bind(this);
+ }
+
+ stop() {
+ if (this._socket) {
+ this._socket.close();
+
+ this._socket.s.onopen = null;
+ this._socket.s.onerror = null;
+ this._socket.s.onclose = null;
+ this._socket.s.ondata = null;
+ }
+ }
+
+ _onOpen() {
+ dumpn("trackDevices onopen");
+ const req = client.createRequest("host:track-devices");
+ this._socket.send(req);
+ }
+
+ _onError(event) {
+ dumpn("trackDevices onerror: " + event);
+ }
+
+ _onClose() {
+ dumpn("trackDevices onclose");
+
+ // Report all devices as disconnected
+ this._disconnectAllDevices();
+
+ // 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.
+
+ // Give some time to the new adb to start
+ setTimeout(() => {
+ // Only try to reconnect/restart if the add-on is still enabled
+ if (adbProcess.ready) {
+ // try to connect to the new local adb server or spawn a new one
+ adbProcess.start().then(() => {
+ // Re-track devices
+ this.run();
+ });
+ }
+ }, 2000);
+ }
+
+ _onData(event) {
+ dumpn("trackDevices ondata");
+ const data = event.data;
+ dumpn("length=" + data.byteLength);
+ const dec = new TextDecoder();
+ dumpn(dec.decode(new Uint8Array(data)).trim());
+
+ // check the OKAY or FAIL on first packet.
+ if (this._waitForFirst) {
+ if (!client.checkResponse(data, OKAY)) {
+ this._socket.close();
+ return;
+ }
+ }
+
+ const packet = client.unpackPacket(data, !this._waitForFirst);
+ this._waitForFirst = false;
+
+ if (packet.data == "") {
+ // All devices got disconnected.
+ this._disconnectAllDevices();
+ } else {
+ // One line per device, each line being $DEVICE\t(offline|device)
+ const lines = packet.data.split("\n");
+ const newDevices = new Map();
+ lines.forEach(function (line) {
+ if (!line.length) {
+ return;
+ }
+
+ const [deviceId, status] = line.split("\t");
+ newDevices.set(deviceId, status);
+ });
+
+ // Fire events if needed.
+ const deviceIds = new Set([
+ ...this._devices.keys(),
+ ...newDevices.keys(),
+ ]);
+ for (const deviceId of deviceIds) {
+ const currentStatus = this._devices.get(deviceId);
+ const newStatus = newDevices.get(deviceId);
+ this._fireConnectionEventIfNeeded(deviceId, currentStatus, newStatus);
+ }
+
+ // Update devices.
+ this._devices = newDevices;
+ }
+ }
+
+ _disconnectAllDevices() {
+ if (this._devices.size === 0) {
+ // If no devices were detected, fire an event to let consumer resume.
+ this.emit("no-devices-detected");
+ } else {
+ for (const [deviceId, status] of this._devices.entries()) {
+ if (status !== ADB_STATUS_OFFLINE) {
+ this.emit("device-disconnected", deviceId);
+ }
+ }
+ }
+ this._devices = new Map();
+ }
+
+ _fireConnectionEventIfNeeded(deviceId, currentStatus, newStatus) {
+ const isCurrentOnline = !!(
+ currentStatus && currentStatus !== ADB_STATUS_OFFLINE
+ );
+ const isNewOnline = !!(newStatus && newStatus !== ADB_STATUS_OFFLINE);
+
+ if (isCurrentOnline === isNewOnline) {
+ return;
+ }
+
+ if (isNewOnline) {
+ this.emit("device-connected", deviceId);
+ } else {
+ this.emit("device-disconnected", deviceId);
+ }
+ }
+}
+exports.TrackDevicesCommand = TrackDevicesCommand;
diff --git a/devtools/client/shared/remote-debugging/adb/moz.build b/devtools/client/shared/remote-debugging/adb/moz.build
new file mode 100644
index 0000000000..9210dec08e
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/moz.build
@@ -0,0 +1,24 @@
+# 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/.
+
+DIRS += [
+ "commands",
+]
+
+DevToolsModules(
+ "adb-addon.js",
+ "adb-binary.js",
+ "adb-client.js",
+ "adb-device.js",
+ "adb-process.js",
+ "adb-running-checker.js",
+ "adb-runtime.js",
+ "adb-socket.js",
+ "adb.js",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "about:debugging")
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.ini"]
diff --git a/devtools/client/shared/remote-debugging/adb/xpcshell/.eslintrc.js b/devtools/client/shared/remote-debugging/adb/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..e4da98dd14
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/xpcshell/.eslintrc.js
@@ -0,0 +1,9 @@
+/* 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";
+
+module.exports = {
+ extends: "../../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/shared/remote-debugging/adb/xpcshell/adb.py b/devtools/client/shared/remote-debugging/adb/xpcshell/adb.py
new file mode 100644
index 0000000000..4b720a1f86
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/xpcshell/adb.py
@@ -0,0 +1,72 @@
+#!/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
+"""
+
+import os
+import socketserver
+import sys
+
+HOST = "127.0.0.1"
+PORT = 5037
+
+
+class ADBRequestHandler(socketserver.BaseRequestHandler):
+ def sendData(self, data):
+ header = "OKAY%04x" % len(data)
+ all_data = header + data
+ total_length = len(all_data)
+ sent_length = 0
+ # Make sure send all data to the client.
+ # Though the data length here is pretty small but sometimes when the
+ # client is on heavy load (e.g. MOZ_CHAOSMODE) we can't send the whole
+ # data at once.
+ while sent_length < total_length:
+ sent = self.request.send(all_data[sent_length:].encode("utf-8", "replace"))
+ sent_length = sent_length + sent
+
+ def handle(self):
+ while True:
+ data = self.request.recv(4096).decode("utf-8", "replace")
+ if "host:kill" in data:
+ self.sendData("")
+ # Implicitly close all open sockets by exiting the program.
+ # This should be done ASAP, because upon receiving the OKAY,
+ # the client expects adb to have released the server's port.
+ os._exit(0)
+ break
+ elif "host:version" in data:
+ self.sendData("001F")
+ self.request.close()
+ break
+ elif "host:track-devices" in data:
+ self.sendData("1234567890\tdevice")
+ break
+
+
+class ADBServer(socketserver.TCPServer):
+ def __init__(self, server_address):
+ # Create a socketserver with bind_and_activate 'False' to set
+ # allow_reuse_address before binding.
+ socketserver.TCPServer.__init__(
+ self, server_address, ADBRequestHandler, bind_and_activate=False
+ )
+
+
+if len(sys.argv) == 2 and sys.argv[1] == "start-server":
+ # daemonize
+ if os.fork() > 0:
+ sys.exit(0)
+ os.setsid()
+ if os.fork() > 0:
+ sys.exit(0)
+
+ server = ADBServer((HOST, PORT))
+ server.allow_reuse_address = True
+ server.server_bind()
+ server.server_activate()
+ server.serve_forever()
diff --git a/devtools/client/shared/remote-debugging/adb/xpcshell/test_adb.js b/devtools/client/shared/remote-debugging/adb/xpcshell/test_adb.js
new file mode 100644
index 0000000000..b2ea288604
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/xpcshell/test_adb.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+const {
+ getFileForBinary,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-binary.js");
+const {
+ check,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-running-checker.js");
+const {
+ adbProcess,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-process.js");
+const {
+ TrackDevicesCommand,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/index.js");
+
+const ADB_JSON = {
+ Linux: {
+ x86: ["linux/adb"],
+ x86_64: ["linux64/adb"],
+ },
+ Darwin: {
+ x86_64: ["mac64/adb"],
+ },
+ WINNT: {
+ x86: ["win32/adb.exe", "win32/AdbWinApi.dll", "win32/AdbWinUsbApi.dll"],
+ x86_64: ["win32/adb.exe", "win32/AdbWinApi.dll", "win32/AdbWinUsbApi.dll"],
+ },
+};
+let extension_version = 1.0;
+
+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: (extension_version++).toString(),
+ browser_specific_settings: {
+ gecko: { id: "not-adb@mozilla.org" },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ const adbBinary = await getFileForBinary();
+ equal(adbBinary, null);
+
+ await extension.unload();
+});
+
+add_task(async function testNoAdbJSON() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ version: (extension_version++).toString(),
+ browser_specific_settings: {
+ // 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();
+ equal(adbBinary, null);
+
+ await extension.unload();
+});
+
+add_task(async function testNoTargetBinaries() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ version: (extension_version++).toString(),
+ browser_specific_settings: {
+ gecko: { id: "adb@mozilla.org" },
+ },
+ },
+ files: {
+ "adb.json": JSON.stringify(ADB_JSON),
+ },
+ });
+
+ await extension.startup();
+
+ const adbBinary = await getFileForBinary();
+ equal(adbBinary, null);
+
+ await extension.unload();
+});
+
+add_task(async function testExtract() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ version: (extension_version++).toString(),
+ browser_specific_settings: {
+ gecko: { id: "adb@mozilla.org" },
+ },
+ },
+ files: {
+ "adb.json": JSON.stringify(ADB_JSON),
+ "linux/adb": "adb",
+ "linux64/adb": "adb",
+ "mac64/adb": "adb",
+ "win32/adb.exe": "adb.exe",
+ "win32/AdbWinApi.dll": "AdbWinApi.dll",
+ "win32/AdbWinUsbApi.dll": "AdbWinUsbApi.dll",
+ },
+ });
+
+ await extension.startup();
+
+ const adbBinary = await getFileForBinary();
+ ok(await adbBinary.exists());
+
+ await extension.unload();
+});
+
+add_task(
+ {
+ skip_if: () => mozinfo.os == "win", // bug 1482008
+ },
+ async function testStartAndStop() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ version: (extension_version++).toString(),
+ browser_specific_settings: {
+ 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();
+
+ // Call start() once and call stop() afterwards.
+ await adbProcess.start();
+ ok(adbProcess.ready);
+ ok(await check(), "adb is now running");
+
+ await adbProcess.stop();
+ ok(!adbProcess.ready);
+ ok(!(await check()), "adb is no longer running");
+
+ // Call start() twice and call stop() afterwards.
+ await adbProcess.start();
+ await adbProcess.start();
+ ok(adbProcess.ready);
+ ok(await check(), "adb is now running");
+
+ await adbProcess.stop();
+ ok(!adbProcess.ready);
+ ok(!(await check()), "adb is no longer running");
+
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ skip_if: () => mozinfo.os == "win", // bug 1482008
+ },
+ async function testTrackDevices() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ version: (extension_version++).toString(),
+ browser_specific_settings: {
+ 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 adbProcess.start();
+ ok(adbProcess.ready);
+
+ ok(await check(), "adb is now running");
+
+ const receivedDeviceId = await new Promise(resolve => {
+ const trackDevicesCommand = new TrackDevicesCommand();
+ trackDevicesCommand.on("device-connected", deviceId => {
+ resolve(deviceId);
+ });
+ trackDevicesCommand.run();
+ });
+
+ equal(receivedDeviceId, "1234567890");
+
+ await adbProcess.stop();
+ ok(!adbProcess.ready);
+
+ await extension.unload();
+ }
+);
diff --git a/devtools/client/shared/remote-debugging/adb/xpcshell/test_prepare-tcp-connection.js b/devtools/client/shared/remote-debugging/adb/xpcshell/test_prepare-tcp-connection.js
new file mode 100644
index 0000000000..09dad165f2
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/xpcshell/test_prepare-tcp-connection.js
@@ -0,0 +1,78 @@
+"use strict";
+
+const DEVICE_ID = "FAKE_DEVICE_ID";
+const SOCKET_PATH = "fake/socket/path";
+
+/**
+ * Test the prepare-tcp-connection command in isolation.
+ */
+add_task(async function testParseFileUri() {
+ info("Enable devtools.testing for this test to allow mocked modules");
+ Services.prefs.setBoolPref("devtools.testing", true);
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.testing");
+ });
+
+ // Mocks are not supported for the regular DevTools loader.
+ info("Create a BrowserLoader to enable mocks in the test");
+ const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/browser-loader.js"
+ );
+ const mockedRequire = BrowserLoader({
+ baseURI: "resource://devtools/client/shared/remote-debugging/adb",
+ window: {},
+ }).require;
+
+ // Prepare a mocked version of the run-command.js module to test
+ // prepare-tcp-connection in isolation.
+ info("Create a run-command module mock");
+ let createdPort;
+ const mock = {
+ runCommand: command => {
+ // Check that we only receive the expected command and extract the port
+ // number.
+
+ // The command should follow the pattern
+ // `host-serial:${deviceId}:forward:tcp:${localPort};${devicePort}`
+ // with devicePort = `localfilesystem:${socketPath}`
+ const socketPathRe = SOCKET_PATH.replace(/\//g, "\\/");
+ const regexp = new RegExp(
+ `host-serial:${DEVICE_ID}:forward:tcp:(\\d+);localfilesystem:${socketPathRe}`
+ );
+ const matches = regexp.exec(command);
+ equal(matches.length, 2, "The command is the expected");
+ createdPort = matches[1];
+
+ // Finally return a Promise-like object.
+ return {
+ then: () => {},
+ };
+ },
+ };
+
+ info("Enable the mocked run-command module");
+ const { setMockedModule, removeMockedModule } = mockedRequire(
+ "devtools/shared/loader/browser-loader-mocks"
+ );
+ setMockedModule(
+ mock,
+ "devtools/client/shared/remote-debugging/adb/commands/run-command"
+ );
+ registerCleanupFunction(() => {
+ removeMockedModule(
+ "devtools/client/shared/remote-debugging/adb/commands/run-command"
+ );
+ });
+
+ const { prepareTCPConnection } = mockedRequire(
+ "devtools/client/shared/remote-debugging/adb/commands/prepare-tcp-connection"
+ );
+
+ const port = await prepareTCPConnection(DEVICE_ID, SOCKET_PATH);
+ ok(!Number.isNaN(port), "prepareTCPConnection returned a valid port");
+ equal(
+ port,
+ Number(createdPort),
+ "prepareTCPConnection returned the port sent to the host-serial command"
+ );
+});
diff --git a/devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell-head.js b/devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell-head.js
new file mode 100644
index 0000000000..733c0400da
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell-head.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
diff --git a/devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell.ini b/devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..d2b34c105a
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/adb/xpcshell/xpcshell.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = devtools
+head = xpcshell-head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android' || socketprocess_networking
+support-files =
+ adb.py
+
+[test_adb.js]
+run-sequentially = An extension having the same id is installed/uninstalled in different tests
+[test_prepare-tcp-connection.js]
diff --git a/devtools/client/shared/remote-debugging/constants.js b/devtools/client/shared/remote-debugging/constants.js
new file mode 100644
index 0000000000..ad4d180548
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/constants.js
@@ -0,0 +1,24 @@
+/* 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 CONNECTION_TYPES = {
+ NETWORK: "network",
+ THIS_FIREFOX: "this-firefox",
+ UNKNOWN: "unknown",
+ USB: "usb",
+};
+
+const DEBUG_TARGET_TYPES = {
+ EXTENSION: "extension",
+ PROCESS: "process",
+ TAB: "tab",
+ WORKER: "worker",
+};
+
+module.exports = {
+ CONNECTION_TYPES,
+ DEBUG_TARGET_TYPES,
+};
diff --git a/devtools/client/shared/remote-debugging/moz.build b/devtools/client/shared/remote-debugging/moz.build
new file mode 100644
index 0000000000..da4494b1de
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=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/.
+
+DIRS += [
+ "adb",
+]
+
+DevToolsModules(
+ "constants.js",
+ "remote-client-manager.js",
+ "version-checker.js",
+)
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "about:debugging")
diff --git a/devtools/client/shared/remote-debugging/remote-client-manager.js b/devtools/client/shared/remote-debugging/remote-client-manager.js
new file mode 100644
index 0000000000..45e7a1be4a
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/remote-client-manager.js
@@ -0,0 +1,146 @@
+/* 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 {
+ CONNECTION_TYPES,
+} = require("resource://devtools/client/shared/remote-debugging/constants.js");
+
+/**
+ * This class is designed to be a singleton shared by all DevTools to get access to
+ * existing clients created for remote debugging.
+ */
+class RemoteClientManager {
+ constructor() {
+ this._clients = new Map();
+ this._runtimeInfoMap = new Map();
+ this._onClientClosed = this._onClientClosed.bind(this);
+ }
+
+ /**
+ * Store a remote client that is already connected.
+ *
+ * @param {String} id
+ * Remote runtime id (see devtools/client/aboutdebugging/src/types).
+ * @param {String} type
+ * Remote runtime type (see devtools/client/aboutdebugging/src/types).
+ * @param {DevToolsClient} client
+ * @param {Object} runtimeInfo
+ * See runtimeInfo type from client/aboutdebugging/src/types/runtime.js
+ */
+ setClient(id, type, client, runtimeInfo) {
+ const key = this._getKey(id, type);
+ this._clients.set(key, client);
+ if (runtimeInfo) {
+ this._runtimeInfoMap.set(key, runtimeInfo);
+ }
+ client.once("closed", this._onClientClosed);
+ }
+
+ // See JSDoc for id, type from setClient.
+ hasClient(id, type) {
+ return this._clients.has(this._getKey(id, type));
+ }
+
+ // See JSDoc for id, type from setClient.
+ getClient(id, type) {
+ return this._clients.get(this._getKey(id, type));
+ }
+
+ // See JSDoc for id, type from setClient.
+ removeClient(id, type) {
+ const key = this._getKey(id, type);
+ this._removeClientByKey(key);
+ }
+
+ removeAllClients() {
+ const keys = [...this._clients.keys()];
+ for (const key of keys) {
+ this._removeClientByKey(key);
+ }
+ }
+
+ /**
+ * Retrieve a unique, url-safe key based on a runtime id and type.
+ */
+ getRemoteId(id, type) {
+ return encodeURIComponent(this._getKey(id, type));
+ }
+
+ /**
+ * Retrieve a managed client for a remote id. The remote id should have been generated
+ * using getRemoteId.
+ */
+ getClientByRemoteId(remoteId) {
+ const key = this._getKeyByRemoteId(remoteId);
+ return this._clients.get(key);
+ }
+
+ /**
+ * Retrieve the runtime info for a remote id. To display metadata about a runtime, such
+ * as name, device name, version... this runtimeInfo should be used rather than calling
+ * APIs on the client.
+ */
+ getRuntimeInfoByRemoteId(remoteId) {
+ const key = this._getKeyByRemoteId(remoteId);
+ return this._runtimeInfoMap.get(key);
+ }
+
+ /**
+ * Retrieve a managed client for a remote id. The remote id should have been generated
+ * using getRemoteId.
+ */
+ getConnectionTypeByRemoteId(remoteId) {
+ const key = this._getKeyByRemoteId(remoteId);
+ for (const type of Object.values(CONNECTION_TYPES)) {
+ if (key.endsWith(type)) {
+ return type;
+ }
+ }
+ return CONNECTION_TYPES.UNKNOWN;
+ }
+
+ _getKey(id, type) {
+ return id + "-" + type;
+ }
+
+ _getKeyByRemoteId(remoteId) {
+ if (!remoteId) {
+ // If no remote id was provided, return the key corresponding to the local
+ // this-firefox runtime.
+ const { THIS_FIREFOX } = CONNECTION_TYPES;
+ return this._getKey(THIS_FIREFOX, THIS_FIREFOX);
+ }
+
+ return decodeURIComponent(remoteId);
+ }
+
+ _removeClientByKey(key) {
+ const client = this._clients.get(key);
+ if (client) {
+ client.off("closed", this._onClientClosed);
+ this._clients.delete(key);
+ this._runtimeInfoMap.delete(key);
+ }
+ }
+
+ /**
+ * Cleanup all closed clients when a "closed" notification is received from a client.
+ */
+ _onClientClosed() {
+ const closedClientKeys = [...this._clients.keys()].filter(key => {
+ return this._clients.get(key)._transportClosed;
+ });
+
+ for (const key of closedClientKeys) {
+ this._removeClientByKey(key);
+ }
+ }
+}
+
+// Expose a singleton of RemoteClientManager.
+module.exports = {
+ remoteClientManager: new RemoteClientManager(),
+};
diff --git a/devtools/client/shared/remote-debugging/test/xpcshell/.eslintrc.js b/devtools/client/shared/remote-debugging/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..86bd54c245
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/shared/remote-debugging/test/xpcshell/test_remote_client_manager.js b/devtools/client/shared/remote-debugging/test/xpcshell/test_remote_client_manager.js
new file mode 100644
index 0000000000..e66c38b67d
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/xpcshell/test_remote_client_manager.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+const {
+ CONNECTION_TYPES,
+} = require("resource://devtools/client/shared/remote-debugging/constants.js");
+
+add_task(async function testRemoteClientManager() {
+ for (const type of Object.values(CONNECTION_TYPES)) {
+ const fakeClient = createFakeClient();
+ const runtimeInfo = {};
+ const clientId = "clientId";
+ const remoteId = remoteClientManager.getRemoteId(clientId, type);
+
+ const connectionType =
+ remoteClientManager.getConnectionTypeByRemoteId(remoteId);
+ equal(
+ connectionType,
+ type,
+ `[${type}]: Correct connection type was returned by getConnectionTypeByRemoteId`
+ );
+
+ equal(
+ remoteClientManager.hasClient(clientId, type),
+ false,
+ `[${type}]: hasClient returns false if no client was set`
+ );
+ equal(
+ remoteClientManager.getClient(clientId, type),
+ null,
+ `[${type}]: getClient returns null if no client was set`
+ );
+ equal(
+ remoteClientManager.getClientByRemoteId(remoteId),
+ null,
+ `[${type}]: getClientByRemoteId returns null if no client was set`
+ );
+ equal(
+ remoteClientManager.getRuntimeInfoByRemoteId(remoteId),
+ null,
+ `[${type}]: getRuntimeInfoByRemoteId returns null if no client was set`
+ );
+
+ remoteClientManager.setClient(clientId, type, fakeClient, runtimeInfo);
+ equal(
+ remoteClientManager.hasClient(clientId, type),
+ true,
+ `[${type}]: hasClient returns true`
+ );
+ equal(
+ remoteClientManager.getClient(clientId, type),
+ fakeClient,
+ `[${type}]: getClient returns the correct client`
+ );
+ equal(
+ remoteClientManager.getClientByRemoteId(remoteId),
+ fakeClient,
+ `[${type}]: getClientByRemoteId returns the correct client`
+ );
+ equal(
+ remoteClientManager.getRuntimeInfoByRemoteId(remoteId),
+ runtimeInfo,
+ `[${type}]: getRuntimeInfoByRemoteId returns the correct object`
+ );
+
+ remoteClientManager.removeClient(clientId, type);
+ equal(
+ remoteClientManager.hasClient(clientId, type),
+ false,
+ `[${type}]: hasClient returns false after removing the client`
+ );
+ equal(
+ remoteClientManager.getClient(clientId, type),
+ null,
+ `[${type}]: getClient returns null after removing the client`
+ );
+ equal(
+ remoteClientManager.getClientByRemoteId(remoteId),
+ null,
+ `[${type}]: getClientByRemoteId returns null after removing the client`
+ );
+ equal(
+ remoteClientManager.getRuntimeInfoByRemoteId(),
+ null,
+ `[${type}]: getRuntimeInfoByRemoteId returns null after removing the client`
+ );
+ }
+
+ // Test various fallback scenarios for APIs relying on remoteId, when called without a
+ // remoteId, we expect to get the information for the local this-firefox runtime.
+ const { THIS_FIREFOX } = CONNECTION_TYPES;
+ const thisFirefoxClient = createFakeClient();
+ const thisFirefoxInfo = {};
+ remoteClientManager.setClient(
+ THIS_FIREFOX,
+ THIS_FIREFOX,
+ thisFirefoxClient,
+ thisFirefoxInfo
+ );
+
+ equal(
+ remoteClientManager.getClientByRemoteId(),
+ thisFirefoxClient,
+ `[fallback]: getClientByRemoteId returns this-firefox if remoteId is null`
+ );
+ equal(
+ remoteClientManager.getRuntimeInfoByRemoteId(),
+ thisFirefoxInfo,
+ `[fallback]: getRuntimeInfoByRemoteId returns this-firefox if remoteId is null`
+ );
+
+ const otherRemoteId = remoteClientManager.getRemoteId(
+ "clientId",
+ CONNECTION_TYPES.USB
+ );
+ equal(
+ remoteClientManager.getClientByRemoteId(otherRemoteId),
+ null,
+ `[fallback]: getClientByRemoteId does not fallback if remoteId is non-null`
+ );
+ equal(
+ remoteClientManager.getRuntimeInfoByRemoteId(otherRemoteId),
+ null,
+ `[fallback]: getRuntimeInfoByRemoteId does not fallback if remoteId is non-null`
+ );
+});
+
+add_task(async function testRemoteClientManagerWithUnknownType() {
+ const remoteId = remoteClientManager.getRemoteId(
+ "someClientId",
+ "NotARealType"
+ );
+ const connectionType =
+ remoteClientManager.getConnectionTypeByRemoteId(remoteId);
+ equal(
+ connectionType,
+ CONNECTION_TYPES.UNKNOWN,
+ `Connection type UNKNOWN was returned by getConnectionTypeByRemoteId`
+ );
+});
+
+function createFakeClient() {
+ const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+ const client = {};
+ EventEmitter.decorate(client);
+ return client;
+}
diff --git a/devtools/client/shared/remote-debugging/test/xpcshell/test_version_checker.js b/devtools/client/shared/remote-debugging/test/xpcshell/test_version_checker.js
new file mode 100644
index 0000000000..92f7d54a1e
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/xpcshell/test_version_checker.js
@@ -0,0 +1,159 @@
+/* global equal */
+
+"use strict";
+
+const {
+ _compareVersionCompatibility,
+ checkVersionCompatibility,
+ COMPATIBILITY_STATUS,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const TEST_DATA = [
+ {
+ description: "same build date and same version number",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190131000000",
+ runtimeVersion: "60.0",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description: "same build date and older version in range (-1)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190131000000",
+ runtimeVersion: "59.0",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description: "same build date and older version in range (-2)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190131000000",
+ runtimeVersion: "58.0",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description: "same build date and older version in range (-2 Nightly)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190131000000",
+ runtimeVersion: "58.0a1",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description: "same build date and older version out of range (-3)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190131000000",
+ runtimeVersion: "57.0",
+ expected: COMPATIBILITY_STATUS.TOO_OLD,
+ },
+ {
+ description: "same build date and newer version out of range (+1)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190131000000",
+ runtimeVersion: "61.0",
+ expected: COMPATIBILITY_STATUS.TOO_RECENT,
+ },
+ {
+ description: "same major version and build date in range (-10 days)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190121000000",
+ runtimeVersion: "60.0",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description: "same major version and build date in range (+2 days)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190202000000",
+ runtimeVersion: "60.0",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description: "same major version and build date out of range (+8 days)",
+ localBuildId: "20190131000000",
+ localVersion: "60.0",
+ runtimeBuildId: "20190208000000",
+ runtimeVersion: "60.0",
+ expected: COMPATIBILITY_STATUS.TOO_RECENT,
+ },
+ {
+ description:
+ "fennec 68 compatibility error not raised for 68 -> 68 Android",
+ localBuildId: "20190131000000",
+ localVersion: "68.0",
+ runtimeBuildId: "20190202000000",
+ runtimeVersion: "68.0",
+ runtimeOs: "Android",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description:
+ "fennec 68 compatibility error not raised for 70 -> 68 Android",
+ localBuildId: "20190131000000",
+ localVersion: "70.0",
+ runtimeBuildId: "20190202000000",
+ runtimeVersion: "68.0",
+ runtimeOs: "Android",
+ expected: COMPATIBILITY_STATUS.COMPATIBLE,
+ },
+ {
+ description: "fennec 68 compatibility error raised for 71 -> 68 Android",
+ localBuildId: "20190131000000",
+ localVersion: "71.0",
+ runtimeBuildId: "20190202000000",
+ runtimeVersion: "68.0",
+ runtimeOs: "Android",
+ expected: COMPATIBILITY_STATUS.TOO_OLD_FENNEC,
+ },
+ {
+ description:
+ "fennec 68 compatibility error not raised for 71 -> 68 non-Android",
+ localBuildId: "20190131000000",
+ localVersion: "71.0",
+ runtimeBuildId: "20190202000000",
+ runtimeVersion: "68.0",
+ runtimeOs: "NotAndroid",
+ expected: COMPATIBILITY_STATUS.TOO_OLD,
+ },
+];
+
+add_task(async function testVersionChecker() {
+ for (const testData of TEST_DATA) {
+ const localDescription = {
+ appbuildid: testData.localBuildId,
+ platformversion: testData.localVersion,
+ };
+
+ const runtimeDescription = {
+ appbuildid: testData.runtimeBuildId,
+ platformversion: testData.runtimeVersion,
+ os: testData.runtimeOs,
+ };
+
+ const report = _compareVersionCompatibility(
+ localDescription,
+ runtimeDescription
+ );
+ equal(
+ report.status,
+ testData.expected,
+ "Expected status for test: " + testData.description
+ );
+ }
+});
+
+add_task(async function testVersionCheckWithVeryOldClient() {
+ // Use an empty object as devtools client, calling any method on it will fail.
+ const emptyClient = {};
+ const report = await checkVersionCompatibility(emptyClient);
+ equal(
+ report.status,
+ COMPATIBILITY_STATUS.TOO_OLD,
+ "Report status too old if devtools client is not implementing expected interface"
+ );
+});
diff --git a/devtools/client/shared/remote-debugging/test/xpcshell/xpcshell-head.js b/devtools/client/shared/remote-debugging/test/xpcshell/xpcshell-head.js
new file mode 100644
index 0000000000..733c0400da
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/xpcshell/xpcshell-head.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
diff --git a/devtools/client/shared/remote-debugging/test/xpcshell/xpcshell.ini b/devtools/client/shared/remote-debugging/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..0579b8a674
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/xpcshell/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+head = xpcshell-head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_remote_client_manager.js]
+[test_version_checker.js]
diff --git a/devtools/client/shared/remote-debugging/version-checker.js b/devtools/client/shared/remote-debugging/version-checker.js
new file mode 100644
index 0000000000..7023a5bcd3
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/version-checker.js
@@ -0,0 +1,154 @@
+/* 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 { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const MS_PER_DAY = 1000 * 60 * 60 * 24;
+
+const COMPATIBILITY_STATUS = {
+ COMPATIBLE: "compatible",
+ TOO_OLD: "too-old",
+ TOO_OLD_FENNEC: "too-old-fennec",
+ TOO_RECENT: "too-recent",
+};
+exports.COMPATIBILITY_STATUS = COMPATIBILITY_STATUS;
+
+function getDateFromBuildID(buildID) {
+ // Build IDs are a timestamp in the yyyyMMddHHmmss format.
+ // Extract the year, month and day information.
+ const fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
+ // Date expects 0 - 11 for months
+ const month = Number.parseInt(fields[2], 10) - 1;
+ return new Date(fields[1], month, fields[3]);
+}
+
+function getMajorVersion(platformVersion) {
+ // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
+ return Number.parseInt(platformVersion.match(/\d+/)[0], 10);
+}
+
+/**
+ * Compute the minimum and maximum supported version for remote debugging for the provided
+ * version of Firefox. Backward compatibility policy for devtools supports at most 2
+ * versions older than the current version.
+ *
+ * @param {String} localVersion
+ * The version of the local Firefox instance, eg "67.0"
+ * @return {Object}
+ * - minVersion {String} the minimum supported version, eg "65.0a1"
+ * - maxVersion {String} the first unsupported version, eg "68.0a1"
+ */
+function computeMinMaxVersion(localVersion) {
+ // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
+ const localMajorVersion = getMajorVersion(localVersion);
+
+ return {
+ // Define the minimum officially supported version of Firefox when connecting to a
+ // remote runtime. (Use ".0a1" to support the very first nightly version)
+ // This matches the release channel's version when we are on nightly,
+ // or 2 versions before when we are on other channels.
+ minVersion: localMajorVersion - 2 + ".0a1",
+ // The maximum version is the first excluded from the support range. That's why we
+ // increase the current version by 1 and use ".0a1" to point to the first Nightly.
+ // We do not support forward compatibility at all.
+ maxVersion: localMajorVersion + 1 + ".0a1",
+ };
+}
+
+/**
+ * Tells if the remote device is using a supported version of Firefox.
+ *
+ * @param {DevToolsClient} devToolsClient
+ * DevToolsClient instance connected to the target remote Firefox.
+ * @return Object with the following attributes:
+ * * String status, one of COMPATIBILITY_STATUS
+ * COMPATIBLE if the runtime is compatible,
+ * TOO_RECENT if the runtime uses a too recent version,
+ * TOO_OLD if the runtime uses a too old version.
+ * * String minVersion
+ * The minimum supported version.
+ * * String runtimeVersion
+ * The remote runtime version.
+ * * String localID
+ * Build ID of local runtime. A date with like this: YYYYMMDD.
+ * * String deviceID
+ * Build ID of remote runtime. A date with like this: YYYYMMDD.
+ */
+async function checkVersionCompatibility(devToolsClient) {
+ const localDescription = {
+ appbuildid: Services.appinfo.appBuildID,
+ platformversion: AppConstants.MOZ_APP_VERSION,
+ };
+
+ try {
+ const deviceFront = await devToolsClient.mainRoot.getFront("device");
+ const description = await deviceFront.getDescription();
+ return _compareVersionCompatibility(localDescription, description);
+ } catch (e) {
+ // If we failed to retrieve the device description, assume we are trying to connect to
+ // a really old version of Firefox.
+ const localVersion = localDescription.platformversion;
+ const { minVersion } = computeMinMaxVersion(localVersion);
+ return {
+ minVersion,
+ runtimeVersion: "<55",
+ status: COMPATIBILITY_STATUS.TOO_OLD,
+ };
+ }
+}
+exports.checkVersionCompatibility = checkVersionCompatibility;
+
+function _compareVersionCompatibility(localDescription, deviceDescription) {
+ const runtimeID = deviceDescription.appbuildid.substr(0, 8);
+ const localID = localDescription.appbuildid.substr(0, 8);
+
+ const runtimeDate = getDateFromBuildID(runtimeID);
+ const localDate = getDateFromBuildID(localID);
+
+ const runtimeVersion = deviceDescription.platformversion;
+ const localVersion = localDescription.platformversion;
+
+ const { minVersion, maxVersion } = computeMinMaxVersion(localVersion);
+ const isTooOld = Services.vc.compare(runtimeVersion, minVersion) < 0;
+ const isTooRecent = Services.vc.compare(runtimeVersion, maxVersion) >= 0;
+
+ const runtimeMajorVersion = getMajorVersion(runtimeVersion);
+ const localMajorVersion = getMajorVersion(localVersion);
+ const isSameMajorVersion = runtimeMajorVersion === localMajorVersion;
+
+ let status;
+ if (isTooOld) {
+ if (runtimeMajorVersion === 68 && deviceDescription.os === "Android") {
+ status = COMPATIBILITY_STATUS.TOO_OLD_FENNEC;
+ } else {
+ status = COMPATIBILITY_STATUS.TOO_OLD;
+ }
+ } else if (isTooRecent) {
+ status = COMPATIBILITY_STATUS.TOO_RECENT;
+ } else if (isSameMajorVersion && runtimeDate - localDate > 7 * MS_PER_DAY) {
+ // If both local and remote runtimes have the same major version, compare build dates.
+ // This check is useful for Gecko developers as we might introduce breaking changes
+ // within a Nightly cycle.
+ // Still allow devices to be newer by up to a week. This accommodates those with local
+ // device builds, since their devices will almost always be newer than the client.
+ status = COMPATIBILITY_STATUS.TOO_RECENT;
+ } else {
+ status = COMPATIBILITY_STATUS.COMPATIBLE;
+ }
+
+ return {
+ localID,
+ localVersion,
+ minVersion,
+ runtimeID,
+ runtimeVersion,
+ status,
+ };
+}
+// Exported for tests.
+exports._compareVersionCompatibility = _compareVersionCompatibility;