blob: c1c9300308b22f0468accb3c38c1572b9890a480 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ddmlib;
import com.android.ddmlib.log.LogReceiver;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SocketChannel;
/**
* Helper class to handle requests and connections to adb.
* <p/>{@link DebugBridgeServer} is the public API to connection to adb, while {@link AdbHelper}
* does the low level stuff.
* <p/>This currently uses spin-wait non-blocking I/O. A Selector would be more efficient,
* but seems like overkill for what we're doing here.
*/
final class AdbHelper {
// public static final long kOkay = 0x59414b4fL;
// public static final long kFail = 0x4c494146L;
static final int WAIT_TIME = 5; // spin-wait sleep, in ms
static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
/** do not instantiate */
private AdbHelper() {
}
/**
* Response from ADB.
*/
static class AdbResponse {
public AdbResponse() {
message = "";
}
public boolean okay; // first 4 bytes in response were "OKAY"?
public String message; // diagnostic string if #okay is false
}
/**
* Create and connect a new pass-through socket, from the host to a port on
* the device.
*
* @param adbSockAddr
* @param device the device to connect to. Can be null in which case the connection will be
* to the first available device.
* @param devicePort the port we're opening
* @throws TimeoutException in case of timeout on the connection.
* @throws IOException in case of I/O error on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
*/
public static SocketChannel open(InetSocketAddress adbSockAddr,
Device device, int devicePort)
throws IOException, TimeoutException, AdbCommandRejectedException {
SocketChannel adbChan = SocketChannel.open(adbSockAddr);
try {
adbChan.socket().setTcpNoDelay(true);
adbChan.configureBlocking(false);
// if the device is not -1, then we first tell adb we're looking to
// talk to a specific device
setDevice(adbChan, device);
byte[] req = createAdbForwardRequest(null, devicePort);
// Log.hexDump(req);
write(adbChan, req);
AdbResponse resp = readAdbResponse(adbChan, false);
if (resp.okay == false) {
throw new AdbCommandRejectedException(resp.message);
}
adbChan.configureBlocking(true);
} catch (TimeoutException e) {
adbChan.close();
throw e;
} catch (IOException e) {
adbChan.close();
throw e;
}
return adbChan;
}
/**
* Creates and connects a new pass-through socket, from the host to a port on
* the device.
*
* @param adbSockAddr
* @param device the device to connect to. Can be null in which case the connection will be
* to the first available device.
* @param pid the process pid to connect to.
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr,
Device device, int pid)
throws TimeoutException, AdbCommandRejectedException, IOException {
SocketChannel adbChan = SocketChannel.open(adbSockAddr);
try {
adbChan.socket().setTcpNoDelay(true);
adbChan.configureBlocking(false);
// if the device is not -1, then we first tell adb we're looking to
// talk to a specific device
setDevice(adbChan, device);
byte[] req = createJdwpForwardRequest(pid);
// Log.hexDump(req);
write(adbChan, req);
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (resp.okay == false) {
throw new AdbCommandRejectedException(resp.message);
}
adbChan.configureBlocking(true);
} catch (TimeoutException e) {
adbChan.close();
throw e;
} catch (IOException e) {
adbChan.close();
throw e;
}
return adbChan;
}
/**
* Creates a port forwarding request for adb. This returns an array
* containing "####tcp:{port}:{addStr}".
* @param addrStr the host. Can be null.
* @param port the port on the device. This does not need to be numeric.
*/
private static byte[] createAdbForwardRequest(String addrStr, int port) {
String reqStr;
if (addrStr == null)
reqStr = "tcp:" + port;
else
reqStr = "tcp:" + port + ":" + addrStr;
return formAdbRequest(reqStr);
}
/**
* Creates a port forwarding request to a jdwp process. This returns an array
* containing "####jwdp:{pid}".
* @param pid the jdwp process pid on the device.
*/
private static byte[] createJdwpForwardRequest(int pid) {
String reqStr = String.format("jdwp:%1$d", pid); //$NON-NLS-1$
return formAdbRequest(reqStr);
}
/**
* Create an ASCII string preceeded by four hex digits. The opening "####"
* is the length of the rest of the string, encoded as ASCII hex (case
* doesn't matter). "port" and "host" are what we want to forward to. If
* we're on the host side connecting into the device, "addrStr" should be
* null.
*/
static byte[] formAdbRequest(String req) {
String resultStr = String.format("%04X%s", req.length(), req); //$NON-NLS-1$
byte[] result;
try {
result = resultStr.getBytes(DEFAULT_ENCODING);
} catch (UnsupportedEncodingException uee) {
uee.printStackTrace(); // not expected
return null;
}
assert result.length == req.length() + 4;
return result;
}
/**
* Reads the response from ADB after a command.
* @param chan The socket channel that is connected to adb.
* @param readDiagString If true, we're expecting an OKAY response to be
* followed by a diagnostic string. Otherwise, we only expect the
* diagnostic string to follow a FAIL.
* @throws TimeoutException in case of timeout on the connection.
* @throws IOException in case of I/O error on the connection.
*/
static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString)
throws TimeoutException, IOException {
AdbResponse resp = new AdbResponse();
byte[] reply = new byte[4];
read(chan, reply);
if (isOkay(reply)) {
resp.okay = true;
} else {
readDiagString = true; // look for a reason after the FAIL
resp.okay = false;
}
// not a loop -- use "while" so we can use "break"
try {
while (readDiagString) {
// length string is in next 4 bytes
byte[] lenBuf = new byte[4];
read(chan, lenBuf);
String lenStr = replyToString(lenBuf);
int len;
try {
len = Integer.parseInt(lenStr, 16);
} catch (NumberFormatException nfe) {
Log.w("ddms", "Expected digits, got '" + lenStr + "': "
+ lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " "
+ lenBuf[3]);
Log.w("ddms", "reply was " + replyToString(reply));
break;
}
byte[] msg = new byte[len];
read(chan, msg);
resp.message = replyToString(msg);
Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='"
+ resp.message + "'");
break;
}
} catch (Exception e) {
// ignore those, since it's just reading the diagnose string, the response will
// contain okay==false anyway.
}
return resp;
}
/**
* Retrieve the frame buffer from the device.
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device)
throws TimeoutException, AdbCommandRejectedException, IOException {
RawImage imageParams = new RawImage();
byte[] request = formAdbRequest("framebuffer:"); //$NON-NLS-1$
byte[] nudge = {
0
};
byte[] reply;
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
// if the device is not -1, then we first tell adb we're looking to talk
// to a specific device
setDevice(adbChan, device);
write(adbChan, request);
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (resp.okay == false) {
throw new AdbCommandRejectedException(resp.message);
}
// first the protocol version.
reply = new byte[4];
read(adbChan, reply);
ByteBuffer buf = ByteBuffer.wrap(reply);
buf.order(ByteOrder.LITTLE_ENDIAN);
int version = buf.getInt();
// get the header size (this is a count of int)
int headerSize = RawImage.getHeaderSize(version);
// read the header
reply = new byte[headerSize * 4];
read(adbChan, reply);
buf = ByteBuffer.wrap(reply);
buf.order(ByteOrder.LITTLE_ENDIAN);
// fill the RawImage with the header
if (imageParams.readHeader(version, buf) == false) {
Log.e("Screenshot", "Unsupported protocol: " + version);
return null;
}
Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size="
+ imageParams.size + ", width=" + imageParams.width
+ ", height=" + imageParams.height);
write(adbChan, nudge);
reply = new byte[imageParams.size];
read(adbChan, reply);
imageParams.data = reply;
} finally {
if (adbChan != null) {
adbChan.close();
}
}
return imageParams;
}
/**
* Executes a shell command on the device and retrieve the output. The output is
* handed to <var>rcvr</var> as it arrives.
*
* @param adbSockAddr the {@link InetSocketAddress} to adb.
* @param command the shell command to execute
* @param device the {@link IDevice} on which to execute the command.
* @param rcvr the {@link IShellOutputReceiver} that will receives the output of the shell
* command
* @param maxTimeToOutputResponse max time between command output. If more time passes
* between command output, the method will throw
* {@link ShellCommandUnresponsiveException}. A value of 0 means the method will
* wait forever for command output and never throw.
* @throws TimeoutException in case of timeout on the connection when sending the command.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output
* for a period longer than <var>maxTimeToOutputResponse</var>.
* @throws IOException in case of I/O error on the connection.
*
* @see DdmPreferences#getTimeOut()
*/
static void executeRemoteCommand(InetSocketAddress adbSockAddr,
String command, IDevice device, IShellOutputReceiver rcvr, int maxTimeToOutputResponse)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
IOException {
Log.v("ddms", "execute: running " + command);
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
// if the device is not -1, then we first tell adb we're looking to
// talk
// to a specific device
setDevice(adbChan, device);
byte[] request = formAdbRequest("shell:" + command); //$NON-NLS-1$
write(adbChan, request);
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (resp.okay == false) {
Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message);
throw new AdbCommandRejectedException(resp.message);
}
byte[] data = new byte[16384];
ByteBuffer buf = ByteBuffer.wrap(data);
int timeToResponseCount = 0;
while (true) {
int count;
if (rcvr != null && rcvr.isCancelled()) {
Log.v("ddms", "execute: cancelled");
break;
}
count = adbChan.read(buf);
if (count < 0) {
// we're at the end, we flush the output
rcvr.flush();
Log.v("ddms", "execute '" + command + "' on '" + device + "' : EOF hit. Read: "
+ count);
break;
} else if (count == 0) {
try {
int wait = WAIT_TIME * 5;
timeToResponseCount += wait;
if (maxTimeToOutputResponse > 0 &&
timeToResponseCount > maxTimeToOutputResponse) {
throw new ShellCommandUnresponsiveException();
}
Thread.sleep(wait);
} catch (InterruptedException ie) {
}
} else {
// reset timeout
timeToResponseCount = 0;
// send data to receiver if present
if (rcvr != null) {
rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position());
}
buf.rewind();
}
}
} finally {
if (adbChan != null) {
adbChan.close();
}
Log.v("ddms", "execute: returning");
}
}
/**
* Runs the Event log service on the {@link Device}, and provides its output to the
* {@link LogReceiver}.
* <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
* @param adbSockAddr the socket address to connect to adb
* @param device the Device on which to run the service
* @param rcvr the {@link LogReceiver} to receive the log output
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
public static void runEventLogService(InetSocketAddress adbSockAddr, Device device,
LogReceiver rcvr) throws TimeoutException, AdbCommandRejectedException, IOException {
runLogService(adbSockAddr, device, "events", rcvr); //$NON-NLS-1$
}
/**
* Runs a log service on the {@link Device}, and provides its output to the {@link LogReceiver}.
* <p/>This call is blocking until {@link LogReceiver#isCancelled()} returns true.
* @param adbSockAddr the socket address to connect to adb
* @param device the Device on which to run the service
* @param logName the name of the log file to output
* @param rcvr the {@link LogReceiver} to receive the log output
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName,
LogReceiver rcvr) throws TimeoutException, AdbCommandRejectedException, IOException {
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
// if the device is not -1, then we first tell adb we're looking to talk
// to a specific device
setDevice(adbChan, device);
byte[] request = formAdbRequest("log:" + logName);
write(adbChan, request);
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (resp.okay == false) {
throw new AdbCommandRejectedException(resp.message);
}
byte[] data = new byte[16384];
ByteBuffer buf = ByteBuffer.wrap(data);
while (true) {
int count;
if (rcvr != null && rcvr.isCancelled()) {
break;
}
count = adbChan.read(buf);
if (count < 0) {
break;
} else if (count == 0) {
try {
Thread.sleep(WAIT_TIME * 5);
} catch (InterruptedException ie) {
}
} else {
if (rcvr != null) {
rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position());
}
buf.rewind();
}
}
} finally {
if (adbChan != null) {
adbChan.close();
}
}
}
/**
* Creates a port forwarding between a local and a remote port.
* @param adbSockAddr the socket address to connect to adb
* @param device the device on which to do the port fowarding
* @param localPortSpec specification of the local port to forward, should be of format
* tcp:<port number>
* @param remotePortSpec specification of the remote port to forward to, one of:
* tcp:<port>
* localabstract:<unix domain socket name>
* localreserved:<unix domain socket name>
* localfilesystem:<unix domain socket name>
* dev:<character device name>
* jdwp:<process pid> (remote only)
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
public static void createForward(InetSocketAddress adbSockAddr, Device device,
String localPortSpec, String remotePortSpec)
throws TimeoutException, AdbCommandRejectedException, IOException {
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
byte[] request = formAdbRequest(String.format(
"host-serial:%1$s:forward:%2$s;%3$s", //$NON-NLS-1$
device.getSerialNumber(), localPortSpec, remotePortSpec));
write(adbChan, request);
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (resp.okay == false) {
Log.w("create-forward", "Error creating forward: " + resp.message);
throw new AdbCommandRejectedException(resp.message);
}
} finally {
if (adbChan != null) {
adbChan.close();
}
}
}
/**
* Remove a port forwarding between a local and a remote port.
* @param adbSockAddr the socket address to connect to adb
* @param device the device on which to remove the port fowarding
* @param localPortSpec specification of the local port that was forwarded, should be of format
* tcp:<port number>
* @param remotePortSpec specification of the remote port forwarded to, one of:
* tcp:<port>
* localabstract:<unix domain socket name>
* localreserved:<unix domain socket name>
* localfilesystem:<unix domain socket name>
* dev:<character device name>
* jdwp:<process pid> (remote only)
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
public static void removeForward(InetSocketAddress adbSockAddr, Device device,
String localPortSpec, String remotePortSpec)
throws TimeoutException, AdbCommandRejectedException, IOException {
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
byte[] request = formAdbRequest(String.format(
"host-serial:%1$s:killforward:%2$s;%3$s", //$NON-NLS-1$
device.getSerialNumber(), localPortSpec, remotePortSpec));
write(adbChan, request);
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (resp.okay == false) {
Log.w("remove-forward", "Error creating forward: " + resp.message);
throw new AdbCommandRejectedException(resp.message);
}
} finally {
if (adbChan != null) {
adbChan.close();
}
}
}
/**
* Checks to see if the first four bytes in "reply" are OKAY.
*/
static boolean isOkay(byte[] reply) {
return reply[0] == (byte)'O' && reply[1] == (byte)'K'
&& reply[2] == (byte)'A' && reply[3] == (byte)'Y';
}
/**
* Converts an ADB reply to a string.
*/
static String replyToString(byte[] reply) {
String result;
try {
result = new String(reply, DEFAULT_ENCODING);
} catch (UnsupportedEncodingException uee) {
uee.printStackTrace(); // not expected
result = "";
}
return result;
}
/**
* Reads from the socket until the array is filled, or no more data is coming (because
* the socket closed or the timeout expired).
* <p/>This uses the default time out value.
*
* @param chan the opened socket to read from. It must be in non-blocking
* mode for timeouts to work
* @param data the buffer to store the read data into.
* @throws TimeoutException in case of timeout on the connection.
* @throws IOException in case of I/O error on the connection.
*/
static void read(SocketChannel chan, byte[] data) throws TimeoutException, IOException {
read(chan, data, -1, DdmPreferences.getTimeOut());
}
/**
* Reads from the socket until the array is filled, the optional length
* is reached, or no more data is coming (because the socket closed or the
* timeout expired). After "timeout" milliseconds since the
* previous successful read, this will return whether or not new data has
* been found.
*
* @param chan the opened socket to read from. It must be in non-blocking
* mode for timeouts to work
* @param data the buffer to store the read data into.
* @param length the length to read or -1 to fill the data buffer completely
* @param timeout The timeout value. A timeout of zero means "wait forever".
*/
static void read(SocketChannel chan, byte[] data, int length, int timeout)
throws TimeoutException, IOException {
ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
int numWaits = 0;
while (buf.position() != buf.limit()) {
int count;
count = chan.read(buf);
if (count < 0) {
Log.d("ddms", "read: channel EOF");
throw new IOException("EOF");
} else if (count == 0) {
// TODO: need more accurate timeout?
if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
Log.d("ddms", "read: timeout");
throw new TimeoutException();
}
// non-blocking spin
try {
Thread.sleep(WAIT_TIME);
} catch (InterruptedException ie) {
}
numWaits++;
} else {
numWaits = 0;
}
}
}
/**
* Write until all data in "data" is written or the connection fails or times out.
* <p/>This uses the default time out value.
* @param chan the opened socket to write to.
* @param data the buffer to send.
* @throws TimeoutException in case of timeout on the connection.
* @throws IOException in case of I/O error on the connection.
*/
static void write(SocketChannel chan, byte[] data) throws TimeoutException, IOException {
write(chan, data, -1, DdmPreferences.getTimeOut());
}
/**
* Write until all data in "data" is written, the optional length is reached,
* the timeout expires, or the connection fails. Returns "true" if all
* data was written.
* @param chan the opened socket to write to.
* @param data the buffer to send.
* @param length the length to write or -1 to send the whole buffer.
* @param timeout The timeout value. A timeout of zero means "wait forever".
* @throws TimeoutException in case of timeout on the connection.
* @throws IOException in case of I/O error on the connection.
*/
static void write(SocketChannel chan, byte[] data, int length, int timeout)
throws TimeoutException, IOException {
ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length);
int numWaits = 0;
while (buf.position() != buf.limit()) {
int count;
count = chan.write(buf);
if (count < 0) {
Log.d("ddms", "write: channel EOF");
throw new IOException("channel EOF");
} else if (count == 0) {
// TODO: need more accurate timeout?
if (timeout != 0 && numWaits * WAIT_TIME > timeout) {
Log.d("ddms", "write: timeout");
throw new TimeoutException();
}
// non-blocking spin
try {
Thread.sleep(WAIT_TIME);
} catch (InterruptedException ie) {
}
numWaits++;
} else {
numWaits = 0;
}
}
}
/**
* tells adb to talk to a specific device
*
* @param adbChan the socket connection to adb
* @param device The device to talk to.
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
static void setDevice(SocketChannel adbChan, IDevice device)
throws TimeoutException, AdbCommandRejectedException, IOException {
// if the device is not -1, then we first tell adb we're looking to talk
// to a specific device
if (device != null) {
String msg = "host:transport:" + device.getSerialNumber(); //$NON-NLS-1$
byte[] device_query = formAdbRequest(msg);
write(adbChan, device_query);
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (resp.okay == false) {
throw new AdbCommandRejectedException(resp.message,
true/*errorDuringDeviceSelection*/);
}
}
}
/**
* Reboot the device.
*
* @param into what to reboot into (recovery, bootloader). Or null to just reboot.
* @throws TimeoutException in case of timeout on the connection.
* @throws AdbCommandRejectedException if adb rejects the command
* @throws IOException in case of I/O error on the connection.
*/
public static void reboot(String into, InetSocketAddress adbSockAddr,
Device device) throws TimeoutException, AdbCommandRejectedException, IOException {
byte[] request;
if (into == null) {
request = formAdbRequest("reboot:"); //$NON-NLS-1$
} else {
request = formAdbRequest("reboot:" + into); //$NON-NLS-1$
}
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
// if the device is not -1, then we first tell adb we're looking to talk
// to a specific device
setDevice(adbChan, device);
write(adbChan, request);
} finally {
if (adbChan != null) {
adbChan.close();
}
}
}
}