2018-08-09 05:27:55 +00:00
|
|
|
/* 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");
|
2018-08-10 07:32:21 +00:00
|
|
|
const { dumpn } = require("devtools/shared/DevToolsUtils");
|
2018-08-09 05:27:55 +00:00
|
|
|
const { getFileForBinary } = require("./adb-binary");
|
|
|
|
const { setTimeout } = require("resource://gre/modules/Timer.jsm");
|
|
|
|
const { Services } = require("resource://gre/modules/Services.jsm");
|
2018-08-09 05:27:55 +00:00
|
|
|
loader.lazyRequireGetter(this, "check",
|
|
|
|
"devtools/shared/adb/adb-running-checker", true);
|
2018-08-09 05:27:55 +00:00
|
|
|
|
|
|
|
let ready = false;
|
|
|
|
let didRunInitially = false;
|
|
|
|
|
|
|
|
const OKAY = 0x59414b4f;
|
|
|
|
|
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2018-08-09 06:43:39 +00:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// Waits until a predicate returns true or re-tries the predicate calls
|
|
|
|
// |retry| times, we wait for 100ms between each calls.
|
|
|
|
async _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;
|
|
|
|
},
|
|
|
|
|
2018-08-09 05:27:55 +00:00
|
|
|
// We startup by launching adb in server mode, and setting
|
|
|
|
// the tcp socket preference to |true|
|
2018-08-09 05:27:56 +00:00
|
|
|
async start() {
|
2018-08-09 05:27:55 +00:00
|
|
|
return new Promise(async (resolve, reject) => {
|
2018-08-09 05:27:55 +00:00
|
|
|
const onSuccessfulStart = () => {
|
2018-08-09 05:27:55 +00:00
|
|
|
Services.obs.notifyObservers(null, "adb-ready");
|
|
|
|
this.ready = true;
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
|
2018-08-09 05:27:55 +00:00
|
|
|
const isAdbRunning = await check();
|
2018-08-09 05:27:55 +00:00
|
|
|
if (isAdbRunning) {
|
|
|
|
this.didRunInitially = false;
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("Found ADB process running, not restarting");
|
2018-08-09 05:27:55 +00:00
|
|
|
onSuccessfulStart();
|
|
|
|
return;
|
|
|
|
}
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("Didn't find ADB process running, restarting");
|
2018-08-09 05:27:55 +00:00
|
|
|
|
|
|
|
this.didRunInitially = true;
|
2018-08-09 05:27:55 +00:00
|
|
|
const process = Cc["@mozilla.org/process/util;1"]
|
2018-08-09 05:27:55 +00:00
|
|
|
.createInstance(Ci.nsIProcess);
|
|
|
|
// FIXME: Bug 1481691 - We should avoid extracting files every time.
|
2018-08-09 05:27:55 +00:00
|
|
|
const adbFile = await this.adbFilePromise;
|
2018-08-09 05:27:55 +00:00
|
|
|
process.init(adbFile);
|
|
|
|
// Hide command prompt window on Windows
|
2018-08-10 07:32:21 +00:00
|
|
|
process.startHidden = true;
|
|
|
|
process.noShell = true;
|
2018-08-09 05:27:55 +00:00
|
|
|
const params = ["start-server"];
|
2018-08-09 06:43:39 +00:00
|
|
|
let isStarted = false;
|
|
|
|
try {
|
|
|
|
await this._runProcess(process, params);
|
|
|
|
isStarted = await this._waitUntil(check);
|
|
|
|
} catch (e) {
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isStarted) {
|
|
|
|
onSuccessfulStart();
|
|
|
|
} else {
|
|
|
|
this.ready = false;
|
|
|
|
reject();
|
|
|
|
}
|
2018-08-09 05:27:55 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop the ADB server, but only if we started it. If it was started before
|
|
|
|
* us, we return immediately.
|
|
|
|
*/
|
2018-08-10 07:32:21 +00:00
|
|
|
async stop() {
|
2018-08-09 05:27:55 +00:00
|
|
|
if (!this.didRunInitially) {
|
|
|
|
return; // We didn't start the server, nothing to do
|
|
|
|
}
|
2018-08-10 07:32:21 +00:00
|
|
|
await this.kill();
|
2018-08-09 05:27:55 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2018-08-12 21:12:55 +00:00
|
|
|
* Kill the ADB server.
|
2018-08-09 05:27:55 +00:00
|
|
|
*/
|
2018-08-10 07:32:21 +00:00
|
|
|
async kill() {
|
2018-08-12 21:12:55 +00:00
|
|
|
try {
|
|
|
|
await this.runCommand("host:kill");
|
|
|
|
} catch (e) {
|
2018-08-12 21:12:55 +00:00
|
|
|
dumpn("Failed to send host:kill command");
|
2018-08-12 21:12:55 +00:00
|
|
|
}
|
2018-08-12 21:12:55 +00:00
|
|
|
dumpn("adb server was terminated by host:kill");
|
2018-08-10 07:32:21 +00:00
|
|
|
this.ready = false;
|
|
|
|
this.didRunInitially = false;
|
2018-08-09 05:27:55 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
// 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.
|
2018-08-09 05:27:56 +00:00
|
|
|
trackDevices() {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("trackDevices");
|
2018-08-09 05:27:55 +00:00
|
|
|
const socket = client.connect();
|
2018-08-09 05:27:55 +00:00
|
|
|
let waitForFirst = true;
|
2018-08-09 05:27:55 +00:00
|
|
|
const devices = {};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
|
|
|
socket.s.onopen = function() {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("trackDevices onopen");
|
2018-08-09 05:27:55 +00:00
|
|
|
Services.obs.notifyObservers(null, "adb-track-devices-start");
|
2018-08-09 05:27:55 +00:00
|
|
|
const req = client.createRequest("host:track-devices");
|
2018-08-09 05:27:55 +00:00
|
|
|
socket.send(req);
|
|
|
|
};
|
|
|
|
|
|
|
|
socket.s.onerror = function(event) {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("trackDevices onerror: " + event);
|
2018-08-09 05:27:55 +00:00
|
|
|
Services.obs.notifyObservers(null, "adb-track-devices-stop");
|
|
|
|
};
|
|
|
|
|
|
|
|
socket.s.onclose = function() {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("trackDevices onclose");
|
2018-08-09 05:27:55 +00:00
|
|
|
|
|
|
|
// Report all devices as disconnected
|
2018-08-09 05:27:55 +00:00
|
|
|
for (const dev in devices) {
|
2018-08-09 05:27:55 +00:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2018-08-09 05:27:55 +00:00
|
|
|
socket.s.ondata = function(event) {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("trackDevices ondata");
|
2018-08-09 05:27:55 +00:00
|
|
|
const data = event.data;
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("length=" + data.byteLength);
|
2018-08-09 05:27:55 +00:00
|
|
|
const dec = new TextDecoder();
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn(dec.decode(new Uint8Array(data)).trim());
|
2018-08-09 05:27:55 +00:00
|
|
|
|
|
|
|
// check the OKAY or FAIL on first packet.
|
|
|
|
if (waitForFirst) {
|
|
|
|
if (!client.checkResponse(data, OKAY)) {
|
|
|
|
socket.close();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-09 05:27:55 +00:00
|
|
|
const packet = client.unpackPacket(data, !waitForFirst);
|
2018-08-09 05:27:55 +00:00
|
|
|
waitForFirst = false;
|
|
|
|
|
|
|
|
if (packet.data == "") {
|
|
|
|
// All devices got disconnected.
|
2018-08-09 05:27:55 +00:00
|
|
|
for (const dev in devices) {
|
2018-08-09 05:27:55 +00:00
|
|
|
devices[dev] = false;
|
|
|
|
EventEmitter.emit(ADB, "device-disconnected", dev);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// One line per device, each line being $DEVICE\t(offline|device)
|
2018-08-09 05:27:55 +00:00
|
|
|
const lines = packet.data.split("\n");
|
|
|
|
const newDev = {};
|
2018-08-09 05:27:55 +00:00
|
|
|
lines.forEach(function(line) {
|
|
|
|
if (line.length == 0) {
|
2018-08-09 05:27:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-09 05:27:55 +00:00
|
|
|
const [dev, status] = line.split("\t");
|
2018-08-09 05:27:55 +00:00
|
|
|
newDev[dev] = status !== "offline";
|
|
|
|
});
|
|
|
|
// Check which device changed state.
|
2018-08-09 05:27:55 +00:00
|
|
|
for (const dev in newDev) {
|
2018-08-09 05:27:55 +00:00
|
|
|
if (devices[dev] != newDev[dev]) {
|
|
|
|
if (dev in devices || newDev[dev]) {
|
2018-08-09 05:27:55 +00:00
|
|
|
const topic = newDev[dev] ? "device-connected"
|
|
|
|
: "device-disconnected";
|
2018-08-09 05:27:55 +00:00
|
|
|
EventEmitter.emit(ADB, topic, dev);
|
|
|
|
}
|
|
|
|
devices[dev] = newDev[dev];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
// Sends back an array of device names.
|
2018-08-09 05:27:56 +00:00
|
|
|
listDevices() {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("listDevices");
|
2018-08-09 05:27:55 +00:00
|
|
|
|
|
|
|
return this.runCommand("host:devices").then(
|
|
|
|
function onSuccess(data) {
|
2018-08-09 05:27:55 +00:00
|
|
|
const lines = data.split("\n");
|
|
|
|
const res = [];
|
2018-08-09 05:27:55 +00:00
|
|
|
lines.forEach(function(line) {
|
|
|
|
if (line.length == 0) {
|
2018-08-09 05:27:55 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-08-09 05:27:55 +00:00
|
|
|
const [ device ] = line.split("\t");
|
2018-08-09 05:27:55 +00:00
|
|
|
res.push(device);
|
|
|
|
});
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2018-08-09 05:27:55 +00:00
|
|
|
// sends adb forward localPort devicePort
|
2018-08-09 05:27:56 +00:00
|
|
|
forwardPort(localPort, devicePort) {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("forwardPort " + localPort + " -- " + devicePort);
|
2018-08-09 05:27:55 +00:00
|
|
|
// <host-prefix>:forward:<local>;<remote>
|
|
|
|
|
2018-08-09 05:27:55 +00:00
|
|
|
return this.runCommand("host:forward:" + localPort + ";" + devicePort)
|
2018-08-09 05:27:55 +00:00
|
|
|
.then(function onSuccess(data) {
|
|
|
|
return data;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// Run a shell command
|
2018-08-10 07:32:21 +00:00
|
|
|
async shell(command) {
|
2018-08-09 05:27:55 +00:00
|
|
|
let state;
|
|
|
|
let stdout = "";
|
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("shell " + command);
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const shutdown = function() {
|
|
|
|
dumpn("shell shutdown");
|
|
|
|
socket.close();
|
|
|
|
reject("BAD_RESPONSE");
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
const runFSM = function runFSM(data) {
|
|
|
|
dumpn("runFSM " + state);
|
|
|
|
let req;
|
|
|
|
let ignoreResponseCode = false;
|
|
|
|
switch (state) {
|
|
|
|
case "start":
|
|
|
|
state = "send-transport";
|
|
|
|
runFSM();
|
2018-08-09 05:27:55 +00:00
|
|
|
break;
|
2018-08-10 07:32:21 +00:00
|
|
|
case "send-transport":
|
|
|
|
req = client.createRequest("host:transport-any");
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
const socket = client.connect();
|
|
|
|
socket.s.onerror = function(event) {
|
|
|
|
dumpn("shell onerror");
|
|
|
|
reject("SOCKET_ERROR");
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
socket.s.onopen = function(event) {
|
|
|
|
dumpn("shell onopen");
|
|
|
|
state = "start";
|
|
|
|
runFSM();
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
socket.s.onclose = function(event) {
|
|
|
|
resolve(stdout);
|
|
|
|
dumpn("shell onclose");
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
socket.s.ondata = function(event) {
|
|
|
|
dumpn("shell ondata");
|
|
|
|
runFSM(event.data);
|
|
|
|
};
|
|
|
|
});
|
2018-08-09 05:27:55 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
// Asynchronously runs an adb command.
|
2018-08-09 05:27:55 +00:00
|
|
|
// @param command The command as documented in
|
2018-08-09 05:27:55 +00:00
|
|
|
// http://androidxref.com/4.0.4/xref/system/core/adb/SERVICES.TXT
|
2018-08-09 05:27:56 +00:00
|
|
|
runCommand(command) {
|
2018-08-10 07:32:21 +00:00
|
|
|
dumpn("runCommand " + command);
|
2018-08-10 07:32:21 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (!this.ready) {
|
|
|
|
setTimeout(function() {
|
|
|
|
reject("ADB_NOT_READY");
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
const socket = client.connect();
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
socket.s.onopen = function() {
|
|
|
|
dumpn("runCommand onopen");
|
|
|
|
const req = client.createRequest(command);
|
|
|
|
socket.send(req);
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
socket.s.onerror = function() {
|
|
|
|
dumpn("runCommand onerror");
|
|
|
|
reject("NETWORK_ERROR");
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
socket.s.onclose = function() {
|
|
|
|
dumpn("runCommand onclose");
|
|
|
|
};
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
socket.s.ondata = function(event) {
|
|
|
|
dumpn("runCommand ondata");
|
|
|
|
const data = event.data;
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
const packet = client.unpackPacket(data, false);
|
|
|
|
if (!client.checkResponse(data, OKAY)) {
|
|
|
|
socket.close();
|
|
|
|
dumpn("Error: " + packet.data);
|
|
|
|
reject("PROTOCOL_ERROR");
|
|
|
|
return;
|
|
|
|
}
|
2018-08-09 05:27:55 +00:00
|
|
|
|
2018-08-10 07:32:21 +00:00
|
|
|
resolve(packet.data);
|
|
|
|
};
|
|
|
|
});
|
2018-08-09 05:27:55 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.ADB = ADB;
|