auto import from //depot/cupcake/@135843

This commit is contained in:
The Android Open Source Project
2009-03-03 19:29:09 -08:00
parent d4aee0c0ca
commit 52d4c30ca5
2386 changed files with 299112 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
# Copyright 2007 The Android Open Source Project
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE := ddmlib
include $(BUILD_HOST_JAVA_LIBRARY)

View File

@@ -0,0 +1,714 @@
/*
* 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.LogLevel;
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
public static final int STD_TIMEOUT = 5000; // standard delay, 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() {
// ioSuccess = okay = timeout = false;
message = "";
}
public boolean ioSuccess; // read all expected data, no timeoutes
public boolean okay; // first 4 bytes in response were "OKAY"?
public boolean timeout; // TODO: implement
public String message; // diagnostic string
}
/**
* 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
*/
public static SocketChannel open(InetSocketAddress adbSockAddr,
Device device, int devicePort) throws 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 = createAdbForwardRequest(null, devicePort);
// Log.hexDump(req);
if (write(adbChan, req) == false)
throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$
AdbResponse resp = readAdbResponse(adbChan, false);
if (!resp.okay)
throw new IOException("connection request rejected"); //$NON-NLS-1$
adbChan.configureBlocking(true);
} catch (IOException ioe) {
adbChan.close();
throw ioe;
}
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.
*/
public static SocketChannel createPassThroughConnection(InetSocketAddress adbSockAddr,
Device device, int pid) throws 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);
if (write(adbChan, req) == false)
throw new IOException("failed submitting request to ADB"); //$NON-NLS-1$
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (!resp.okay)
throw new IOException("connection request rejected: " + resp.message); //$NON-NLS-1$
adbChan.configureBlocking(true);
} catch (IOException ioe) {
adbChan.close();
throw ioe;
}
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.
*/
static AdbResponse readAdbResponse(SocketChannel chan, boolean readDiagString)
throws IOException {
AdbResponse resp = new AdbResponse();
byte[] reply = new byte[4];
if (read(chan, reply) == false) {
return resp;
}
resp.ioSuccess = true;
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"
while (readDiagString) {
// length string is in next 4 bytes
byte[] lenBuf = new byte[4];
if (read(chan, lenBuf) == false) {
Log.w("ddms", "Expected diagnostic string not found");
break;
}
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];
if (read(chan, msg) == false) {
Log.w("ddms", "Failed reading diagnostic string, len=" + len);
break;
}
resp.message = replyToString(msg);
Log.v("ddms", "Got reply '" + replyToString(reply) + "', diag='"
+ resp.message + "'");
break;
}
return resp;
}
/**
* Retrieve the frame buffer from the device.
*/
public static RawImage getFrameBuffer(InetSocketAddress adbSockAddr, Device device)
throws 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);
if (write(adbChan, request) == false)
throw new IOException("failed asking for frame buffer");
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (!resp.ioSuccess || !resp.okay) {
Log.w("ddms", "Got timeout or unhappy response from ADB fb req: "
+ resp.message);
adbChan.close();
return null;
}
reply = new byte[16];
if (read(adbChan, reply) == false) {
Log.w("ddms", "got partial reply from ADB fb:");
Log.hexDump("ddms", LogLevel.WARN, reply, 0, reply.length);
adbChan.close();
return null;
}
ByteBuffer buf = ByteBuffer.wrap(reply);
buf.order(ByteOrder.LITTLE_ENDIAN);
imageParams.bpp = buf.getInt();
imageParams.size = buf.getInt();
imageParams.width = buf.getInt();
imageParams.height = buf.getInt();
Log.d("ddms", "image params: bpp=" + imageParams.bpp + ", size="
+ imageParams.size + ", width=" + imageParams.width
+ ", height=" + imageParams.height);
if (write(adbChan, nudge) == false)
throw new IOException("failed nudging");
reply = new byte[imageParams.size];
if (read(adbChan, reply) == false) {
Log.w("ddms", "got truncated reply from ADB fb data");
adbChan.close();
return null;
}
imageParams.data = reply;
} finally {
if (adbChan != null) {
adbChan.close();
}
}
return imageParams;
}
/**
* Execute a command on the device and retrieve the output. The output is
* handed to "rcvr" as it arrives.
*/
public static void executeRemoteCommand(InetSocketAddress adbSockAddr,
String command, Device device, IShellOutputReceiver rcvr)
throws 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$
if (write(adbChan, request) == false)
throw new IOException("failed submitting shell command");
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (!resp.ioSuccess || !resp.okay) {
Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message);
throw new IOException("sad result from adb: " + resp.message);
}
byte[] data = new byte[16384];
ByteBuffer buf = ByteBuffer.wrap(data);
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 {
Thread.sleep(WAIT_TIME * 5);
} catch (InterruptedException ie) {
}
} else {
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}.
* @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 IOException
*/
public static void runEventLogService(InetSocketAddress adbSockAddr, Device device,
LogReceiver rcvr) throws 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}.
* @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 IOException
*/
public static void runLogService(InetSocketAddress adbSockAddr, Device device, String logName,
LogReceiver rcvr) throws 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);
if (write(adbChan, request) == false) {
throw new IOException("failed to submit the log command");
}
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (!resp.ioSuccess || !resp.okay) {
throw new IOException("Device rejected log command: " + 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 localPort the local port to forward
* @param remotePort the remote port.
* @return <code>true</code> if success.
* @throws IOException
*/
public static boolean createForward(InetSocketAddress adbSockAddr, Device device, int localPort,
int remotePort) throws IOException {
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
byte[] request = formAdbRequest(String.format(
"host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$
device.serialNumber, localPort, remotePort));
if (write(adbChan, request) == false) {
throw new IOException("failed to submit the forward command.");
}
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (!resp.ioSuccess || !resp.okay) {
throw new IOException("Device rejected command: " + resp.message);
}
} finally {
if (adbChan != null) {
adbChan.close();
}
}
return true;
}
/**
* 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 localPort the local port of the forward
* @param remotePort the remote port.
* @return <code>true</code> if success.
* @throws IOException
*/
public static boolean removeForward(InetSocketAddress adbSockAddr, Device device, int localPort,
int remotePort) throws IOException {
SocketChannel adbChan = null;
try {
adbChan = SocketChannel.open(adbSockAddr);
adbChan.configureBlocking(false);
byte[] request = formAdbRequest(String.format(
"host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$
device.serialNumber, localPort, remotePort));
if (!write(adbChan, request)) {
throw new IOException("failed to submit the remove forward command.");
}
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (!resp.ioSuccess || !resp.okay) {
throw new IOException("Device rejected command: " + resp.message);
}
} finally {
if (adbChan != null) {
adbChan.close();
}
}
return true;
}
/**
* 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).
*
* @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.
* @return "true" if all data was read.
* @throws IOException
*/
static boolean read(SocketChannel chan, byte[] data) {
try {
read(chan, data, -1, STD_TIMEOUT);
} catch (IOException e) {
Log.d("ddms", "readAll: IOException: " + e.getMessage());
return false;
}
return true;
}
/**
* 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".
* @throws IOException
*/
static void read(SocketChannel chan, byte[] data, int length, int timeout) throws 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.i("ddms", "read: timeout");
throw new IOException("timeout");
}
// 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.
* @param chan the opened socket to write to.
* @param data the buffer to send.
* @return "true" if all data was written.
*/
static boolean write(SocketChannel chan, byte[] data) {
try {
write(chan, data, -1, STD_TIMEOUT);
} catch (IOException e) {
Log.e("ddms", e);
return false;
}
return true;
}
/**
* 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 IOException
*/
static void write(SocketChannel chan, byte[] data, int length, int timeout)
throws 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.i("ddms", "write: timeout");
throw new IOException("timeout");
}
// 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 IOException
*/
static void setDevice(SocketChannel adbChan, Device device)
throws 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.serialNumber; //$NON-NLS-1$
byte[] device_query = formAdbRequest(msg);
if (write(adbChan, device_query) == false)
throw new IOException("failed submitting device (" + device +
") request to ADB");
AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);
if (!resp.okay)
throw new IOException("device (" + device +
") request rejected: " + resp.message);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2008 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;
/**
* Holds an Allocation information.
*/
public class AllocationInfo implements Comparable<AllocationInfo>, IStackTraceInfo {
private String mAllocatedClass;
private int mAllocationSize;
private short mThreadId;
private StackTraceElement[] mStackTrace;
/*
* Simple constructor.
*/
AllocationInfo(String allocatedClass, int allocationSize,
short threadId, StackTraceElement[] stackTrace) {
mAllocatedClass = allocatedClass;
mAllocationSize = allocationSize;
mThreadId = threadId;
mStackTrace = stackTrace;
}
/**
* Returns the name of the allocated class.
*/
public String getAllocatedClass() {
return mAllocatedClass;
}
/**
* Returns the size of the allocation.
*/
public int getSize() {
return mAllocationSize;
}
/**
* Returns the id of the thread that performed the allocation.
*/
public short getThreadId() {
return mThreadId;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IStackTraceInfo#getStackTrace()
*/
public StackTraceElement[] getStackTrace() {
return mStackTrace;
}
public int compareTo(AllocationInfo otherAlloc) {
return otherAlloc.mAllocationSize - mAllocationSize;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java
**
** Copyright 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;
/**
* Thrown if the contents of a packet are bad.
*/
@SuppressWarnings("serial")
class BadPacketException extends RuntimeException {
public BadPacketException()
{
super();
}
public BadPacketException(String msg)
{
super(msg);
}
}

View File

@@ -0,0 +1,222 @@
/*
* 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.DebugPortManager.IDebugPortProvider;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Subclass this with a class that handles one or more chunk types.
*/
abstract class ChunkHandler {
public static final int CHUNK_HEADER_LEN = 8; // 4-byte type, 4-byte len
public static final ByteOrder CHUNK_ORDER = ByteOrder.BIG_ENDIAN;
public static final int CHUNK_FAIL = type("FAIL");
ChunkHandler() {}
/**
* Client is ready. The monitor thread calls this method on all
* handlers when the client is determined to be DDM-aware (usually
* after receiving a HELO response.)
*
* The handler can use this opportunity to initialize client-side
* activity. Because there's a fair chance we'll want to send a
* message to the client, this method can throw an IOException.
*/
abstract void clientReady(Client client) throws IOException;
/**
* Client has gone away. Can be used to clean up any resources
* associated with this client connection.
*/
abstract void clientDisconnected(Client client);
/**
* Handle an incoming chunk. The data, of chunk type "type", begins
* at the start of "data" and continues to data.limit().
*
* If "isReply" is set, then "msgId" will be the ID of the request
* we sent to the client. Otherwise, it's the ID generated by the
* client for this event. Note that it's possible to receive chunks
* in reply packets for which we are not registered.
*
* The handler may not modify the contents of "data".
*/
abstract void handleChunk(Client client, int type,
ByteBuffer data, boolean isReply, int msgId);
/**
* Handle chunks not recognized by handlers. The handleChunk() method
* in sub-classes should call this if the chunk type isn't recognized.
*/
protected void handleUnknownChunk(Client client, int type,
ByteBuffer data, boolean isReply, int msgId) {
if (type == CHUNK_FAIL) {
int errorCode, msgLen;
String msg;
errorCode = data.getInt();
msgLen = data.getInt();
msg = getString(data, msgLen);
Log.w("ddms", "WARNING: failure code=" + errorCode + " msg=" + msg);
} else {
Log.w("ddms", "WARNING: received unknown chunk " + name(type)
+ ": len=" + data.limit() + ", reply=" + isReply
+ ", msgId=0x" + Integer.toHexString(msgId));
}
Log.w("ddms", " client " + client + ", handler " + this);
}
/**
* Utility function to copy a String out of a ByteBuffer.
*
* This is here because multiple chunk handlers can make use of it,
* and there's nowhere better to put it.
*/
static String getString(ByteBuffer buf, int len) {
char[] data = new char[len];
for (int i = 0; i < len; i++)
data[i] = buf.getChar();
return new String(data);
}
/**
* Utility function to copy a String into a ByteBuffer.
*/
static void putString(ByteBuffer buf, String str) {
int len = str.length();
for (int i = 0; i < len; i++)
buf.putChar(str.charAt(i));
}
/**
* Convert a 4-character string to a 32-bit type.
*/
static int type(String typeName) {
int val = 0;
if (typeName.length() != 4) {
Log.e("ddms", "Type name must be 4 letter long");
throw new RuntimeException("Type name must be 4 letter long");
}
for (int i = 0; i < 4; i++) {
val <<= 8;
val |= (byte) typeName.charAt(i);
}
return val;
}
/**
* Convert an integer type to a 4-character string.
*/
static String name(int type) {
char[] ascii = new char[4];
ascii[0] = (char) ((type >> 24) & 0xff);
ascii[1] = (char) ((type >> 16) & 0xff);
ascii[2] = (char) ((type >> 8) & 0xff);
ascii[3] = (char) (type & 0xff);
return new String(ascii);
}
/**
* Allocate a ByteBuffer with enough space to hold the JDWP packet
* header and one chunk header in addition to the demands of the
* chunk being created.
*
* "maxChunkLen" indicates the size of the chunk contents only.
*/
static ByteBuffer allocBuffer(int maxChunkLen) {
ByteBuffer buf =
ByteBuffer.allocate(JdwpPacket.JDWP_HEADER_LEN + 8 +maxChunkLen);
buf.order(CHUNK_ORDER);
return buf;
}
/**
* Return the slice of the JDWP packet buffer that holds just the
* chunk data.
*/
static ByteBuffer getChunkDataBuf(ByteBuffer jdwpBuf) {
ByteBuffer slice;
assert jdwpBuf.position() == 0;
jdwpBuf.position(JdwpPacket.JDWP_HEADER_LEN + CHUNK_HEADER_LEN);
slice = jdwpBuf.slice();
slice.order(CHUNK_ORDER);
jdwpBuf.position(0);
return slice;
}
/**
* Write the chunk header at the start of the chunk.
*
* Pass in the byte buffer returned by JdwpPacket.getPayload().
*/
static void finishChunkPacket(JdwpPacket packet, int type, int chunkLen) {
ByteBuffer buf = packet.getPayload();
buf.putInt(0x00, type);
buf.putInt(0x04, chunkLen);
packet.finishPacket(CHUNK_HEADER_LEN + chunkLen);
}
/**
* Check that the client is opened with the proper debugger port for the
* specified application name, and if not, reopen it.
* @param client
* @param uiThread
* @param appName
* @return
*/
protected static Client checkDebuggerPortForAppName(Client client, String appName) {
IDebugPortProvider provider = DebugPortManager.getProvider();
if (provider != null) {
Device device = client.getDevice();
int newPort = provider.getPort(device, appName);
if (newPort != IDebugPortProvider.NO_STATIC_PORT &&
newPort != client.getDebuggerListenPort()) {
AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
if (bridge != null) {
DeviceMonitor deviceMonitor = bridge.getDeviceMonitor();
if (deviceMonitor != null) {
deviceMonitor.addClientToDropAndReopen(client, newPort);
client = null;
}
}
}
}
return client;
}
}

View File

@@ -0,0 +1,768 @@
/*
* 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.DebugPortManager.IDebugPortProvider;
import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
/**
* This represents a single client, usually a DAlvik VM process.
* <p/>This class gives access to basic client information, as well as methods to perform actions
* on the client.
* <p/>More detailed information, usually updated in real time, can be access through the
* {@link ClientData} class. Each <code>Client</code> object has its own <code>ClientData</code>
* accessed through {@link #getClientData()}.
*/
public class Client {
private static final int SERVER_PROTOCOL_VERSION = 1;
/** Client change bit mask: application name change */
public static final int CHANGE_NAME = 0x0001;
/** Client change bit mask: debugger interest change */
public static final int CHANGE_DEBUGGER_INTEREST = 0x0002;
/** Client change bit mask: debugger port change */
public static final int CHANGE_PORT = 0x0004;
/** Client change bit mask: thread update flag change */
public static final int CHANGE_THREAD_MODE = 0x0008;
/** Client change bit mask: thread data updated */
public static final int CHANGE_THREAD_DATA = 0x0010;
/** Client change bit mask: heap update flag change */
public static final int CHANGE_HEAP_MODE = 0x0020;
/** Client change bit mask: head data updated */
public static final int CHANGE_HEAP_DATA = 0x0040;
/** Client change bit mask: native heap data updated */
public static final int CHANGE_NATIVE_HEAP_DATA = 0x0080;
/** Client change bit mask: thread stack trace updated */
public static final int CHANGE_THREAD_STACKTRACE = 0x0100;
/** Client change bit mask: allocation information updated */
public static final int CHANGE_HEAP_ALLOCATIONS = 0x0200;
/** Client change bit mask: allocation information updated */
public static final int CHANGE_HEAP_ALLOCATION_STATUS = 0x0400;
/** Client change bit mask: combination of {@link Client#CHANGE_NAME},
* {@link Client#CHANGE_DEBUGGER_INTEREST}, and {@link Client#CHANGE_PORT}.
*/
public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_INTEREST | CHANGE_PORT;
private SocketChannel mChan;
// debugger we're associated with, if any
private Debugger mDebugger;
private int mDebuggerListenPort;
// list of IDs for requests we have sent to the client
private HashMap<Integer,ChunkHandler> mOutstandingReqs;
// chunk handlers stash state data in here
private ClientData mClientData;
// User interface state. Changing the value causes a message to be
// sent to the client.
private boolean mThreadUpdateEnabled;
private boolean mHeapUpdateEnabled;
/*
* Read/write buffers. We can get large quantities of data from the
* client, e.g. the response to a "give me the list of all known classes"
* request from the debugger. Requests from the debugger, and from us,
* are much smaller.
*
* Pass-through debugger traffic is sent without copying. "mWriteBuffer"
* is only used for data generated within Client.
*/
private static final int INITIAL_BUF_SIZE = 2*1024;
private static final int MAX_BUF_SIZE = 200*1024*1024;
private ByteBuffer mReadBuffer;
private static final int WRITE_BUF_SIZE = 256;
private ByteBuffer mWriteBuffer;
private Device mDevice;
private int mConnState;
private static final int ST_INIT = 1;
private static final int ST_NOT_JDWP = 2;
private static final int ST_AWAIT_SHAKE = 10;
private static final int ST_NEED_DDM_PKT = 11;
private static final int ST_NOT_DDM = 12;
private static final int ST_READY = 13;
private static final int ST_ERROR = 20;
private static final int ST_DISCONNECTED = 21;
/**
* Create an object for a new client connection.
*
* @param device the device this client belongs to
* @param chan the connected {@link SocketChannel}.
* @param pid the client pid.
*/
Client(Device device, SocketChannel chan, int pid) {
mDevice = device;
mChan = chan;
mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE);
mWriteBuffer = ByteBuffer.allocate(WRITE_BUF_SIZE);
mOutstandingReqs = new HashMap<Integer,ChunkHandler>();
mConnState = ST_INIT;
mClientData = new ClientData(pid);
mThreadUpdateEnabled = DdmPreferences.getInitialThreadUpdate();
mHeapUpdateEnabled = DdmPreferences.getInitialHeapUpdate();
}
/**
* Returns a string representation of the {@link Client} object.
*/
@Override
public String toString() {
return "[Client pid: " + mClientData.getPid() + "]";
}
/**
* Returns the {@link Device} on which this Client is running.
*/
public Device getDevice() {
return mDevice;
}
/**
* Returns the debugger port for this client.
*/
public int getDebuggerListenPort() {
return mDebuggerListenPort;
}
/**
* Returns <code>true</code> if the client VM is DDM-aware.
*
* Calling here is only allowed after the connection has been
* established.
*/
public boolean isDdmAware() {
switch (mConnState) {
case ST_INIT:
case ST_NOT_JDWP:
case ST_AWAIT_SHAKE:
case ST_NEED_DDM_PKT:
case ST_NOT_DDM:
case ST_ERROR:
case ST_DISCONNECTED:
return false;
case ST_READY:
return true;
default:
assert false;
return false;
}
}
/**
* Returns <code>true</code> if a debugger is currently attached to the client.
*/
public boolean isDebuggerAttached() {
return mDebugger.isDebuggerAttached();
}
/**
* Return the Debugger object associated with this client.
*/
Debugger getDebugger() {
return mDebugger;
}
/**
* Returns the {@link ClientData} object containing this client information.
*/
public ClientData getClientData() {
return mClientData;
}
/**
* Forces the client to execute its garbage collector.
*/
public void executeGarbageCollector() {
try {
HandleHeap.sendHPGC(this);
} catch (IOException ioe) {
Log.w("ddms", "Send of HPGC message failed");
// ignore
}
}
/**
* Enables or disables the thread update.
* <p/>If <code>true</code> the VM will be able to send thread information. Thread information
* must be requested with {@link #requestThreadUpdate()}.
* @param enabled the enable flag.
*/
public void setThreadUpdateEnabled(boolean enabled) {
mThreadUpdateEnabled = enabled;
if (enabled == false) {
mClientData.clearThreads();
}
try {
HandleThread.sendTHEN(this, enabled);
} catch (IOException ioe) {
// ignore it here; client will clean up shortly
ioe.printStackTrace();
}
update(CHANGE_THREAD_MODE);
}
/**
* Returns whether the thread update is enabled.
*/
public boolean isThreadUpdateEnabled() {
return mThreadUpdateEnabled;
}
/**
* Sends a thread update request. This is asynchronous.
* <p/>The thread info can be accessed by {@link ClientData#getThreads()}. The notification
* that the new data is available will be received through
* {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
* containing the mask {@link #CHANGE_THREAD_DATA}.
*/
public void requestThreadUpdate() {
HandleThread.requestThreadUpdate(this);
}
/**
* Sends a thread stack trace update request. This is asynchronous.
* <p/>The thread info can be accessed by {@link ClientData#getThreads()} and
* {@link ThreadInfo#getStackTrace()}.
* <p/>The notification that the new data is available
* will be received through {@link IClientChangeListener#clientChanged(Client, int)}
* with a <code>changeMask</code> containing the mask {@link #CHANGE_THREAD_STACKTRACE}.
*/
public void requestThreadStackTrace(int threadId) {
HandleThread.requestThreadStackCallRefresh(this, threadId);
}
/**
* Enables or disables the heap update.
* <p/>If <code>true</code>, any GC will cause the client to send its heap information.
* <p/>The heap information can be accessed by {@link ClientData#getVmHeapData()}.
* <p/>The notification that the new data is available
* will be received through {@link IClientChangeListener#clientChanged(Client, int)}
* with a <code>changeMask</code> containing the value {@link #CHANGE_HEAP_DATA}.
* @param enabled the enable flag
*/
public void setHeapUpdateEnabled(boolean enabled) {
mHeapUpdateEnabled = enabled;
try {
HandleHeap.sendHPIF(this,
enabled ? HandleHeap.HPIF_WHEN_EVERY_GC : HandleHeap.HPIF_WHEN_NEVER);
HandleHeap.sendHPSG(this,
enabled ? HandleHeap.WHEN_GC : HandleHeap.WHEN_DISABLE,
HandleHeap.WHAT_MERGE);
} catch (IOException ioe) {
// ignore it here; client will clean up shortly
}
update(CHANGE_HEAP_MODE);
}
/**
* Returns whether the heap update is enabled.
* @see #setHeapUpdateEnabled(boolean)
*/
public boolean isHeapUpdateEnabled() {
return mHeapUpdateEnabled;
}
/**
* Sends a native heap update request. this is asynchronous.
* <p/>The native heap info can be accessed by {@link ClientData#getNativeAllocationList()}.
* The notification that the new data is available will be received through
* {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
* containing the mask {@link #CHANGE_NATIVE_HEAP_DATA}.
*/
public boolean requestNativeHeapInformation() {
try {
HandleNativeHeap.sendNHGT(this);
return true;
} catch (IOException e) {
Log.e("ddmlib", e);
}
return false;
}
/**
* Enables or disables the Allocation tracker for this client.
* <p/>If enabled, the VM will start tracking allocation informations. A call to
* {@link #requestAllocationDetails()} will make the VM sends the information about all the
* allocations that happened between the enabling and the request.
* @param enable
* @see #requestAllocationDetails()
*/
public void enableAllocationTracker(boolean enable) {
try {
HandleHeap.sendREAE(this, enable);
} catch (IOException e) {
Log.e("ddmlib", e);
}
}
/**
* Sends a request to the VM to send the enable status of the allocation tracking.
* This is asynchronous.
* <p/>The allocation status can be accessed by {@link ClientData#getAllocationStatus()}.
* The notification that the new status is available will be received through
* {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
* containing the mask {@link #CHANGE_HEAP_ALLOCATION_STATUS}.
*/
public void requestAllocationStatus() {
try {
HandleHeap.sendREAQ(this);
} catch (IOException e) {
Log.e("ddmlib", e);
}
}
/**
* Sends a request to the VM to send the information about all the allocations that have
* happened since the call to {@link #enableAllocationTracker(boolean)} with <var>enable</var>
* set to <code>null</code>. This is asynchronous.
* <p/>The allocation information can be accessed by {@link ClientData#getAllocations()}.
* The notification that the new data is available will be received through
* {@link IClientChangeListener#clientChanged(Client, int)} with a <code>changeMask</code>
* containing the mask {@link #CHANGE_HEAP_ALLOCATIONS}.
*/
public void requestAllocationDetails() {
try {
HandleHeap.sendREAL(this);
} catch (IOException e) {
Log.e("ddmlib", e);
}
}
/**
* Sends a kill message to the VM.
*/
public void kill() {
try {
HandleExit.sendEXIT(this, 1);
} catch (IOException ioe) {
Log.w("ddms", "Send of EXIT message failed");
// ignore
}
}
/**
* Registers the client with a Selector.
*/
void register(Selector sel) throws IOException {
if (mChan != null) {
mChan.register(sel, SelectionKey.OP_READ, this);
}
}
/**
* Sets the client to accept debugger connection on the "selected debugger port".
*
* @see AndroidDebugBridge#setSelectedClient(Client)
* @see DdmPreferences#setSelectedDebugPort(int)
*/
public void setAsSelectedClient() {
MonitorThread monitorThread = MonitorThread.getInstance();
if (monitorThread != null) {
monitorThread.setSelectedClient(this);
}
}
/**
* Returns whether this client is the current selected client, accepting debugger connection
* on the "selected debugger port".
*
* @see #setAsSelectedClient()
* @see AndroidDebugBridge#setSelectedClient(Client)
* @see DdmPreferences#setSelectedDebugPort(int)
*/
public boolean isSelectedClient() {
MonitorThread monitorThread = MonitorThread.getInstance();
if (monitorThread != null) {
return monitorThread.getSelectedClient() == this;
}
return false;
}
/**
* Tell the client to open a server socket channel and listen for
* connections on the specified port.
*/
void listenForDebugger(int listenPort) throws IOException {
mDebuggerListenPort = listenPort;
mDebugger = new Debugger(this, listenPort);
}
/**
* Initiate the JDWP handshake.
*
* On failure, closes the socket and returns false.
*/
boolean sendHandshake() {
assert mWriteBuffer.position() == 0;
try {
// assume write buffer can hold 14 bytes
JdwpPacket.putHandshake(mWriteBuffer);
int expectedLen = mWriteBuffer.position();
mWriteBuffer.flip();
if (mChan.write(mWriteBuffer) != expectedLen)
throw new IOException("partial handshake write");
}
catch (IOException ioe) {
Log.e("ddms-client", "IO error during handshake: " + ioe.getMessage());
mConnState = ST_ERROR;
close(true /* notify */);
return false;
}
finally {
mWriteBuffer.clear();
}
mConnState = ST_AWAIT_SHAKE;
return true;
}
/**
* Send a non-DDM packet to the client.
*
* Equivalent to sendAndConsume(packet, null).
*/
void sendAndConsume(JdwpPacket packet) throws IOException {
sendAndConsume(packet, null);
}
/**
* Send a DDM packet to the client.
*
* Ideally, we can do this with a single channel write. If that doesn't
* happen, we have to prevent anybody else from writing to the channel
* until this packet completes, so we synchronize on the channel.
*
* Another goal is to avoid unnecessary buffer copies, so we write
* directly out of the JdwpPacket's ByteBuffer.
*/
void sendAndConsume(JdwpPacket packet, ChunkHandler replyHandler)
throws IOException {
if (mChan == null) {
// can happen for e.g. THST packets
Log.v("ddms", "Not sending packet -- client is closed");
return;
}
if (replyHandler != null) {
/*
* Add the ID to the list of outstanding requests. We have to do
* this before sending the packet, in case the response comes back
* before our thread returns from the packet-send function.
*/
addRequestId(packet.getId(), replyHandler);
}
synchronized (mChan) {
try {
packet.writeAndConsume(mChan);
}
catch (IOException ioe) {
removeRequestId(packet.getId());
throw ioe;
}
}
}
/**
* Forward the packet to the debugger (if still connected to one).
*
* Consumes the packet.
*/
void forwardPacketToDebugger(JdwpPacket packet)
throws IOException {
Debugger dbg = mDebugger;
if (dbg == null) {
Log.i("ddms", "Discarding packet");
packet.consume();
} else {
dbg.sendAndConsume(packet);
}
}
/**
* Read data from our channel.
*
* This is called when data is known to be available, and we don't yet
* have a full packet in the buffer. If the buffer is at capacity,
* expand it.
*/
void read()
throws IOException, BufferOverflowException {
int count;
if (mReadBuffer.position() == mReadBuffer.capacity()) {
if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) {
Log.e("ddms", "Exceeded MAX_BUF_SIZE!");
throw new BufferOverflowException();
}
Log.d("ddms", "Expanding read buffer to "
+ mReadBuffer.capacity() * 2);
ByteBuffer newBuffer = ByteBuffer.allocate(mReadBuffer.capacity() * 2);
// copy entire buffer to new buffer
mReadBuffer.position(0);
newBuffer.put(mReadBuffer); // leaves "position" at end of copied
mReadBuffer = newBuffer;
}
count = mChan.read(mReadBuffer);
if (count < 0)
throw new IOException("read failed");
if (Log.Config.LOGV) Log.v("ddms", "Read " + count + " bytes from " + this);
//Log.hexDump("ddms", Log.DEBUG, mReadBuffer.array(),
// mReadBuffer.arrayOffset(), mReadBuffer.position());
}
/**
* Return information for the first full JDWP packet in the buffer.
*
* If we don't yet have a full packet, return null.
*
* If we haven't yet received the JDWP handshake, we watch for it here
* and consume it without admitting to have done so. Upon receipt
* we send out the "HELO" message, which is why this can throw an
* IOException.
*/
JdwpPacket getJdwpPacket() throws IOException {
/*
* On entry, the data starts at offset 0 and ends at "position".
* "limit" is set to the buffer capacity.
*/
if (mConnState == ST_AWAIT_SHAKE) {
/*
* The first thing we get from the client is a response to our
* handshake. It doesn't look like a packet, so we have to
* handle it specially.
*/
int result;
result = JdwpPacket.findHandshake(mReadBuffer);
//Log.v("ddms", "findHand: " + result);
switch (result) {
case JdwpPacket.HANDSHAKE_GOOD:
Log.i("ddms",
"Good handshake from client, sending HELO to " + mClientData.getPid());
JdwpPacket.consumeHandshake(mReadBuffer);
mConnState = ST_NEED_DDM_PKT;
HandleHello.sendHELO(this, SERVER_PROTOCOL_VERSION);
// see if we have another packet in the buffer
return getJdwpPacket();
case JdwpPacket.HANDSHAKE_BAD:
Log.i("ddms", "Bad handshake from client");
if (MonitorThread.getInstance().getRetryOnBadHandshake()) {
// we should drop the client, but also attempt to reopen it.
// This is done by the DeviceMonitor.
mDevice.getMonitor().addClientToDropAndReopen(this,
IDebugPortProvider.NO_STATIC_PORT);
} else {
// mark it as bad, close the socket, and don't retry
mConnState = ST_NOT_JDWP;
close(true /* notify */);
}
break;
case JdwpPacket.HANDSHAKE_NOTYET:
Log.i("ddms", "No handshake from client yet.");
break;
default:
Log.e("ddms", "Unknown packet while waiting for client handshake");
}
return null;
} else if (mConnState == ST_NEED_DDM_PKT ||
mConnState == ST_NOT_DDM ||
mConnState == ST_READY) {
/*
* Normal packet traffic.
*/
if (mReadBuffer.position() != 0) {
if (Log.Config.LOGV) Log.v("ddms",
"Checking " + mReadBuffer.position() + " bytes");
}
return JdwpPacket.findPacket(mReadBuffer);
} else {
/*
* Not expecting data when in this state.
*/
Log.e("ddms", "Receiving data in state = " + mConnState);
}
return null;
}
/*
* Add the specified ID to the list of request IDs for which we await
* a response.
*/
private void addRequestId(int id, ChunkHandler handler) {
synchronized (mOutstandingReqs) {
if (Log.Config.LOGV) Log.v("ddms",
"Adding req 0x" + Integer.toHexString(id) +" to set");
mOutstandingReqs.put(id, handler);
}
}
/*
* Remove the specified ID from the list, if present.
*/
void removeRequestId(int id) {
synchronized (mOutstandingReqs) {
if (Log.Config.LOGV) Log.v("ddms",
"Removing req 0x" + Integer.toHexString(id) + " from set");
mOutstandingReqs.remove(id);
}
//Log.w("ddms", "Request " + Integer.toHexString(id)
// + " could not be removed from " + this);
}
/**
* Determine whether this is a response to a request we sent earlier.
* If so, return the ChunkHandler responsible.
*/
ChunkHandler isResponseToUs(int id) {
synchronized (mOutstandingReqs) {
ChunkHandler handler = mOutstandingReqs.get(id);
if (handler != null) {
if (Log.Config.LOGV) Log.v("ddms",
"Found 0x" + Integer.toHexString(id)
+ " in request set - " + handler);
return handler;
}
}
return null;
}
/**
* An earlier request resulted in a failure. This is the expected
* response to a HELO message when talking to a non-DDM client.
*/
void packetFailed(JdwpPacket reply) {
if (mConnState == ST_NEED_DDM_PKT) {
Log.i("ddms", "Marking " + this + " as non-DDM client");
mConnState = ST_NOT_DDM;
} else if (mConnState != ST_NOT_DDM) {
Log.w("ddms", "WEIRD: got JDWP failure packet on DDM req");
}
}
/**
* The MonitorThread calls this when it sees a DDM request or reply.
* If we haven't seen a DDM packet before, we advance the state to
* ST_READY and return "false". Otherwise, just return true.
*
* The idea is to let the MonitorThread know when we first see a DDM
* packet, so we can send a broadcast to the handlers when a client
* connection is made. This method is synchronized so that we only
* send the broadcast once.
*/
synchronized boolean ddmSeen() {
if (mConnState == ST_NEED_DDM_PKT) {
mConnState = ST_READY;
return false;
} else if (mConnState != ST_READY) {
Log.w("ddms", "WEIRD: in ddmSeen with state=" + mConnState);
}
return true;
}
/**
* Close the client socket channel. If there is a debugger associated
* with us, close that too.
*
* Closing a channel automatically unregisters it from the selector.
* However, we have to iterate through the selector loop before it
* actually lets them go and allows the file descriptors to close.
* The caller is expected to manage that.
* @param notify Whether or not to notify the listeners of a change.
*/
void close(boolean notify) {
Log.i("ddms", "Closing " + this.toString());
mOutstandingReqs.clear();
try {
if (mChan != null) {
mChan.close();
mChan = null;
}
if (mDebugger != null) {
mDebugger.close();
mDebugger = null;
}
}
catch (IOException ioe) {
Log.w("ddms", "failed to close " + this);
// swallow it -- not much else to do
}
mDevice.removeClient(this, notify);
}
/**
* Returns whether this {@link Client} has a valid connection to the application VM.
*/
public boolean isValid() {
return mChan != null;
}
void update(int changeMask) {
mDevice.update(this, changeMask);
}
}

View File

@@ -0,0 +1,502 @@
/*
* 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.HeapSegment.HeapSegmentElement;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Contains the data of a {@link Client}.
*/
public class ClientData {
/* This is a place to stash data associated with a Client, such as thread
* states or heap data. ClientData maps 1:1 to Client, but it's a little
* cleaner if we separate the data out.
*
* Message handlers are welcome to stash arbitrary data here.
*
* IMPORTANT: The data here is written by HandleFoo methods and read by
* FooPanel methods, which run in different threads. All non-trivial
* access should be synchronized against the ClientData object.
*/
/** Temporary name of VM to be ignored. */
private final static String PRE_INITIALIZED = "<pre-initialized>"; //$NON-NLS-1$
/** Debugger connection status: not waiting on one, not connected to one, but accepting
* new connections. This is the default value. */
public static final int DEBUGGER_DEFAULT = 1;
/**
* Debugger connection status: the application's VM is paused, waiting for a debugger to
* connect to it before resuming. */
public static final int DEBUGGER_WAITING = 2;
/** Debugger connection status : Debugger is connected */
public static final int DEBUGGER_ATTACHED = 3;
/** Debugger connection status: The listening port for debugger connection failed to listen.
* No debugger will be able to connect. */
public static final int DEBUGGER_ERROR = 4;
/**
* Allocation tracking status: unknown.
* <p/>This happens right after a {@link Client} is discovered
* by the {@link AndroidDebugBridge}, and before the {@link Client} answered the query regarding
* its allocation tracking status.
* @see Client#requestAllocationStatus()
*/
public static final int ALLOCATION_TRACKING_UNKNOWN = -1;
/**
* Allocation tracking status: the {@link Client} is not tracking allocations. */
public static final int ALLOCATION_TRACKING_OFF = 0;
/**
* Allocation tracking status: the {@link Client} is tracking allocations. */
public static final int ALLOCATION_TRACKING_ON = 1;
/**
* Name of the value representing the max size of the heap, in the {@link Map} returned by
* {@link #getVmHeapInfo(int)}
*/
public final static String HEAP_MAX_SIZE_BYTES = "maxSizeInBytes"; // $NON-NLS-1$
/**
* Name of the value representing the size of the heap, in the {@link Map} returned by
* {@link #getVmHeapInfo(int)}
*/
public final static String HEAP_SIZE_BYTES = "sizeInBytes"; // $NON-NLS-1$
/**
* Name of the value representing the number of allocated bytes of the heap, in the
* {@link Map} returned by {@link #getVmHeapInfo(int)}
*/
public final static String HEAP_BYTES_ALLOCATED = "bytesAllocated"; // $NON-NLS-1$
/**
* Name of the value representing the number of objects in the heap, in the {@link Map}
* returned by {@link #getVmHeapInfo(int)}
*/
public final static String HEAP_OBJECTS_ALLOCATED = "objectsAllocated"; // $NON-NLS-1$
// is this a DDM-aware client?
private boolean mIsDdmAware;
// the client's process ID
private final int mPid;
// Java VM identification string
private String mVmIdentifier;
// client's self-description
private String mClientDescription;
// how interested are we in a debugger?
private int mDebuggerInterest;
// Thread tracking (THCR, THDE).
private TreeMap<Integer,ThreadInfo> mThreadMap;
/** VM Heap data */
private final HeapData mHeapData = new HeapData();
/** Native Heap data */
private final HeapData mNativeHeapData = new HeapData();
private HashMap<Integer, HashMap<String, Long>> mHeapInfoMap =
new HashMap<Integer, HashMap<String, Long>>();
/** library map info. Stored here since the backtrace data
* is computed on a need to display basis.
*/
private ArrayList<NativeLibraryMapInfo> mNativeLibMapInfo =
new ArrayList<NativeLibraryMapInfo>();
/** Native Alloc info list */
private ArrayList<NativeAllocationInfo> mNativeAllocationList =
new ArrayList<NativeAllocationInfo>();
private int mNativeTotalMemory;
private AllocationInfo[] mAllocations;
private int mAllocationStatus = ALLOCATION_TRACKING_UNKNOWN;
/**
* Heap Information.
* <p/>The heap is composed of several {@link HeapSegment} objects.
* <p/>A call to {@link #isHeapDataComplete()} will indicate if the segments (available through
* {@link #getHeapSegments()}) represent the full heap.
*/
public static class HeapData {
private TreeSet<HeapSegment> mHeapSegments = new TreeSet<HeapSegment>();
private boolean mHeapDataComplete = false;
private byte[] mProcessedHeapData;
private Map<Integer, ArrayList<HeapSegmentElement>> mProcessedHeapMap;
/**
* Abandon the current list of heap segments.
*/
public synchronized void clearHeapData() {
/* Abandon the old segments instead of just calling .clear().
* This lets the user hold onto the old set if it wants to.
*/
mHeapSegments = new TreeSet<HeapSegment>();
mHeapDataComplete = false;
}
/**
* Add raw HPSG chunk data to the list of heap segments.
*
* @param data The raw data from an HPSG chunk.
*/
synchronized void addHeapData(ByteBuffer data) {
HeapSegment hs;
if (mHeapDataComplete) {
clearHeapData();
}
try {
hs = new HeapSegment(data);
} catch (BufferUnderflowException e) {
System.err.println("Discarding short HPSG data (length " + data.limit() + ")");
return;
}
mHeapSegments.add(hs);
}
/**
* Called when all heap data has arrived.
*/
synchronized void sealHeapData() {
mHeapDataComplete = true;
}
/**
* Returns whether the heap data has been sealed.
*/
public boolean isHeapDataComplete() {
return mHeapDataComplete;
}
/**
* Get the collected heap data, if sealed.
*
* @return The list of heap segments if the heap data has been sealed, or null if it hasn't.
*/
public Collection<HeapSegment> getHeapSegments() {
if (isHeapDataComplete()) {
return mHeapSegments;
}
return null;
}
/**
* Sets the processed heap data.
*
* @param heapData The new heap data (can be null)
*/
public void setProcessedHeapData(byte[] heapData) {
mProcessedHeapData = heapData;
}
/**
* Get the processed heap data, if present.
*
* @return the processed heap data, or null.
*/
public byte[] getProcessedHeapData() {
return mProcessedHeapData;
}
public void setProcessedHeapMap(Map<Integer, ArrayList<HeapSegmentElement>> heapMap) {
mProcessedHeapMap = heapMap;
}
public Map<Integer, ArrayList<HeapSegmentElement>> getProcessedHeapMap() {
return mProcessedHeapMap;
}
}
/**
* Generic constructor.
*/
ClientData(int pid) {
mPid = pid;
mDebuggerInterest = DEBUGGER_DEFAULT;
mThreadMap = new TreeMap<Integer,ThreadInfo>();
}
/**
* Returns whether the process is DDM-aware.
*/
public boolean isDdmAware() {
return mIsDdmAware;
}
/**
* Sets DDM-aware status.
*/
void isDdmAware(boolean aware) {
mIsDdmAware = aware;
}
/**
* Returns the process ID.
*/
public int getPid() {
return mPid;
}
/**
* Returns the Client's VM identifier.
*/
public String getVmIdentifier() {
return mVmIdentifier;
}
/**
* Sets VM identifier.
*/
void setVmIdentifier(String ident) {
mVmIdentifier = ident;
}
/**
* Returns the client description.
* <p/>This is generally the name of the package defined in the
* <code>AndroidManifest.xml</code>.
*
* @return the client description or <code>null</code> if not the description was not yet
* sent by the client.
*/
public String getClientDescription() {
return mClientDescription;
}
/**
* Sets client description.
*
* There may be a race between HELO and APNM. Rather than try
* to enforce ordering on the device, we just don't allow an empty
* name to replace a specified one.
*/
void setClientDescription(String description) {
if (mClientDescription == null && description.length() > 0) {
/*
* The application VM is first named <pre-initialized> before being assigned
* its real name.
* Depending on the timing, we can get an APNM chunk setting this name before
* another one setting the final actual name. So if we get a SetClientDescription
* with this value we ignore it.
*/
if (PRE_INITIALIZED.equals(description) == false) {
mClientDescription = description;
}
}
}
/**
* Returns the debugger connection status. Possible values are {@link #DEBUGGER_DEFAULT},
* {@link #DEBUGGER_WAITING}, {@link #DEBUGGER_ATTACHED}, and {@link #DEBUGGER_ERROR}.
*/
public int getDebuggerConnectionStatus() {
return mDebuggerInterest;
}
/**
* Sets debugger connection status.
*/
void setDebuggerConnectionStatus(int val) {
mDebuggerInterest = val;
}
/**
* Sets the current heap info values for the specified heap.
*
* @param heapId The heap whose info to update
* @param sizeInBytes The size of the heap, in bytes
* @param bytesAllocated The number of bytes currently allocated in the heap
* @param objectsAllocated The number of objects currently allocated in
* the heap
*/
// TODO: keep track of timestamp, reason
synchronized void setHeapInfo(int heapId, long maxSizeInBytes,
long sizeInBytes, long bytesAllocated, long objectsAllocated) {
HashMap<String, Long> heapInfo = new HashMap<String, Long>();
heapInfo.put(HEAP_MAX_SIZE_BYTES, maxSizeInBytes);
heapInfo.put(HEAP_SIZE_BYTES, sizeInBytes);
heapInfo.put(HEAP_BYTES_ALLOCATED, bytesAllocated);
heapInfo.put(HEAP_OBJECTS_ALLOCATED, objectsAllocated);
mHeapInfoMap.put(heapId, heapInfo);
}
/**
* Returns the {@link HeapData} object for the VM.
*/
public HeapData getVmHeapData() {
return mHeapData;
}
/**
* Returns the {@link HeapData} object for the native code.
*/
HeapData getNativeHeapData() {
return mNativeHeapData;
}
/**
* Returns an iterator over the list of known VM heap ids.
* <p/>
* The caller must synchronize on the {@link ClientData} object while iterating.
*
* @return an iterator over the list of heap ids
*/
public synchronized Iterator<Integer> getVmHeapIds() {
return mHeapInfoMap.keySet().iterator();
}
/**
* Returns the most-recent info values for the specified VM heap.
*
* @param heapId The heap whose info should be returned
* @return a map containing the info values for the specified heap.
* Returns <code>null</code> if the heap ID is unknown.
*/
public synchronized Map<String, Long> getVmHeapInfo(int heapId) {
return mHeapInfoMap.get(heapId);
}
/**
* Adds a new thread to the list.
*/
synchronized void addThread(int threadId, String threadName) {
ThreadInfo attr = new ThreadInfo(threadId, threadName);
mThreadMap.put(threadId, attr);
}
/**
* Removes a thread from the list.
*/
synchronized void removeThread(int threadId) {
mThreadMap.remove(threadId);
}
/**
* Returns the list of threads as {@link ThreadInfo} objects.
* <p/>The list is empty until a thread update was requested with
* {@link Client#requestThreadUpdate()}.
*/
public synchronized ThreadInfo[] getThreads() {
Collection<ThreadInfo> threads = mThreadMap.values();
return threads.toArray(new ThreadInfo[threads.size()]);
}
/**
* Returns the {@link ThreadInfo} by thread id.
*/
synchronized ThreadInfo getThread(int threadId) {
return mThreadMap.get(threadId);
}
synchronized void clearThreads() {
mThreadMap.clear();
}
/**
* Returns the list of {@link NativeAllocationInfo}.
* @see Client#requestNativeHeapInformation()
*/
public synchronized List<NativeAllocationInfo> getNativeAllocationList() {
return Collections.unmodifiableList(mNativeAllocationList);
}
/**
* adds a new {@link NativeAllocationInfo} to the {@link Client}
* @param allocInfo The {@link NativeAllocationInfo} to add.
*/
synchronized void addNativeAllocation(NativeAllocationInfo allocInfo) {
mNativeAllocationList.add(allocInfo);
}
/**
* Clear the current malloc info.
*/
synchronized void clearNativeAllocationInfo() {
mNativeAllocationList.clear();
}
/**
* Returns the total native memory.
* @see Client#requestNativeHeapInformation()
*/
public synchronized int getTotalNativeMemory() {
return mNativeTotalMemory;
}
synchronized void setTotalNativeMemory(int totalMemory) {
mNativeTotalMemory = totalMemory;
}
synchronized void addNativeLibraryMapInfo(long startAddr, long endAddr, String library) {
mNativeLibMapInfo.add(new NativeLibraryMapInfo(startAddr, endAddr, library));
}
/**
* Returns an {@link Iterator} on {@link NativeLibraryMapInfo} objects.
* <p/>
* The caller must synchronize on the {@link ClientData} object while iterating.
*/
public synchronized Iterator<NativeLibraryMapInfo> getNativeLibraryMapInfo() {
return mNativeLibMapInfo.iterator();
}
synchronized void setAllocationStatus(boolean enabled) {
mAllocationStatus = enabled ? ALLOCATION_TRACKING_ON : ALLOCATION_TRACKING_OFF;
}
/**
* Returns the allocation tracking status.
* @see Client#requestAllocationStatus()
*/
public synchronized int getAllocationStatus() {
return mAllocationStatus;
}
synchronized void setAllocations(AllocationInfo[] allocs) {
mAllocations = allocs;
}
/**
* Returns the list of tracked allocations.
* @see Client#requestAllocationDetails()
*/
public synchronized AllocationInfo[] getAllocations() {
return mAllocations;
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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.LogLevel;
/**
* Preferences for the ddm library.
* <p/>This class does not handle storing the preferences. It is merely a central point for
* applications using the ddmlib to override the default values.
* <p/>Various components of the ddmlib query this class to get their values.
* <p/>Calls to some <code>set##()</code> methods will update the components using the values
* right away, while other methods will have no effect once {@link AndroidDebugBridge#init(boolean)}
* has been called.
* <p/>Check the documentation of each method.
*/
public final class DdmPreferences {
/** Default value for thread update flag upon client connection. */
public final static boolean DEFAULT_INITIAL_THREAD_UPDATE = false;
/** Default value for heap update flag upon client connection. */
public final static boolean DEFAULT_INITIAL_HEAP_UPDATE = false;
/** Default value for the selected client debug port */
public final static int DEFAULT_SELECTED_DEBUG_PORT = 8700;
/** Default value for the debug port base */
public final static int DEFAULT_DEBUG_PORT_BASE = 8600;
/** Default value for the logcat {@link LogLevel} */
public final static LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR;
private static boolean sThreadUpdate = DEFAULT_INITIAL_THREAD_UPDATE;
private static boolean sInitialHeapUpdate = DEFAULT_INITIAL_HEAP_UPDATE;
private static int sSelectedDebugPort = DEFAULT_SELECTED_DEBUG_PORT;
private static int sDebugPortBase = DEFAULT_DEBUG_PORT_BASE;
private static LogLevel sLogLevel = DEFAULT_LOG_LEVEL;
/**
* Returns the initial {@link Client} flag for thread updates.
* @see #setInitialThreadUpdate(boolean)
*/
public static boolean getInitialThreadUpdate() {
return sThreadUpdate;
}
/**
* Sets the initial {@link Client} flag for thread updates.
* <p/>This change takes effect right away, for newly created {@link Client} objects.
*/
public static void setInitialThreadUpdate(boolean state) {
sThreadUpdate = state;
}
/**
* Returns the initial {@link Client} flag for heap updates.
* @see #setInitialHeapUpdate(boolean)
*/
public static boolean getInitialHeapUpdate() {
return sInitialHeapUpdate;
}
/**
* Sets the initial {@link Client} flag for heap updates.
* <p/>If <code>true</code>, the {@link ClientData} will automatically be updated with
* the VM heap information whenever a GC happens.
* <p/>This change takes effect right away, for newly created {@link Client} objects.
*/
public static void setInitialHeapUpdate(boolean state) {
sInitialHeapUpdate = state;
}
/**
* Returns the debug port used by the selected {@link Client}.
*/
public static int getSelectedDebugPort() {
return sSelectedDebugPort;
}
/**
* Sets the debug port used by the selected {@link Client}.
* <p/>This change takes effect right away.
* @param port the new port to use.
*/
public static void setSelectedDebugPort(int port) {
sSelectedDebugPort = port;
MonitorThread monitorThread = MonitorThread.getInstance();
if (monitorThread != null) {
monitorThread.setDebugSelectedPort(port);
}
}
/**
* Returns the debug port used by the first {@link Client}. Following clients, will use the
* next port.
*/
public static int getDebugPortBase() {
return sDebugPortBase;
}
/**
* Sets the debug port used by the first {@link Client}.
* <p/>Once a port is used, the next Client will use port + 1. Quitting applications will
* release their debug port, and new clients will be able to reuse them.
* <p/>This must be called before {@link AndroidDebugBridge#init(boolean)}.
*/
public static void setDebugPortBase(int port) {
sDebugPortBase = port;
}
/**
* Returns the minimum {@link LogLevel} being displayed.
*/
public static LogLevel getLogLevel() {
return sLogLevel;
}
/**
* Sets the minimum {@link LogLevel} to display.
* <p/>This change takes effect right away.
*/
public static void setLogLevel(String value) {
sLogLevel = LogLevel.getByString(value);
Log.setLevel(sLogLevel);
}
/**
* Non accessible constructor.
*/
private DdmPreferences() {
// pass, only static methods in the class.
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.Device;
/**
* Centralized point to provide a {@link IDebugPortProvider} to ddmlib.
*
* <p/>When {@link Client} objects are created, they start listening for debuggers on a specific
* port. The default behavior is to start with {@link DdmPreferences#getDebugPortBase()} and
* increment this value for each new <code>Client</code>.
*
* <p/>This {@link DebugPortManager} allows applications using ddmlib to provide a custom
* port provider on a per-<code>Client</code> basis, depending on the device/emulator they are
* running on, and/or their names.
*/
public class DebugPortManager {
/**
* Classes which implement this interface provide a method that provides a non random
* debugger port for a newly created {@link Client}.
*/
public interface IDebugPortProvider {
public static final int NO_STATIC_PORT = -1;
/**
* Returns a non-random debugger port for the specified application running on the
* specified {@link Device}.
* @param device The device the application is running on.
* @param appName The application name, as defined in the <code>AndroidManifest.xml</code>
* <var>package</var> attribute of the <var>manifest</var> node.
* @return The non-random debugger port or {@link #NO_STATIC_PORT} if the {@link Client}
* should use the automatic debugger port provider.
*/
public int getPort(Device device, String appName);
}
private static IDebugPortProvider sProvider = null;
/**
* Sets the {@link IDebugPortProvider} that will be used when a new {@link Client} requests
* a debugger port.
* @param provider the <code>IDebugPortProvider</code> to use.
*/
public static void setProvider(IDebugPortProvider provider) {
sProvider = provider;
}
/**
* Returns the
* @return
*/
static IDebugPortProvider getProvider() {
return sProvider;
}
}

View File

@@ -0,0 +1,351 @@
/*
* 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 java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* This represents a pending or established connection with a JDWP debugger.
*/
class Debugger {
/*
* Messages from the debugger should be pretty small; may not even
* need an expanding-buffer implementation for this.
*/
private static final int INITIAL_BUF_SIZE = 1 * 1024;
private static final int MAX_BUF_SIZE = 32 * 1024;
private ByteBuffer mReadBuffer;
private static final int PRE_DATA_BUF_SIZE = 256;
private ByteBuffer mPreDataBuffer;
/* connection state */
private int mConnState;
private static final int ST_NOT_CONNECTED = 1;
private static final int ST_AWAIT_SHAKE = 2;
private static final int ST_READY = 3;
/* peer */
private Client mClient; // client we're forwarding to/from
private int mListenPort; // listen to me
private ServerSocketChannel mListenChannel;
/* this goes up and down; synchronize methods that access the field */
private SocketChannel mChannel;
/**
* Create a new Debugger object, configured to listen for connections
* on a specific port.
*/
Debugger(Client client, int listenPort) throws IOException {
mClient = client;
mListenPort = listenPort;
mListenChannel = ServerSocketChannel.open();
mListenChannel.configureBlocking(false); // required for Selector
InetSocketAddress addr = new InetSocketAddress(
InetAddress.getByName("localhost"), // $NON-NLS-1$
listenPort);
mListenChannel.socket().setReuseAddress(true); // enable SO_REUSEADDR
mListenChannel.socket().bind(addr);
mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE);
mPreDataBuffer = ByteBuffer.allocate(PRE_DATA_BUF_SIZE);
mConnState = ST_NOT_CONNECTED;
Log.i("ddms", "Created: " + this.toString());
}
/**
* Returns "true" if a debugger is currently attached to us.
*/
boolean isDebuggerAttached() {
return mChannel != null;
}
/**
* Represent the Debugger as a string.
*/
@Override
public String toString() {
// mChannel != null means we have connection, ST_READY means it's going
return "[Debugger " + mListenPort + "-->" + mClient.getClientData().getPid()
+ ((mConnState != ST_READY) ? " inactive]" : " active]");
}
/**
* Register the debugger's listen socket with the Selector.
*/
void registerListener(Selector sel) throws IOException {
mListenChannel.register(sel, SelectionKey.OP_ACCEPT, this);
}
/**
* Return the Client being debugged.
*/
Client getClient() {
return mClient;
}
/**
* Accept a new connection, but only if we don't already have one.
*
* Must be synchronized with other uses of mChannel and mPreBuffer.
*
* Returns "null" if we're already talking to somebody.
*/
synchronized SocketChannel accept() throws IOException {
return accept(mListenChannel);
}
/**
* Accept a new connection from the specified listen channel. This
* is so we can listen on a dedicated port for the "current" client,
* where "current" is constantly in flux.
*
* Must be synchronized with other uses of mChannel and mPreBuffer.
*
* Returns "null" if we're already talking to somebody.
*/
synchronized SocketChannel accept(ServerSocketChannel listenChan)
throws IOException {
if (listenChan != null) {
SocketChannel newChan;
newChan = listenChan.accept();
if (mChannel != null) {
Log.w("ddms", "debugger already talking to " + mClient
+ " on " + mListenPort);
newChan.close();
return null;
}
mChannel = newChan;
mChannel.configureBlocking(false); // required for Selector
mConnState = ST_AWAIT_SHAKE;
return mChannel;
}
return null;
}
/**
* Close the data connection only.
*/
synchronized void closeData() {
try {
if (mChannel != null) {
mChannel.close();
mChannel = null;
mConnState = ST_NOT_CONNECTED;
ClientData cd = mClient.getClientData();
cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_DEFAULT);
mClient.update(Client.CHANGE_DEBUGGER_INTEREST);
}
} catch (IOException ioe) {
Log.w("ddms", "Failed to close data " + this);
}
}
/**
* Close the socket that's listening for new connections and (if
* we're connected) the debugger data socket.
*/
synchronized void close() {
try {
if (mListenChannel != null) {
mListenChannel.close();
}
mListenChannel = null;
closeData();
} catch (IOException ioe) {
Log.w("ddms", "Failed to close listener " + this);
}
}
// TODO: ?? add a finalizer that verifies the channel was closed
/**
* Read data from our channel.
*
* This is called when data is known to be available, and we don't yet
* have a full packet in the buffer. If the buffer is at capacity,
* expand it.
*/
void read() throws IOException {
int count;
if (mReadBuffer.position() == mReadBuffer.capacity()) {
if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) {
throw new BufferOverflowException();
}
Log.d("ddms", "Expanding read buffer to "
+ mReadBuffer.capacity() * 2);
ByteBuffer newBuffer =
ByteBuffer.allocate(mReadBuffer.capacity() * 2);
mReadBuffer.position(0);
newBuffer.put(mReadBuffer); // leaves "position" at end
mReadBuffer = newBuffer;
}
count = mChannel.read(mReadBuffer);
Log.v("ddms", "Read " + count + " bytes from " + this);
if (count < 0) throw new IOException("read failed");
}
/**
* Return information for the first full JDWP packet in the buffer.
*
* If we don't yet have a full packet, return null.
*
* If we haven't yet received the JDWP handshake, we watch for it here
* and consume it without admitting to have done so. We also send
* the handshake response to the debugger, along with any pending
* pre-connection data, which is why this can throw an IOException.
*/
JdwpPacket getJdwpPacket() throws IOException {
/*
* On entry, the data starts at offset 0 and ends at "position".
* "limit" is set to the buffer capacity.
*/
if (mConnState == ST_AWAIT_SHAKE) {
int result;
result = JdwpPacket.findHandshake(mReadBuffer);
//Log.v("ddms", "findHand: " + result);
switch (result) {
case JdwpPacket.HANDSHAKE_GOOD:
Log.i("ddms", "Good handshake from debugger");
JdwpPacket.consumeHandshake(mReadBuffer);
sendHandshake();
mConnState = ST_READY;
ClientData cd = mClient.getClientData();
cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_ATTACHED);
mClient.update(Client.CHANGE_DEBUGGER_INTEREST);
// see if we have another packet in the buffer
return getJdwpPacket();
case JdwpPacket.HANDSHAKE_BAD:
// not a debugger, throw an exception so we drop the line
Log.i("ddms", "Bad handshake from debugger");
throw new IOException("bad handshake");
case JdwpPacket.HANDSHAKE_NOTYET:
break;
default:
Log.e("ddms", "Unknown packet while waiting for client handshake");
}
return null;
} else if (mConnState == ST_READY) {
if (mReadBuffer.position() != 0) {
Log.v("ddms", "Checking " + mReadBuffer.position() + " bytes");
}
return JdwpPacket.findPacket(mReadBuffer);
} else {
Log.e("ddms", "Receiving data in state = " + mConnState);
}
return null;
}
/**
* Forward a packet to the client.
*
* "mClient" will never be null, though it's possible that the channel
* in the client has closed and our send attempt will fail.
*
* Consumes the packet.
*/
void forwardPacketToClient(JdwpPacket packet) throws IOException {
mClient.sendAndConsume(packet);
}
/**
* Send the handshake to the debugger. We also send along any packets
* we already received from the client (usually just a VM_START event,
* if anything at all).
*/
private synchronized void sendHandshake() throws IOException {
ByteBuffer tempBuffer = ByteBuffer.allocate(JdwpPacket.HANDSHAKE_LEN);
JdwpPacket.putHandshake(tempBuffer);
int expectedLength = tempBuffer.position();
tempBuffer.flip();
if (mChannel.write(tempBuffer) != expectedLength) {
throw new IOException("partial handshake write");
}
expectedLength = mPreDataBuffer.position();
if (expectedLength > 0) {
Log.d("ddms", "Sending " + mPreDataBuffer.position()
+ " bytes of saved data");
mPreDataBuffer.flip();
if (mChannel.write(mPreDataBuffer) != expectedLength) {
throw new IOException("partial pre-data write");
}
mPreDataBuffer.clear();
}
}
/**
* Send a packet to the debugger.
*
* Ideally, we can do this with a single channel write. If that doesn't
* happen, we have to prevent anybody else from writing to the channel
* until this packet completes, so we synchronize on the channel.
*
* Another goal is to avoid unnecessary buffer copies, so we write
* directly out of the JdwpPacket's ByteBuffer.
*
* We must synchronize on "mChannel" before writing to it. We want to
* coordinate the buffered data with mChannel creation, so this whole
* method is synchronized.
*/
synchronized void sendAndConsume(JdwpPacket packet)
throws IOException {
if (mChannel == null) {
/*
* Buffer this up so we can send it to the debugger when it
* finally does connect. This is essential because the VM_START
* message might be telling the debugger that the VM is
* suspended. The alternative approach would be for us to
* capture and interpret VM_START and send it later if we
* didn't choose to un-suspend the VM for our own purposes.
*/
Log.d("ddms", "Saving packet 0x"
+ Integer.toHexString(packet.getId()));
packet.movePacket(mPreDataBuffer);
} else {
packet.writeAndConsume(mChannel);
}
}
}

View File

@@ -0,0 +1,385 @@
/*
* 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.Client;
import com.android.ddmlib.log.LogReceiver;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A Device. It can be a physical device or an emulator.
*
* TODO: make this class package-protected, and shift all callers to use IDevice
*/
public final class Device implements IDevice {
/**
* The state of a device.
*/
public static enum DeviceState {
BOOTLOADER("bootloader"), //$NON-NLS-1$
OFFLINE("offline"), //$NON-NLS-1$
ONLINE("device"); //$NON-NLS-1$
private String mState;
DeviceState(String state) {
mState = state;
}
/**
* Returns a {@link DeviceState} from the string returned by <code>adb devices</code>.
* @param state the device state.
* @return a {@link DeviceState} object or <code>null</code> if the state is unknown.
*/
public static DeviceState getState(String state) {
for (DeviceState deviceState : values()) {
if (deviceState.mState.equals(state)) {
return deviceState;
}
}
return null;
}
}
/** Emulator Serial Number regexp. */
final static String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$
/** Serial number of the device */
String serialNumber = null;
/** Name of the AVD */
String mAvdName = null;
/** State of the device. */
DeviceState state = null;
/** Device properties. */
private final Map<String, String> mProperties = new HashMap<String, String>();
private final ArrayList<Client> mClients = new ArrayList<Client>();
private DeviceMonitor mMonitor;
/**
* Socket for the connection monitoring client connection/disconnection.
*/
private SocketChannel mSocketChannel;
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getSerialNumber()
*/
public String getSerialNumber() {
return serialNumber;
}
public String getAvdName() {
return mAvdName;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getState()
*/
public DeviceState getState() {
return state;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getProperties()
*/
public Map<String, String> getProperties() {
return Collections.unmodifiableMap(mProperties);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getPropertyCount()
*/
public int getPropertyCount() {
return mProperties.size();
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getProperty(java.lang.String)
*/
public String getProperty(String name) {
return mProperties.get(name);
}
@Override
public String toString() {
return serialNumber;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isOnline()
*/
public boolean isOnline() {
return state == DeviceState.ONLINE;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isEmulator()
*/
public boolean isEmulator() {
return serialNumber.matches(RE_EMULATOR_SN);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isOffline()
*/
public boolean isOffline() {
return state == DeviceState.OFFLINE;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#isBootLoader()
*/
public boolean isBootLoader() {
return state == DeviceState.BOOTLOADER;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#hasClients()
*/
public boolean hasClients() {
return mClients.size() > 0;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getClients()
*/
public Client[] getClients() {
synchronized (mClients) {
return mClients.toArray(new Client[mClients.size()]);
}
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getClient(java.lang.String)
*/
public Client getClient(String applicationName) {
synchronized (mClients) {
for (Client c : mClients) {
if (applicationName.equals(c.getClientData().getClientDescription())) {
return c;
}
}
}
return null;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getSyncService()
*/
public SyncService getSyncService() {
SyncService syncService = new SyncService(AndroidDebugBridge.sSocketAddr, this);
if (syncService.openSync()) {
return syncService;
}
return null;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getFileListingService()
*/
public FileListingService getFileListingService() {
return new FileListingService(this);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getScreenshot()
*/
public RawImage getScreenshot() throws IOException {
return AdbHelper.getFrameBuffer(AndroidDebugBridge.sSocketAddr, this);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#executeShellCommand(java.lang.String, com.android.ddmlib.IShellOutputReceiver)
*/
public void executeShellCommand(String command, IShellOutputReceiver receiver)
throws IOException {
AdbHelper.executeRemoteCommand(AndroidDebugBridge.sSocketAddr, command, this,
receiver);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#runEventLogService(com.android.ddmlib.log.LogReceiver)
*/
public void runEventLogService(LogReceiver receiver) throws IOException {
AdbHelper.runEventLogService(AndroidDebugBridge.sSocketAddr, this, receiver);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#runLogService(com.android.ddmlib.log.LogReceiver)
*/
public void runLogService(String logname,
LogReceiver receiver) throws IOException {
AdbHelper.runLogService(AndroidDebugBridge.sSocketAddr, this, logname, receiver);
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#createForward(int, int)
*/
public boolean createForward(int localPort, int remotePort) {
try {
return AdbHelper.createForward(AndroidDebugBridge.sSocketAddr, this,
localPort, remotePort);
} catch (IOException e) {
Log.e("adb-forward", e); //$NON-NLS-1$
return false;
}
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#removeForward(int, int)
*/
public boolean removeForward(int localPort, int remotePort) {
try {
return AdbHelper.removeForward(AndroidDebugBridge.sSocketAddr, this,
localPort, remotePort);
} catch (IOException e) {
Log.e("adb-remove-forward", e); //$NON-NLS-1$
return false;
}
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getClientName(int)
*/
public String getClientName(int pid) {
synchronized (mClients) {
for (Client c : mClients) {
if (c.getClientData().getPid() == pid) {
return c.getClientData().getClientDescription();
}
}
}
return null;
}
Device(DeviceMonitor monitor) {
mMonitor = monitor;
}
DeviceMonitor getMonitor() {
return mMonitor;
}
void addClient(Client client) {
synchronized (mClients) {
mClients.add(client);
}
}
List<Client> getClientList() {
return mClients;
}
boolean hasClient(int pid) {
synchronized (mClients) {
for (Client client : mClients) {
if (client.getClientData().getPid() == pid) {
return true;
}
}
}
return false;
}
void clearClientList() {
synchronized (mClients) {
mClients.clear();
}
}
/**
* Sets the client monitoring socket.
* @param socketChannel the sockets
*/
void setClientMonitoringSocket(SocketChannel socketChannel) {
mSocketChannel = socketChannel;
}
/**
* Returns the client monitoring socket.
*/
SocketChannel getClientMonitoringSocket() {
return mSocketChannel;
}
/**
* Removes a {@link Client} from the list.
* @param client the client to remove.
* @param notify Whether or not to notify the listeners of a change.
*/
void removeClient(Client client, boolean notify) {
mMonitor.addPortToAvailableList(client.getDebuggerListenPort());
synchronized (mClients) {
mClients.remove(client);
}
if (notify) {
mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST);
}
}
void update(int changeMask) {
mMonitor.getServer().deviceChanged(this, changeMask);
}
void update(Client client, int changeMask) {
mMonitor.getServer().clientChanged(client, changeMask);
}
void addProperty(String label, String value) {
mProperties.put(label, value);
}
}

View File

@@ -0,0 +1,866 @@
/*
* 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.AdbHelper.AdbResponse;
import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
import com.android.ddmlib.Device.DeviceState;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* A Device monitor. This connects to the Android Debug Bridge and get device and
* debuggable process information from it.
*/
final class DeviceMonitor {
private byte[] mLengthBuffer = new byte[4];
private byte[] mLengthBuffer2 = new byte[4];
private boolean mQuit = false;
private AndroidDebugBridge mServer;
private SocketChannel mMainAdbConnection = null;
private boolean mMonitoring = false;
private int mConnectionAttempt = 0;
private int mRestartAttemptCount = 0;
private boolean mInitialDeviceListDone = false;
private Selector mSelector;
private final ArrayList<Device> mDevices = new ArrayList<Device>();
private final ArrayList<Integer> mDebuggerPorts = new ArrayList<Integer>();
private final HashMap<Client, Integer> mClientsToReopen = new HashMap<Client, Integer>();
/**
* Creates a new {@link DeviceMonitor} object and links it to the running
* {@link AndroidDebugBridge} object.
* @param server the running {@link AndroidDebugBridge}.
*/
DeviceMonitor(AndroidDebugBridge server) {
mServer = server;
mDebuggerPorts.add(DdmPreferences.getDebugPortBase());
}
/**
* Starts the monitoring.
*/
void start() {
new Thread("Device List Monitor") { //$NON-NLS-1$
@Override
public void run() {
deviceMonitorLoop();
}
}.start();
}
/**
* Stops the monitoring.
*/
void stop() {
mQuit = true;
// wakeup the main loop thread by closing the main connection to adb.
try {
if (mMainAdbConnection != null) {
mMainAdbConnection.close();
}
} catch (IOException e1) {
}
// wake up the secondary loop by closing the selector.
if (mSelector != null) {
mSelector.wakeup();
}
}
/**
* Returns if the monitor is currently connected to the debug bridge server.
* @return
*/
boolean isMonitoring() {
return mMonitoring;
}
int getConnectionAttemptCount() {
return mConnectionAttempt;
}
int getRestartAttemptCount() {
return mRestartAttemptCount;
}
/**
* Returns the devices.
*/
Device[] getDevices() {
synchronized (mDevices) {
return mDevices.toArray(new Device[mDevices.size()]);
}
}
boolean hasInitialDeviceList() {
return mInitialDeviceListDone;
}
AndroidDebugBridge getServer() {
return mServer;
}
void addClientToDropAndReopen(Client client, int port) {
synchronized (mClientsToReopen) {
Log.d("DeviceMonitor",
"Adding " + client + " to list of client to reopen (" + port +").");
if (mClientsToReopen.get(client) == null) {
mClientsToReopen.put(client, port);
}
}
mSelector.wakeup();
}
/**
* Monitors the devices. This connects to the Debug Bridge
*/
private void deviceMonitorLoop() {
do {
try {
if (mMainAdbConnection == null) {
Log.d("DeviceMonitor", "Opening adb connection");
mMainAdbConnection = openAdbConnection();
if (mMainAdbConnection == null) {
mConnectionAttempt++;
Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt);
if (mConnectionAttempt > 10) {
if (mServer.startAdb() == false) {
mRestartAttemptCount++;
Log.e("DeviceMonitor",
"adb restart attempts: " + mRestartAttemptCount);
} else {
mRestartAttemptCount = 0;
}
}
waitABit();
} else {
Log.d("DeviceMonitor", "Connected to adb for device monitoring");
mConnectionAttempt = 0;
}
}
if (mMainAdbConnection != null && mMonitoring == false) {
mMonitoring = sendDeviceListMonitoringRequest();
}
if (mMonitoring) {
// read the length of the incoming message
int length = readLength(mMainAdbConnection, mLengthBuffer);
if (length >= 0) {
// read the incoming message
processIncomingDeviceData(length);
// flag the fact that we have build the list at least once.
mInitialDeviceListDone = true;
}
}
} catch (AsynchronousCloseException ace) {
// this happens because of a call to Quit. We do nothing, and the loop will break.
} catch (IOException ioe) {
if (mQuit == false) {
Log.e("DeviceMonitor", "Adb connection Error:" + ioe.getMessage());
mMonitoring = false;
if (mMainAdbConnection != null) {
try {
mMainAdbConnection.close();
} catch (IOException ioe2) {
// we can safely ignore that one.
}
mMainAdbConnection = null;
}
}
}
} while (mQuit == false);
}
/**
* Sleeps for a little bit.
*/
private void waitABit() {
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
}
}
/**
* Attempts to connect to the debug bridge server.
* @return a connect socket if success, null otherwise
*/
private SocketChannel openAdbConnection() {
Log.d("DeviceMonitor", "Connecting to adb for Device List Monitoring...");
SocketChannel adbChannel = null;
try {
adbChannel = SocketChannel.open(AndroidDebugBridge.sSocketAddr);
adbChannel.socket().setTcpNoDelay(true);
} catch (IOException e) {
}
return adbChannel;
}
/**
*
* @return
* @throws IOException
*/
private boolean sendDeviceListMonitoringRequest() throws IOException {
byte[] request = AdbHelper.formAdbRequest("host:track-devices"); //$NON-NLS-1$
if (AdbHelper.write(mMainAdbConnection, request) == false) {
Log.e("DeviceMonitor", "Sending Tracking request failed!");
mMainAdbConnection.close();
throw new IOException("Sending Tracking request failed!");
}
AdbResponse resp = AdbHelper.readAdbResponse(mMainAdbConnection,
false /* readDiagString */);
if (resp.ioSuccess == false) {
Log.e("DeviceMonitor", "Failed to read the adb response!");
mMainAdbConnection.close();
throw new IOException("Failed to read the adb response!");
}
if (resp.okay == false) {
// request was refused by adb!
Log.e("DeviceMonitor", "adb refused request: " + resp.message);
}
return resp.okay;
}
/**
* Processes an incoming device message from the socket
* @param socket
* @param length
* @throws IOException
*/
private void processIncomingDeviceData(int length) throws IOException {
ArrayList<Device> list = new ArrayList<Device>();
if (length > 0) {
byte[] buffer = new byte[length];
String result = read(mMainAdbConnection, buffer);
String[] devices = result.split("\n"); // $NON-NLS-1$
for (String d : devices) {
String[] param = d.split("\t"); // $NON-NLS-1$
if (param.length == 2) {
// new adb uses only serial numbers to identify devices
Device device = new Device(this);
device.serialNumber = param[0];
device.state = DeviceState.getState(param[1]);
//add the device to the list
list.add(device);
}
}
}
// now merge the new devices with the old ones.
updateDevices(list);
}
/**
* Updates the device list with the new items received from the monitoring service.
*/
private void updateDevices(ArrayList<Device> newList) {
// because we are going to call mServer.deviceDisconnected which will acquire this lock
// we lock it first, so that the AndroidDebugBridge lock is always locked first.
synchronized (AndroidDebugBridge.getLock()) {
synchronized (mDevices) {
// For each device in the current list, we look for a matching the new list.
// * if we find it, we update the current object with whatever new information
// there is
// (mostly state change, if the device becomes ready, we query for build info).
// We also remove the device from the new list to mark it as "processed"
// * if we do not find it, we remove it from the current list.
// Once this is done, the new list contains device we aren't monitoring yet, so we
// add them to the list, and start monitoring them.
for (int d = 0 ; d < mDevices.size() ;) {
Device device = mDevices.get(d);
// look for a similar device in the new list.
int count = newList.size();
boolean foundMatch = false;
for (int dd = 0 ; dd < count ; dd++) {
Device newDevice = newList.get(dd);
// see if it matches in id and serial number.
if (newDevice.serialNumber.equals(device.serialNumber)) {
foundMatch = true;
// update the state if needed.
if (device.state != newDevice.state) {
device.state = newDevice.state;
device.update(Device.CHANGE_STATE);
// if the device just got ready/online, we need to start
// monitoring it.
if (device.isOnline()) {
if (AndroidDebugBridge.getClientSupport() == true) {
if (startMonitoringDevice(device) == false) {
Log.e("DeviceMonitor",
"Failed to start monitoring "
+ device.serialNumber);
}
}
if (device.getPropertyCount() == 0) {
queryNewDeviceForInfo(device);
}
}
}
// remove the new device from the list since it's been used
newList.remove(dd);
break;
}
}
if (foundMatch == false) {
// the device is gone, we need to remove it, and keep current index
// to process the next one.
removeDevice(device);
mServer.deviceDisconnected(device);
} else {
// process the next one
d++;
}
}
// at this point we should still have some new devices in newList, so we
// process them.
for (Device newDevice : newList) {
// add them to the list
mDevices.add(newDevice);
mServer.deviceConnected(newDevice);
// start monitoring them.
if (AndroidDebugBridge.getClientSupport() == true) {
if (newDevice.isOnline()) {
startMonitoringDevice(newDevice);
}
}
// look for their build info.
if (newDevice.isOnline()) {
queryNewDeviceForInfo(newDevice);
}
}
}
}
newList.clear();
}
private void removeDevice(Device device) {
device.clearClientList();
mDevices.remove(device);
SocketChannel channel = device.getClientMonitoringSocket();
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
// doesn't really matter if the close fails.
}
}
}
/**
* Queries a device for its build info.
* @param device the device to query.
*/
private void queryNewDeviceForInfo(Device device) {
// TODO: do this in a separate thread.
try {
// first get the list of properties.
device.executeShellCommand(GetPropReceiver.GETPROP_COMMAND,
new GetPropReceiver(device));
// now get the emulator Virtual Device name (if applicable).
if (device.isEmulator()) {
EmulatorConsole console = EmulatorConsole.getConsole(device);
if (console != null) {
device.mAvdName = console.getAvdName();
}
}
} catch (IOException e) {
// if we can't get the build info, it doesn't matter too much
}
}
/**
* Starts a monitoring service for a device.
* @param device the device to monitor.
* @return true if success.
*/
private boolean startMonitoringDevice(Device device) {
SocketChannel socketChannel = openAdbConnection();
if (socketChannel != null) {
try {
boolean result = sendDeviceMonitoringRequest(socketChannel, device);
if (result) {
if (mSelector == null) {
startDeviceMonitorThread();
}
device.setClientMonitoringSocket(socketChannel);
synchronized (mDevices) {
// always wakeup before doing the register. The synchronized block
// ensure that the selector won't select() before the end of this block.
// @see deviceClientMonitorLoop
mSelector.wakeup();
socketChannel.configureBlocking(false);
socketChannel.register(mSelector, SelectionKey.OP_READ, device);
}
return true;
}
} catch (IOException e) {
try {
// attempt to close the socket if needed.
socketChannel.close();
} catch (IOException e1) {
// we can ignore that one. It may already have been closed.
}
Log.d("DeviceMonitor",
"Connection Failure when starting to monitor device '"
+ device + "' : " + e.getMessage());
}
}
return false;
}
private void startDeviceMonitorThread() throws IOException {
mSelector = Selector.open();
new Thread("Device Client Monitor") { //$NON-NLS-1$
@Override
public void run() {
deviceClientMonitorLoop();
}
}.start();
}
private void deviceClientMonitorLoop() {
do {
try {
// This synchronized block stops us from doing the select() if a new
// Device is being added.
// @see startMonitoringDevice()
synchronized (mDevices) {
}
int count = mSelector.select();
if (mQuit) {
return;
}
synchronized (mClientsToReopen) {
if (mClientsToReopen.size() > 0) {
Set<Client> clients = mClientsToReopen.keySet();
MonitorThread monitorThread = MonitorThread.getInstance();
for (Client client : clients) {
Device device = client.getDevice();
int pid = client.getClientData().getPid();
monitorThread.dropClient(client, false /* notify */);
// This is kinda bad, but if we don't wait a bit, the client
// will never answer the second handshake!
waitABit();
int port = mClientsToReopen.get(client);
if (port == IDebugPortProvider.NO_STATIC_PORT) {
port = getNextDebuggerPort();
}
Log.d("DeviceMonitor", "Reopening " + client);
openClient(device, pid, port, monitorThread);
device.update(Device.CHANGE_CLIENT_LIST);
}
mClientsToReopen.clear();
}
}
if (count == 0) {
continue;
}
Set<SelectionKey> keys = mSelector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isValid() && key.isReadable()) {
Object attachment = key.attachment();
if (attachment instanceof Device) {
Device device = (Device)attachment;
SocketChannel socket = device.getClientMonitoringSocket();
if (socket != null) {
try {
int length = readLength(socket, mLengthBuffer2);
processIncomingJdwpData(device, socket, length);
} catch (IOException ioe) {
Log.d("DeviceMonitor",
"Error reading jdwp list: " + ioe.getMessage());
socket.close();
// restart the monitoring of that device
synchronized (mDevices) {
if (mDevices.contains(device)) {
Log.d("DeviceMonitor",
"Restarting monitoring service for " + device);
startMonitoringDevice(device);
}
}
}
}
}
}
}
} catch (IOException e) {
if (mQuit == false) {
}
}
} while (mQuit == false);
}
private boolean sendDeviceMonitoringRequest(SocketChannel socket, Device device)
throws IOException {
AdbHelper.setDevice(socket, device);
byte[] request = AdbHelper.formAdbRequest("track-jdwp"); //$NON-NLS-1$
if (AdbHelper.write(socket, request) == false) {
Log.e("DeviceMonitor", "Sending jdwp tracking request failed!");
socket.close();
throw new IOException();
}
AdbResponse resp = AdbHelper.readAdbResponse(socket, false /* readDiagString */);
if (resp.ioSuccess == false) {
Log.e("DeviceMonitor", "Failed to read the adb response!");
socket.close();
throw new IOException();
}
if (resp.okay == false) {
// request was refused by adb!
Log.e("DeviceMonitor", "adb refused request: " + resp.message);
}
return resp.okay;
}
private void processIncomingJdwpData(Device device, SocketChannel monitorSocket, int length)
throws IOException {
if (length >= 0) {
// array for the current pids.
ArrayList<Integer> pidList = new ArrayList<Integer>();
// get the string data if there are any
if (length > 0) {
byte[] buffer = new byte[length];
String result = read(monitorSocket, buffer);
// split each line in its own list and create an array of integer pid
String[] pids = result.split("\n"); //$NON-NLS-1$
for (String pid : pids) {
try {
pidList.add(Integer.valueOf(pid));
} catch (NumberFormatException nfe) {
// looks like this pid is not really a number. Lets ignore it.
continue;
}
}
}
MonitorThread monitorThread = MonitorThread.getInstance();
// Now we merge the current list with the old one.
// this is the same mechanism as the merging of the device list.
// For each client in the current list, we look for a matching the pid in the new list.
// * if we find it, we do nothing, except removing the pid from its list,
// to mark it as "processed"
// * if we do not find any match, we remove the client from the current list.
// Once this is done, the new list contains pids for which we don't have clients yet,
// so we create clients for them, add them to the list, and start monitoring them.
List<Client> clients = device.getClientList();
boolean changed = false;
// because MonitorThread#dropClient acquires first the monitorThread lock and then the
// Device client list lock (when removing the Client from the list), we have to make
// sure we acquire the locks in the same order, since another thread (MonitorThread),
// could call dropClient itself.
synchronized (monitorThread) {
synchronized (clients) {
for (int c = 0 ; c < clients.size() ;) {
Client client = clients.get(c);
int pid = client.getClientData().getPid();
// look for a matching pid
Integer match = null;
for (Integer matchingPid : pidList) {
if (pid == matchingPid.intValue()) {
match = matchingPid;
break;
}
}
if (match != null) {
pidList.remove(match);
c++; // move on to the next client.
} else {
// we need to drop the client. the client will remove itself from the
// list of its device which is 'clients', so there's no need to
// increment c.
// We ask the monitor thread to not send notification, as we'll do
// it once at the end.
monitorThread.dropClient(client, false /* notify */);
changed = true;
}
}
}
}
// at this point whatever pid is left in the list needs to be converted into Clients.
for (int newPid : pidList) {
openClient(device, newPid, getNextDebuggerPort(), monitorThread);
changed = true;
}
if (changed) {
mServer.deviceChanged(device, Device.CHANGE_CLIENT_LIST);
}
}
}
/**
* Opens and creates a new client.
* @return
*/
private void openClient(Device device, int pid, int port, MonitorThread monitorThread) {
SocketChannel clientSocket;
try {
clientSocket = AdbHelper.createPassThroughConnection(
AndroidDebugBridge.sSocketAddr, device, pid);
// required for Selector
clientSocket.configureBlocking(false);
} catch (UnknownHostException uhe) {
Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid);
return;
} catch (IOException ioe) {
Log.w("DeviceMonitor",
"Failed to connect to client '" + pid + "': " + ioe.getMessage());
return ;
}
createClient(device, pid, clientSocket, port, monitorThread);
}
/**
* Creates a client and register it to the monitor thread
* @param device
* @param pid
* @param socket
* @param debuggerPort the debugger port.
* @param monitorThread the {@link MonitorThread} object.
*/
private void createClient(Device device, int pid, SocketChannel socket, int debuggerPort,
MonitorThread monitorThread) {
/*
* Successfully connected to something. Create a Client object, add
* it to the list, and initiate the JDWP handshake.
*/
Client client = new Client(device, socket, pid);
if (client.sendHandshake()) {
try {
if (AndroidDebugBridge.getClientSupport()) {
client.listenForDebugger(debuggerPort);
}
client.requestAllocationStatus();
} catch (IOException ioe) {
client.getClientData().setDebuggerConnectionStatus(ClientData.DEBUGGER_ERROR);
Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger");
// oh well
}
} else {
Log.e("ddms", "Handshake with " + client + " failed!");
/*
* The handshake send failed. We could remove it now, but if the
* failure is "permanent" we'll just keep banging on it and
* getting the same result. Keep it in the list with its "error"
* state so we don't try to reopen it.
*/
}
if (client.isValid()) {
device.addClient(client);
monitorThread.addClient(client);
} else {
client = null;
}
}
private int getNextDebuggerPort() {
// get the first port and remove it
synchronized (mDebuggerPorts) {
if (mDebuggerPorts.size() > 0) {
int port = mDebuggerPorts.get(0);
// remove it.
mDebuggerPorts.remove(0);
// if there's nothing left, add the next port to the list
if (mDebuggerPorts.size() == 0) {
mDebuggerPorts.add(port+1);
}
return port;
}
}
return -1;
}
void addPortToAvailableList(int port) {
if (port > 0) {
synchronized (mDebuggerPorts) {
// because there could be case where clients are closed twice, we have to make
// sure the port number is not already in the list.
if (mDebuggerPorts.indexOf(port) == -1) {
// add the port to the list while keeping it sorted. It's not like there's
// going to be tons of objects so we do it linearly.
int count = mDebuggerPorts.size();
for (int i = 0 ; i < count ; i++) {
if (port < mDebuggerPorts.get(i)) {
mDebuggerPorts.add(i, port);
break;
}
}
// TODO: check if we can compact the end of the list.
}
}
}
}
/**
* Reads the length of the next message from a socket.
* @param socket The {@link SocketChannel} to read from.
* @return the length, or 0 (zero) if no data is available from the socket.
* @throws IOException if the connection failed.
*/
private int readLength(SocketChannel socket, byte[] buffer) throws IOException {
String msg = read(socket, buffer);
if (msg != null) {
try {
return Integer.parseInt(msg, 16);
} catch (NumberFormatException nfe) {
// we'll throw an exception below.
}
}
// we receive something we can't read. It's better to reset the connection at this point.
throw new IOException("Unable to read length");
}
/**
* Fills a buffer from a socket.
* @param socket
* @param buffer
* @return the content of the buffer as a string, or null if it failed to convert the buffer.
* @throws IOException
*/
private String read(SocketChannel socket, byte[] buffer) throws IOException {
ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length);
while (buf.position() != buf.limit()) {
int count;
count = socket.read(buf);
if (count < 0) {
throw new IOException("EOF");
}
}
try {
return new String(buffer, 0, buf.position(), AdbHelper.DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
// we'll return null below.
}
return null;
}
}

View File

@@ -0,0 +1,751 @@
/*
* 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 java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.security.InvalidParameterException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Provides control over emulated hardware of the Android emulator.
* <p/>This is basically a wrapper around the command line console normally used with telnet.
*<p/>
* Regarding line termination handling:<br>
* One of the issues is that the telnet protocol <b>requires</b> usage of <code>\r\n</code>. Most
* implementations don't enforce it (the dos one does). In this particular case, this is mostly
* irrelevant since we don't use telnet in Java, but that means we want to make
* sure we use the same line termination than what the console expects. The console
* code removes <code>\r</code> and waits for <code>\n</code>.
* <p/>However this means you <i>may</i> receive <code>\r\n</code> when reading from the console.
* <p/>
* <b>This API will change in the near future.</b>
*/
public final class EmulatorConsole {
private final static String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
private final static int WAIT_TIME = 5; // spin-wait sleep, in ms
private final static int STD_TIMEOUT = 5000; // standard delay, in ms
private final static String HOST = "127.0.0.1"; //$NON-NLS-1$
private final static String COMMAND_PING = "help\r\n"; //$NON-NLS-1$
private final static String COMMAND_AVD_NAME = "avd name\r\n"; //$NON-NLS-1$
private final static String COMMAND_KILL = "kill\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_STATUS = "gsm status\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_CALL = "gsm call %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_CANCEL_CALL = "gsm cancel %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_DATA = "gsm data %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GSM_VOICE = "gsm voice %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_SMS_SEND = "sms send %1$s %2$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_NETWORK_STATUS = "network status\r\n"; //$NON-NLS-1$
private final static String COMMAND_NETWORK_SPEED = "network speed %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_NETWORK_LATENCY = "network delay %1$s\r\n"; //$NON-NLS-1$
private final static String COMMAND_GPS =
"geo nmea $GPGGA,%1$02d%2$02d%3$02d.%4$03d," + //$NON-NLS-1$
"%5$03d%6$09.6f,%7$c,%8$03d%9$09.6f,%10$c," + //$NON-NLS-1$
"1,10,0.0,0.0,0,0.0,0,0.0,0000\r\n"; //$NON-NLS-1$
private final static Pattern RE_KO = Pattern.compile("KO:\\s+(.*)"); //$NON-NLS-1$
/**
* Array of delay values: no delay, gprs, edge/egprs, umts/3d
*/
public final static int[] MIN_LATENCIES = new int[] {
0, // No delay
150, // gprs
80, // edge/egprs
35 // umts/3g
};
/**
* Array of download speeds: full speed, gsm, hscsd, gprs, edge/egprs, umts/3g, hsdpa.
*/
public final int[] DOWNLOAD_SPEEDS = new int[] {
0, // full speed
14400, // gsm
43200, // hscsd
80000, // gprs
236800, // edge/egprs
1920000, // umts/3g
14400000 // hsdpa
};
/** Arrays of valid network speeds */
public final static String[] NETWORK_SPEEDS = new String[] {
"full", //$NON-NLS-1$
"gsm", //$NON-NLS-1$
"hscsd", //$NON-NLS-1$
"gprs", //$NON-NLS-1$
"edge", //$NON-NLS-1$
"umts", //$NON-NLS-1$
"hsdpa", //$NON-NLS-1$
};
/** Arrays of valid network latencies */
public final static String[] NETWORK_LATENCIES = new String[] {
"none", //$NON-NLS-1$
"gprs", //$NON-NLS-1$
"edge", //$NON-NLS-1$
"umts", //$NON-NLS-1$
};
/** Gsm Mode enum. */
public static enum GsmMode {
UNKNOWN((String)null),
UNREGISTERED(new String[] { "unregistered", "off" }),
HOME(new String[] { "home", "on" }),
ROAMING("roaming"),
SEARCHING("searching"),
DENIED("denied");
private final String[] tags;
GsmMode(String tag) {
if (tag != null) {
this.tags = new String[] { tag };
} else {
this.tags = new String[0];
}
}
GsmMode(String[] tags) {
this.tags = tags;
}
public static GsmMode getEnum(String tag) {
for (GsmMode mode : values()) {
for (String t : mode.tags) {
if (t.equals(tag)) {
return mode;
}
}
}
return UNKNOWN;
}
/**
* Returns the first tag of the enum.
*/
public String getTag() {
if (tags.length > 0) {
return tags[0];
}
return null;
}
}
public final static String RESULT_OK = null;
private final static Pattern sEmulatorRegexp = Pattern.compile(Device.RE_EMULATOR_SN);
private final static Pattern sVoiceStatusRegexp = Pattern.compile(
"gsm\\s+voice\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static Pattern sDataStatusRegexp = Pattern.compile(
"gsm\\s+data\\s+state:\\s*([a-z]+)", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static Pattern sDownloadSpeedRegexp = Pattern.compile(
"\\s+download\\s+speed:\\s+(\\d+)\\s+bits.*", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static Pattern sMinLatencyRegexp = Pattern.compile(
"\\s+minimum\\s+latency:\\s+(\\d+)\\s+ms", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static HashMap<Integer, EmulatorConsole> sEmulators =
new HashMap<Integer, EmulatorConsole>();
/** Gsm Status class */
public static class GsmStatus {
/** Voice status. */
public GsmMode voice = GsmMode.UNKNOWN;
/** Data status. */
public GsmMode data = GsmMode.UNKNOWN;
}
/** Network Status class */
public static class NetworkStatus {
/** network speed status. This is an index in the {@link #DOWNLOAD_SPEEDS} array. */
public int speed = -1;
/** network latency status. This is an index in the {@link #MIN_LATENCIES} array. */
public int latency = -1;
}
private int mPort;
private SocketChannel mSocketChannel;
private byte[] mBuffer = new byte[1024];
/**
* Returns an {@link EmulatorConsole} object for the given {@link Device}. This can
* be an already existing console, or a new one if it hadn't been created yet.
* @param d The device that the console links to.
* @return an <code>EmulatorConsole</code> object or <code>null</code> if the connection failed.
*/
public static synchronized EmulatorConsole getConsole(Device d) {
// we need to make sure that the device is an emulator
Matcher m = sEmulatorRegexp.matcher(d.serialNumber);
if (m.matches()) {
// get the port number. This is the console port.
int port;
try {
port = Integer.parseInt(m.group(1));
if (port <= 0) {
return null;
}
} catch (NumberFormatException e) {
// looks like we failed to get the port number. This is a bit strange since
// it's coming from a regexp that only accept digit, but we handle the case
// and return null.
return null;
}
EmulatorConsole console = sEmulators.get(port);
if (console != null) {
// if the console exist, we ping the emulator to check the connection.
if (console.ping() == false) {
RemoveConsole(console.mPort);
console = null;
}
}
if (console == null) {
// no console object exists for this port so we create one, and start
// the connection.
console = new EmulatorConsole(port);
if (console.start()) {
sEmulators.put(port, console);
} else {
console = null;
}
}
return console;
}
return null;
}
/**
* Removes the console object associated with a port from the map.
* @param port The port of the console to remove.
*/
private static synchronized void RemoveConsole(int port) {
sEmulators.remove(port);
}
private EmulatorConsole(int port) {
super();
mPort = port;
}
/**
* Starts the connection of the console.
* @return true if success.
*/
private boolean start() {
InetSocketAddress socketAddr;
try {
InetAddress hostAddr = InetAddress.getByName(HOST);
socketAddr = new InetSocketAddress(hostAddr, mPort);
} catch (UnknownHostException e) {
return false;
}
try {
mSocketChannel = SocketChannel.open(socketAddr);
} catch (IOException e1) {
return false;
}
// read some stuff from it
readLines();
return true;
}
/**
* Ping the emulator to check if the connection is still alive.
* @return true if the connection is alive.
*/
private synchronized boolean ping() {
// it looks like we can send stuff, even when the emulator quit, but we can't read
// from the socket. So we check the return of readLines()
if (sendCommand(COMMAND_PING)) {
return readLines() != null;
}
return false;
}
/**
* Sends a KILL command to the emulator.
*/
public synchronized void kill() {
if (sendCommand(COMMAND_KILL)) {
RemoveConsole(mPort);
}
}
public synchronized String getAvdName() {
if (sendCommand(COMMAND_AVD_NAME)) {
String[] result = readLines();
if (result != null && result.length == 2) { // this should be the name on first line,
// and ok on 2nd line
return result[0];
} else {
// try to see if there's a message after KO
Matcher m = RE_KO.matcher(result[result.length-1]);
if (m.matches()) {
return m.group(1);
}
}
}
return null;
}
/**
* Get the network status of the emulator.
* @return a {@link NetworkStatus} object containing the {@link GsmStatus}, or
* <code>null</code> if the query failed.
*/
public synchronized NetworkStatus getNetworkStatus() {
if (sendCommand(COMMAND_NETWORK_STATUS)) {
/* Result is in the format
Current network status:
download speed: 14400 bits/s (1.8 KB/s)
upload speed: 14400 bits/s (1.8 KB/s)
minimum latency: 0 ms
maximum latency: 0 ms
*/
String[] result = readLines();
if (isValid(result)) {
// we only compare agains the min latency and the download speed
// let's not rely on the order of the output, and simply loop through
// the line testing the regexp.
NetworkStatus status = new NetworkStatus();
for (String line : result) {
Matcher m = sDownloadSpeedRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.speed = getSpeedIndex(value);
// move on to next line.
continue;
}
m = sMinLatencyRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.latency = getLatencyIndex(value);
// move on to next line.
continue;
}
}
return status;
}
}
return null;
}
/**
* Returns the current gsm status of the emulator
* @return a {@link GsmStatus} object containing the gms status, or <code>null</code>
* if the query failed.
*/
public synchronized GsmStatus getGsmStatus() {
if (sendCommand(COMMAND_GSM_STATUS)) {
/*
* result is in the format:
* gsm status
* gsm voice state: home
* gsm data state: home
*/
String[] result = readLines();
if (isValid(result)) {
GsmStatus status = new GsmStatus();
// let's not rely on the order of the output, and simply loop through
// the line testing the regexp.
for (String line : result) {
Matcher m = sVoiceStatusRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.voice = GsmMode.getEnum(value.toLowerCase());
// move on to next line.
continue;
}
m = sDataStatusRegexp.matcher(line);
if (m.matches()) {
// get the string value
String value = m.group(1);
// get the index from the list
status.data = GsmMode.getEnum(value.toLowerCase());
// move on to next line.
continue;
}
}
return status;
}
}
return null;
}
/**
* Sets the GSM voice mode.
* @param mode the {@link GsmMode} value.
* @return RESULT_OK if success, an error String otherwise.
* @throws InvalidParameterException if mode is an invalid value.
*/
public synchronized String setGsmVoiceMode(GsmMode mode) throws InvalidParameterException {
if (mode == GsmMode.UNKNOWN) {
throw new InvalidParameterException();
}
String command = String.format(COMMAND_GSM_VOICE, mode.getTag());
return processCommand(command);
}
/**
* Sets the GSM data mode.
* @param mode the {@link GsmMode} value
* @return {@link #RESULT_OK} if success, an error String otherwise.
* @throws InvalidParameterException if mode is an invalid value.
*/
public synchronized String setGsmDataMode(GsmMode mode) throws InvalidParameterException {
if (mode == GsmMode.UNKNOWN) {
throw new InvalidParameterException();
}
String command = String.format(COMMAND_GSM_DATA, mode.getTag());
return processCommand(command);
}
/**
* Initiate an incoming call on the emulator.
* @param number a string representing the calling number.
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String call(String number) {
String command = String.format(COMMAND_GSM_CALL, number);
return processCommand(command);
}
/**
* Cancels a current call.
* @param number the number of the call to cancel
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String cancelCall(String number) {
String command = String.format(COMMAND_GSM_CANCEL_CALL, number);
return processCommand(command);
}
/**
* Sends an SMS to the emulator
* @param number The sender phone number
* @param message The SMS message. \ characters must be escaped. The carriage return is
* the 2 character sequence {'\', 'n' }
*
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String sendSms(String number, String message) {
String command = String.format(COMMAND_SMS_SEND, number, message);
return processCommand(command);
}
/**
* Sets the network speed.
* @param selectionIndex The index in the {@link #NETWORK_SPEEDS} table.
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String setNetworkSpeed(int selectionIndex) {
String command = String.format(COMMAND_NETWORK_SPEED, NETWORK_SPEEDS[selectionIndex]);
return processCommand(command);
}
/**
* Sets the network latency.
* @param selectionIndex The index in the {@link #NETWORK_LATENCIES} table.
* @return {@link #RESULT_OK} if success, an error String otherwise.
*/
public synchronized String setNetworkLatency(int selectionIndex) {
String command = String.format(COMMAND_NETWORK_LATENCY, NETWORK_LATENCIES[selectionIndex]);
return processCommand(command);
}
public synchronized String sendLocation(double longitude, double latitude, double elevation) {
Calendar c = Calendar.getInstance();
double absLong = Math.abs(longitude);
int longDegree = (int)Math.floor(absLong);
char longDirection = 'E';
if (longitude < 0) {
longDirection = 'W';
}
double longMinute = (absLong - Math.floor(absLong)) * 60;
double absLat = Math.abs(latitude);
int latDegree = (int)Math.floor(absLat);
char latDirection = 'N';
if (latitude < 0) {
latDirection = 'S';
}
double latMinute = (absLat - Math.floor(absLat)) * 60;
String command = String.format(COMMAND_GPS,
c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE),
c.get(Calendar.SECOND), c.get(Calendar.MILLISECOND),
latDegree, latMinute, latDirection,
longDegree, longMinute, longDirection);
return processCommand(command);
}
/**
* Sends a command to the emulator console.
* @param command The command string. <b>MUST BE TERMINATED BY \n</b>.
* @return true if success
*/
private boolean sendCommand(String command) {
boolean result = false;
try {
byte[] bCommand;
try {
bCommand = command.getBytes(DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
// wrong encoding...
return result;
}
// write the command
AdbHelper.write(mSocketChannel, bCommand, bCommand.length, AdbHelper.STD_TIMEOUT);
result = true;
} catch (IOException e) {
return false;
} finally {
if (result == false) {
// FIXME connection failed somehow, we need to disconnect the console.
RemoveConsole(mPort);
}
}
return result;
}
/**
* Sends a command to the emulator and parses its answer.
* @param command the command to send.
* @return {@link #RESULT_OK} if success, an error message otherwise.
*/
private String processCommand(String command) {
if (sendCommand(command)) {
String[] result = readLines();
if (result != null && result.length > 0) {
Matcher m = RE_KO.matcher(result[result.length-1]);
if (m.matches()) {
return m.group(1);
}
return RESULT_OK;
}
return "Unable to communicate with the emulator";
}
return "Unable to send command to the emulator";
}
/**
* Reads line from the console socket. This call is blocking until we read the lines:
* <ul>
* <li>OK\r\n</li>
* <li>KO<msg>\r\n</li>
* </ul>
* @return the array of strings read from the emulator.
*/
private String[] readLines() {
try {
ByteBuffer buf = ByteBuffer.wrap(mBuffer, 0, mBuffer.length);
int numWaits = 0;
boolean stop = false;
while (buf.position() != buf.limit() && stop == false) {
int count;
count = mSocketChannel.read(buf);
if (count < 0) {
return null;
} else if (count == 0) {
if (numWaits * WAIT_TIME > STD_TIMEOUT) {
return null;
}
// non-blocking spin
try {
Thread.sleep(WAIT_TIME);
} catch (InterruptedException ie) {
}
numWaits++;
} else {
numWaits = 0;
}
// check the last few char aren't OK. For a valid message to test
// we need at least 4 bytes (OK/KO + \r\n)
if (buf.position() >= 4) {
int pos = buf.position();
if (endsWithOK(pos) || lastLineIsKO(pos)) {
stop = true;
}
}
}
String msg = new String(mBuffer, 0, buf.position(), DEFAULT_ENCODING);
return msg.split("\r\n"); //$NON-NLS-1$
} catch (IOException e) {
return null;
}
}
/**
* Returns true if the 4 characters *before* the current position are "OK\r\n"
* @param currentPosition The current position
*/
private boolean endsWithOK(int currentPosition) {
if (mBuffer[currentPosition-1] == '\n' &&
mBuffer[currentPosition-2] == '\r' &&
mBuffer[currentPosition-3] == 'K' &&
mBuffer[currentPosition-4] == 'O') {
return true;
}
return false;
}
/**
* Returns true if the last line starts with KO and is also terminated by \r\n
* @param currentPosition the current position
*/
private boolean lastLineIsKO(int currentPosition) {
// first check that the last 2 characters are CRLF
if (mBuffer[currentPosition-1] != '\n' ||
mBuffer[currentPosition-2] != '\r') {
return false;
}
// now loop backward looking for the previous CRLF, or the beginning of the buffer
int i = 0;
for (i = currentPosition-3 ; i >= 0; i--) {
if (mBuffer[i] == '\n') {
// found \n!
if (i > 0 && mBuffer[i-1] == '\r') {
// found \r!
break;
}
}
}
// here it is either -1 if we reached the start of the buffer without finding
// a CRLF, or the position of \n. So in both case we look at the characters at i+1 and i+2
if (mBuffer[i+1] == 'K' && mBuffer[i+2] == 'O') {
// found error!
return true;
}
return false;
}
/**
* Returns true if the last line of the result does not start with KO
*/
private boolean isValid(String[] result) {
if (result != null && result.length > 0) {
return !(RE_KO.matcher(result[result.length-1]).matches());
}
return false;
}
private int getLatencyIndex(String value) {
try {
// get the int value
int latency = Integer.parseInt(value);
// check for the speed from the index
for (int i = 0 ; i < MIN_LATENCIES.length; i++) {
if (MIN_LATENCIES[i] == latency) {
return i;
}
}
} catch (NumberFormatException e) {
// Do nothing, we'll just return -1.
}
return -1;
}
private int getSpeedIndex(String value) {
try {
// get the int value
int speed = Integer.parseInt(value);
// check for the speed from the index
for (int i = 0 ; i < DOWNLOAD_SPEEDS.length; i++) {
if (DOWNLOAD_SPEEDS[i] == speed) {
return i;
}
}
} catch (NumberFormatException e) {
// Do nothing, we'll just return -1.
}
return -1;
}
}

View File

@@ -0,0 +1,767 @@
/*
* 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 java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Provides {@link Device} side file listing service.
* <p/>To get an instance for a known {@link Device}, call {@link Device#getFileListingService()}.
*/
public final class FileListingService {
/** Pattern to find filenames that match "*.apk" */
private final static Pattern sApkPattern =
Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$
private final static String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$
/** Pattern to parse the output of the 'pm -lf' command.<br>
* The output format looks like:<br>
* /data/app/myapp.apk=com.mypackage.myapp */
private final static Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$
/** Top level data folder. */
public final static String DIRECTORY_DATA = "data"; //$NON-NLS-1$
/** Top level sdcard folder. */
public final static String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$
/** Top level system folder. */
public final static String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$
/** Top level temp folder. */
public final static String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$
/** Application folder. */
public final static String DIRECTORY_APP = "app"; //$NON-NLS-1$
private final static String[] sRootLevelApprovedItems = {
DIRECTORY_DATA,
DIRECTORY_SDCARD,
DIRECTORY_SYSTEM,
DIRECTORY_TEMP
};
public static final long REFRESH_RATE = 5000L;
/**
* Refresh test has to be slightly lower for precision issue.
*/
static final long REFRESH_TEST = (long)(REFRESH_RATE * .8);
/** Entry type: File */
public static final int TYPE_FILE = 0;
/** Entry type: Directory */
public static final int TYPE_DIRECTORY = 1;
/** Entry type: Directory Link */
public static final int TYPE_DIRECTORY_LINK = 2;
/** Entry type: Block */
public static final int TYPE_BLOCK = 3;
/** Entry type: Character */
public static final int TYPE_CHARACTER = 4;
/** Entry type: Link */
public static final int TYPE_LINK = 5;
/** Entry type: Socket */
public static final int TYPE_SOCKET = 6;
/** Entry type: FIFO */
public static final int TYPE_FIFO = 7;
/** Entry type: Other */
public static final int TYPE_OTHER = 8;
/** Device side file separator. */
public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$
private static final String FILE_ROOT = "/"; //$NON-NLS-1$
/**
* Regexp pattern to parse the result from ls.
*/
private static Pattern sLsPattern = Pattern.compile(
"^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$
private Device mDevice;
private FileEntry mRoot;
private ArrayList<Thread> mThreadList = new ArrayList<Thread>();
/**
* Represents an entry in a directory. This can be a file or a directory.
*/
public final static class FileEntry {
/** Pattern to escape filenames for shell command consumption. */
private final static Pattern sEscapePattern = Pattern.compile(
"([\\\\()*+?\"'#/\\s])"); //$NON-NLS-1$
/**
* Comparator object for FileEntry
*/
private static Comparator<FileEntry> sEntryComparator = new Comparator<FileEntry>() {
public int compare(FileEntry o1, FileEntry o2) {
if (o1 instanceof FileEntry && o2 instanceof FileEntry) {
FileEntry fe1 = (FileEntry)o1;
FileEntry fe2 = (FileEntry)o2;
return fe1.name.compareTo(fe2.name);
}
return 0;
}
};
FileEntry parent;
String name;
String info;
String permissions;
String size;
String date;
String time;
String owner;
String group;
int type;
boolean isAppPackage;
boolean isRoot;
/**
* Indicates whether the entry content has been fetched yet, or not.
*/
long fetchTime = 0;
final ArrayList<FileEntry> mChildren = new ArrayList<FileEntry>();
/**
* Creates a new file entry.
* @param parent parent entry or null if entry is root
* @param name name of the entry.
* @param type entry type. Can be one of the following: {@link FileListingService#TYPE_FILE},
* {@link FileListingService#TYPE_DIRECTORY}, {@link FileListingService#TYPE_OTHER}.
*/
private FileEntry(FileEntry parent, String name, int type, boolean isRoot) {
this.parent = parent;
this.name = name;
this.type = type;
this.isRoot = isRoot;
checkAppPackageStatus();
}
/**
* Returns the name of the entry
*/
public String getName() {
return name;
}
/**
* Returns the size string of the entry, as returned by <code>ls</code>.
*/
public String getSize() {
return size;
}
/**
* Returns the size of the entry.
*/
public int getSizeValue() {
return Integer.parseInt(size);
}
/**
* Returns the date string of the entry, as returned by <code>ls</code>.
*/
public String getDate() {
return date;
}
/**
* Returns the time string of the entry, as returned by <code>ls</code>.
*/
public String getTime() {
return time;
}
/**
* Returns the permission string of the entry, as returned by <code>ls</code>.
*/
public String getPermissions() {
return permissions;
}
/**
* Returns the extra info for the entry.
* <p/>For a link, it will be a description of the link.
* <p/>For an application apk file it will be the application package as returned
* by the Package Manager.
*/
public String getInfo() {
return info;
}
/**
* Return the full path of the entry.
* @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator.
*/
public String getFullPath() {
if (isRoot) {
return FILE_ROOT;
}
StringBuilder pathBuilder = new StringBuilder();
fillPathBuilder(pathBuilder, false);
return pathBuilder.toString();
}
/**
* Return the fully escaped path of the entry. This path is safe to use in a
* shell command line.
* @return a path string using {@link FileListingService#FILE_SEPARATOR} as separator
*/
public String getFullEscapedPath() {
StringBuilder pathBuilder = new StringBuilder();
fillPathBuilder(pathBuilder, true);
return pathBuilder.toString();
}
/**
* Returns the path as a list of segments.
*/
public String[] getPathSegments() {
ArrayList<String> list = new ArrayList<String>();
fillPathSegments(list);
return list.toArray(new String[list.size()]);
}
/**
* Returns true if the entry is a directory, false otherwise;
*/
public int getType() {
return type;
}
/**
* Returns if the entry is a folder or a link to a folder.
*/
public boolean isDirectory() {
return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK;
}
/**
* Returns the parent entry.
*/
public FileEntry getParent() {
return parent;
}
/**
* Returns the cached children of the entry. This returns the cache created from calling
* <code>FileListingService.getChildren()</code>.
*/
public FileEntry[] getCachedChildren() {
return mChildren.toArray(new FileEntry[mChildren.size()]);
}
/**
* Returns the child {@link FileEntry} matching the name.
* This uses the cached children list.
* @param name the name of the child to return.
* @return the FileEntry matching the name or null.
*/
public FileEntry findChild(String name) {
for (FileEntry entry : mChildren) {
if (entry.name.equals(name)) {
return entry;
}
}
return null;
}
/**
* Returns whether the entry is the root.
*/
public boolean isRoot() {
return isRoot;
}
void addChild(FileEntry child) {
mChildren.add(child);
}
void setChildren(ArrayList<FileEntry> newChildren) {
mChildren.clear();
mChildren.addAll(newChildren);
}
boolean needFetch() {
if (fetchTime == 0) {
return true;
}
long current = System.currentTimeMillis();
if (current-fetchTime > REFRESH_TEST) {
return true;
}
return false;
}
/**
* Returns if the entry is a valid application package.
*/
public boolean isApplicationPackage() {
return isAppPackage;
}
/**
* Returns if the file name is an application package name.
*/
public boolean isAppFileName() {
Matcher m = sApkPattern.matcher(name);
return m.matches();
}
/**
* Recursively fills the pathBuilder with the full path
* @param pathBuilder a StringBuilder used to create the path.
* @param escapePath Whether the path need to be escaped for consumption by
* a shell command line.
*/
protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) {
if (isRoot) {
return;
}
if (parent != null) {
parent.fillPathBuilder(pathBuilder, escapePath);
}
pathBuilder.append(FILE_SEPARATOR);
pathBuilder.append(escapePath ? escape(name) : name);
}
/**
* Recursively fills the segment list with the full path.
* @param list The list of segments to fill.
*/
protected void fillPathSegments(ArrayList<String> list) {
if (isRoot) {
return;
}
if (parent != null) {
parent.fillPathSegments(list);
}
list.add(name);
}
/**
* Sets the internal app package status flag. This checks whether the entry is in an app
* directory like /data/app or /system/app
*/
private void checkAppPackageStatus() {
isAppPackage = false;
String[] segments = getPathSegments();
if (type == TYPE_FILE && segments.length == 3 && isAppFileName()) {
isAppPackage = DIRECTORY_APP.equals(segments[1]) &&
(DIRECTORY_SYSTEM.equals(segments[0]) || DIRECTORY_DATA.equals(segments[0]));
}
}
/**
* Returns an escaped version of the entry name.
* @param entryName
*/
private String escape(String entryName) {
return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$
}
}
private class LsReceiver extends MultiLineReceiver {
private ArrayList<FileEntry> mEntryList;
private ArrayList<String> mLinkList;
private FileEntry[] mCurrentChildren;
private FileEntry mParentEntry;
/**
* Create an ls receiver/parser.
* @param currentChildren The list of current children. To prevent
* collapse during update, reusing the same FileEntry objects for
* files that were already there is paramount.
* @param entryList the list of new children to be filled by the
* receiver.
* @param linkList the list of link path to compute post ls, to figure
* out if the link pointed to a file or to a directory.
*/
public LsReceiver(FileEntry parentEntry, ArrayList<FileEntry> entryList,
ArrayList<String> linkList) {
mParentEntry = parentEntry;
mCurrentChildren = parentEntry.getCachedChildren();
mEntryList = entryList;
mLinkList = linkList;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
// no need to handle empty lines.
if (line.length() == 0) {
continue;
}
// run the line through the regexp
Matcher m = sLsPattern.matcher(line);
if (m.matches() == false) {
continue;
}
// get the name
String name = m.group(7);
// if the parent is root, we only accept selected items
if (mParentEntry.isRoot()) {
boolean found = false;
for (String approved : sRootLevelApprovedItems) {
if (approved.equals(name)) {
found = true;
break;
}
}
// if it's not in the approved list we skip this entry.
if (found == false) {
continue;
}
}
// get the rest of the groups
String permissions = m.group(1);
String owner = m.group(2);
String group = m.group(3);
String size = m.group(4);
String date = m.group(5);
String time = m.group(6);
String info = null;
// and the type
int objectType = TYPE_OTHER;
switch (permissions.charAt(0)) {
case '-' :
objectType = TYPE_FILE;
break;
case 'b' :
objectType = TYPE_BLOCK;
break;
case 'c' :
objectType = TYPE_CHARACTER;
break;
case 'd' :
objectType = TYPE_DIRECTORY;
break;
case 'l' :
objectType = TYPE_LINK;
break;
case 's' :
objectType = TYPE_SOCKET;
break;
case 'p' :
objectType = TYPE_FIFO;
break;
}
// now check what we may be linking to
if (objectType == TYPE_LINK) {
String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$
// we should have 2 segments
if (segments.length == 2) {
// update the entry name to not contain the link
name = segments[0];
// and the link name
info = segments[1];
// now get the path to the link
String[] pathSegments = info.split(FILE_SEPARATOR);
if (pathSegments.length == 1) {
// the link is to something in the same directory,
// unless the link is ..
if ("..".equals(pathSegments[0])) { //$NON-NLS-1$
// set the type and we're done.
objectType = TYPE_DIRECTORY_LINK;
} else {
// either we found the object already
// or we'll find it later.
}
}
}
// add an arrow in front to specify it's a link.
info = "-> " + info; //$NON-NLS-1$;
}
// get the entry, either from an existing one, or a new one
FileEntry entry = getExistingEntry(name);
if (entry == null) {
entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */);
}
// add some misc info
entry.permissions = permissions;
entry.size = size;
entry.date = date;
entry.time = time;
entry.owner = owner;
entry.group = group;
if (objectType == TYPE_LINK) {
entry.info = info;
}
mEntryList.add(entry);
}
}
/**
* Queries for an already existing Entry per name
* @param name the name of the entry
* @return the existing FileEntry or null if no entry with a matching
* name exists.
*/
private FileEntry getExistingEntry(String name) {
for (int i = 0 ; i < mCurrentChildren.length; i++) {
FileEntry e = mCurrentChildren[i];
// since we're going to "erase" the one we use, we need to
// check that the item is not null.
if (e != null) {
// compare per name, case-sensitive.
if (name.equals(e.name)) {
// erase from the list
mCurrentChildren[i] = null;
// and return the object
return e;
}
}
}
// couldn't find any matching object, return null
return null;
}
public boolean isCancelled() {
return false;
}
public void finishLinks() {
// TODO Handle links in the listing service
}
}
/**
* Classes which implement this interface provide a method that deals with asynchronous
* result from <code>ls</code> command on the device.
*
* @see FileListingService#getChildren(com.android.ddmlib.FileListingService.FileEntry, boolean, com.android.ddmlib.FileListingService.IListingReceiver)
*/
public interface IListingReceiver {
public void setChildren(FileEntry entry, FileEntry[] children);
public void refreshEntry(FileEntry entry);
}
/**
* Creates a File Listing Service for a specified {@link Device}.
* @param device The Device the service is connected to.
*/
FileListingService(Device device) {
mDevice = device;
}
/**
* Returns the root element.
* @return the {@link FileEntry} object representing the root element or
* <code>null</code> if the device is invalid.
*/
public FileEntry getRoot() {
if (mDevice != null) {
if (mRoot == null) {
mRoot = new FileEntry(null /* parent */, "" /* name */, TYPE_DIRECTORY,
true /* isRoot */);
}
return mRoot;
}
return null;
}
/**
* Returns the children of a {@link FileEntry}.
* <p/>
* This method supports a cache mechanism and synchronous and asynchronous modes.
* <p/>
* If <var>receiver</var> is <code>null</code>, the device side <code>ls</code>
* command is done synchronously, and the method will return upon completion of the command.<br>
* If <var>receiver</var> is non <code>null</code>, the command is launched is a separate
* thread and upon completion, the receiver will be notified of the result.
* <p/>
* The result for each <code>ls</code> command is cached in the parent
* <code>FileEntry</code>. <var>useCache</var> allows usage of this cache, but only if the
* cache is valid. The cache is valid only for {@link FileListingService#REFRESH_RATE} ms.
* After that a new <code>ls</code> command is always executed.
* <p/>
* If the cache is valid and <code>useCache == true</code>, the method will always simply
* return the value of the cache, whether a {@link IListingReceiver} has been provided or not.
*
* @param entry The parent entry.
* @param useCache A flag to use the cache or to force a new ls command.
* @param receiver A receiver for asynchronous calls.
* @return The list of children or <code>null</code> for asynchronous calls.
*
* @see FileEntry#getCachedChildren()
*/
public FileEntry[] getChildren(final FileEntry entry, boolean useCache,
final IListingReceiver receiver) {
// first thing we do is check the cache, and if we already have a recent
// enough children list, we just return that.
if (useCache && entry.needFetch() == false) {
return entry.getCachedChildren();
}
// if there's no receiver, then this is a synchronous call, and we
// return the result of ls
if (receiver == null) {
doLs(entry);
return entry.getCachedChildren();
}
// this is a asynchronous call.
// we launch a thread that will do ls and give the listing
// to the receiver
Thread t = new Thread("ls " + entry.getFullPath()) { //$NON-NLS-1$
@Override
public void run() {
doLs(entry);
receiver.setChildren(entry, entry.getCachedChildren());
final FileEntry[] children = entry.getCachedChildren();
if (children.length > 0 && children[0].isApplicationPackage()) {
final HashMap<String, FileEntry> map = new HashMap<String, FileEntry>();
for (FileEntry child : children) {
String path = child.getFullPath();
map.put(path, child);
}
// call pm.
String command = PM_FULL_LISTING;
try {
mDevice.executeShellCommand(command, new MultiLineReceiver() {
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
if (line.length() > 0) {
// get the filepath and package from the line
Matcher m = sPmPattern.matcher(line);
if (m.matches()) {
// get the children with that path
FileEntry entry = map.get(m.group(1));
if (entry != null) {
entry.info = m.group(2);
receiver.refreshEntry(entry);
}
}
}
}
}
public boolean isCancelled() {
return false;
}
});
} catch (IOException e) {
// adb failed somehow, we do nothing.
}
}
// if another thread is pending, launch it
synchronized (mThreadList) {
// first remove ourselves from the list
mThreadList.remove(this);
// then launch the next one if applicable.
if (mThreadList.size() > 0) {
Thread t = mThreadList.get(0);
t.start();
}
}
}
};
// we don't want to run multiple ls on the device at the same time, so we
// store the thread in a list and launch it only if there's no other thread running.
// the thread will launch the next one once it's done.
synchronized (mThreadList) {
// add to the list
mThreadList.add(t);
// if it's the only one, launch it.
if (mThreadList.size() == 1) {
t.start();
}
}
// and we return null.
return null;
}
private void doLs(FileEntry entry) {
// create a list that will receive the list of the entries
ArrayList<FileEntry> entryList = new ArrayList<FileEntry>();
// create a list that will receive the link to compute post ls;
ArrayList<String> linkList = new ArrayList<String>();
try {
// create the command
String command = "ls -l " + entry.getFullPath(); //$NON-NLS-1$
// create the receiver object that will parse the result from ls
LsReceiver receiver = new LsReceiver(entry, entryList, linkList);
// call ls.
mDevice.executeShellCommand(command, receiver);
// finish the process of the receiver to handle links
receiver.finishLinks();
} catch (IOException e) {
}
// at this point we need to refresh the viewer
entry.fetchTime = System.currentTimeMillis();
// sort the children and set them as the new children
Collections.sort(entryList, FileEntry.sEntryComparator);
entry.setChildren(entryList);
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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 java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A receiver able to parse the result of the execution of
* {@link #GETPROP_COMMAND} on a device.
*/
final class GetPropReceiver extends MultiLineReceiver {
final static String GETPROP_COMMAND = "getprop"; //$NON-NLS-1$
private final static Pattern GETPROP_PATTERN = Pattern.compile("^\\[([^]]+)\\]\\:\\s*\\[(.*)\\]$"); //$NON-NLS-1$
/** indicates if we need to read the first */
private Device mDevice = null;
/**
* Creates the receiver with the device the receiver will modify.
* @param device The device to modify
*/
public GetPropReceiver(Device device) {
mDevice = device;
}
@Override
public void processNewLines(String[] lines) {
// We receive an array of lines. We're expecting
// to have the build info in the first line, and the build
// date in the 2nd line. There seems to be an empty line
// after all that.
for (String line : lines) {
if (line.length() == 0 || line.startsWith("#")) {
continue;
}
Matcher m = GETPROP_PATTERN.matcher(line);
if (m.matches()) {
String label = m.group(1);
String value = m.group(2);
if (label.length() > 0) {
mDevice.addProperty(label, value);
}
}
}
}
public boolean isCancelled() {
return false;
}
@Override
public void done() {
mDevice.update(Device.CHANGE_BUILD_INFO);
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
/**
* Handle the "app name" chunk (APNM).
*/
final class HandleAppName extends ChunkHandler {
public static final int CHUNK_APNM = ChunkHandler.type("APNM");
private static final HandleAppName mInst = new HandleAppName();
private HandleAppName() {}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {
mt.registerChunkHandler(CHUNK_APNM, mInst);
}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data,
boolean isReply, int msgId) {
Log.d("ddm-appname", "handling " + ChunkHandler.name(type));
if (type == CHUNK_APNM) {
assert !isReply;
handleAPNM(client, data);
} else {
handleUnknownChunk(client, type, data, isReply, msgId);
}
}
/*
* Handle a reply to our APNM message.
*/
private static void handleAPNM(Client client, ByteBuffer data) {
int appNameLen;
String appName;
appNameLen = data.getInt();
appName = getString(data, appNameLen);
Log.i("ddm-appname", "APNM: app='" + appName + "'");
ClientData cd = client.getClientData();
synchronized (cd) {
cd.setClientDescription(appName);
}
client = checkDebuggerPortForAppName(client, appName);
if (client != null) {
client.update(Client.CHANGE_NAME);
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
/**
* Submit an exit request.
*/
final class HandleExit extends ChunkHandler {
public static final int CHUNK_EXIT = type("EXIT");
private static final HandleExit mInst = new HandleExit();
private HandleExit() {}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
handleUnknownChunk(client, type, data, isReply, msgId);
}
/**
* Send an EXIT request to the client.
*/
public static void sendEXIT(Client client, int status)
throws IOException
{
ByteBuffer rawBuf = allocBuffer(4);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
buf.putInt(status);
finishChunkPacket(packet, CHUNK_EXIT, buf.position());
Log.d("ddm-exit", "Sending " + name(CHUNK_EXIT) + ": " + status);
client.sendAndConsume(packet, mInst);
}
}

View File

@@ -0,0 +1,497 @@
/*
* 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 java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
/**
* Handle heap status updates.
*/
final class HandleHeap extends ChunkHandler {
public static final int CHUNK_HPIF = type("HPIF");
public static final int CHUNK_HPST = type("HPST");
public static final int CHUNK_HPEN = type("HPEN");
public static final int CHUNK_HPSG = type("HPSG");
public static final int CHUNK_HPGC = type("HPGC");
public static final int CHUNK_REAE = type("REAE");
public static final int CHUNK_REAQ = type("REAQ");
public static final int CHUNK_REAL = type("REAL");
// args to sendHPSG
public static final int WHEN_DISABLE = 0;
public static final int WHEN_GC = 1;
public static final int WHAT_MERGE = 0; // merge adjacent objects
public static final int WHAT_OBJ = 1; // keep objects distinct
// args to sendHPIF
public static final int HPIF_WHEN_NEVER = 0;
public static final int HPIF_WHEN_NOW = 1;
public static final int HPIF_WHEN_NEXT_GC = 2;
public static final int HPIF_WHEN_EVERY_GC = 3;
private static final HandleHeap mInst = new HandleHeap();
private HandleHeap() {}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {
mt.registerChunkHandler(CHUNK_HPIF, mInst);
mt.registerChunkHandler(CHUNK_HPST, mInst);
mt.registerChunkHandler(CHUNK_HPEN, mInst);
mt.registerChunkHandler(CHUNK_HPSG, mInst);
mt.registerChunkHandler(CHUNK_REAQ, mInst);
mt.registerChunkHandler(CHUNK_REAL, mInst);
}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {
if (client.isHeapUpdateEnabled()) {
//sendHPSG(client, WHEN_GC, WHAT_MERGE);
sendHPIF(client, HPIF_WHEN_EVERY_GC);
}
}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
Log.d("ddm-heap", "handling " + ChunkHandler.name(type));
if (type == CHUNK_HPIF) {
handleHPIF(client, data);
client.update(Client.CHANGE_HEAP_DATA);
} else if (type == CHUNK_HPST) {
handleHPST(client, data);
} else if (type == CHUNK_HPEN) {
handleHPEN(client, data);
client.update(Client.CHANGE_HEAP_DATA);
} else if (type == CHUNK_HPSG) {
handleHPSG(client, data);
} else if (type == CHUNK_REAQ) {
handleREAQ(client, data);
client.update(Client.CHANGE_HEAP_ALLOCATION_STATUS);
} else if (type == CHUNK_REAL) {
handleREAL(client, data);
client.update(Client.CHANGE_HEAP_ALLOCATIONS);
} else {
handleUnknownChunk(client, type, data, isReply, msgId);
}
}
/*
* Handle a heap info message.
*/
private void handleHPIF(Client client, ByteBuffer data) {
Log.d("ddm-heap", "HPIF!");
try {
int numHeaps = data.getInt();
for (int i = 0; i < numHeaps; i++) {
int heapId = data.getInt();
@SuppressWarnings("unused")
long timeStamp = data.getLong();
@SuppressWarnings("unused")
byte reason = data.get();
long maxHeapSize = (long)data.getInt() & 0x00ffffffff;
long heapSize = (long)data.getInt() & 0x00ffffffff;
long bytesAllocated = (long)data.getInt() & 0x00ffffffff;
long objectsAllocated = (long)data.getInt() & 0x00ffffffff;
client.getClientData().setHeapInfo(heapId, maxHeapSize,
heapSize, bytesAllocated, objectsAllocated);
}
} catch (BufferUnderflowException ex) {
Log.w("ddm-heap", "malformed HPIF chunk from client");
}
}
/**
* Send an HPIF (HeaP InFo) request to the client.
*/
public static void sendHPIF(Client client, int when) throws IOException {
ByteBuffer rawBuf = allocBuffer(1);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
buf.put((byte)when);
finishChunkPacket(packet, CHUNK_HPIF, buf.position());
Log.d("ddm-heap", "Sending " + name(CHUNK_HPIF) + ": when=" + when);
client.sendAndConsume(packet, mInst);
}
/*
* Handle a heap segment series start message.
*/
private void handleHPST(Client client, ByteBuffer data) {
/* Clear out any data that's sitting around to
* get ready for the chunks that are about to come.
*/
//xxx todo: only clear data that belongs to the heap mentioned in <data>.
client.getClientData().getVmHeapData().clearHeapData();
}
/*
* Handle a heap segment series end message.
*/
private void handleHPEN(Client client, ByteBuffer data) {
/* Let the UI know that we've received all of the
* data for this heap.
*/
//xxx todo: only seal data that belongs to the heap mentioned in <data>.
client.getClientData().getVmHeapData().sealHeapData();
}
/*
* Handle a heap segment message.
*/
private void handleHPSG(Client client, ByteBuffer data) {
byte dataCopy[] = new byte[data.limit()];
data.rewind();
data.get(dataCopy);
data = ByteBuffer.wrap(dataCopy);
client.getClientData().getVmHeapData().addHeapData(data);
//xxx todo: add to the heap mentioned in <data>
}
/**
* Sends an HPSG (HeaP SeGment) request to the client.
*/
public static void sendHPSG(Client client, int when, int what)
throws IOException {
ByteBuffer rawBuf = allocBuffer(2);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
buf.put((byte)when);
buf.put((byte)what);
finishChunkPacket(packet, CHUNK_HPSG, buf.position());
Log.d("ddm-heap", "Sending " + name(CHUNK_HPSG) + ": when="
+ when + ", what=" + what);
client.sendAndConsume(packet, mInst);
}
/**
* Sends an HPGC request to the client.
*/
public static void sendHPGC(Client client)
throws IOException {
ByteBuffer rawBuf = allocBuffer(0);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
// no data
finishChunkPacket(packet, CHUNK_HPGC, buf.position());
Log.d("ddm-heap", "Sending " + name(CHUNK_HPGC));
client.sendAndConsume(packet, mInst);
}
/**
* Sends a REAE (REcent Allocation Enable) request to the client.
*/
public static void sendREAE(Client client, boolean enable)
throws IOException {
ByteBuffer rawBuf = allocBuffer(1);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
buf.put((byte) (enable ? 1 : 0));
finishChunkPacket(packet, CHUNK_REAE, buf.position());
Log.d("ddm-heap", "Sending " + name(CHUNK_REAE) + ": " + enable);
client.sendAndConsume(packet, mInst);
}
/**
* Sends a REAQ (REcent Allocation Query) request to the client.
*/
public static void sendREAQ(Client client)
throws IOException {
ByteBuffer rawBuf = allocBuffer(0);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
// no data
finishChunkPacket(packet, CHUNK_REAQ, buf.position());
Log.d("ddm-heap", "Sending " + name(CHUNK_REAQ));
client.sendAndConsume(packet, mInst);
}
/**
* Sends a REAL (REcent ALlocation) request to the client.
*/
public static void sendREAL(Client client)
throws IOException {
ByteBuffer rawBuf = allocBuffer(0);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
// no data
finishChunkPacket(packet, CHUNK_REAL, buf.position());
Log.d("ddm-heap", "Sending " + name(CHUNK_REAL));
client.sendAndConsume(packet, mInst);
}
/*
* Handle the response from our REcent Allocation Query message.
*/
private void handleREAQ(Client client, ByteBuffer data) {
boolean enabled;
enabled = (data.get() != 0);
Log.d("ddm-heap", "REAQ says: enabled=" + enabled);
client.getClientData().setAllocationStatus(enabled);
}
/**
* Converts a VM class descriptor string ("Landroid/os/Debug;") to
* a dot-notation class name ("android.os.Debug").
*/
private String descriptorToDot(String str) {
// count the number of arrays.
int array = 0;
while (str.startsWith("[")) {
str = str.substring(1);
array++;
}
int len = str.length();
/* strip off leading 'L' and trailing ';' if appropriate */
if (len >= 2 && str.charAt(0) == 'L' && str.charAt(len - 1) == ';') {
str = str.substring(1, len-1);
str = str.replace('/', '.');
} else {
// convert the basic types
if ("C".equals(str)) {
str = "char";
} else if ("B".equals(str)) {
str = "byte";
} else if ("Z".equals(str)) {
str = "boolean";
} else if ("S".equals(str)) {
str = "short";
} else if ("I".equals(str)) {
str = "int";
} else if ("J".equals(str)) {
str = "long";
} else if ("F".equals(str)) {
str = "float";
} else if ("D".equals(str)) {
str = "double";
}
}
// now add the array part
for (int a = 0 ; a < array; a++) {
str = str + "[]";
}
return str;
}
/**
* Reads a string table out of "data".
*
* This is just a serial collection of strings, each of which is a
* four-byte length followed by UTF-16 data.
*/
private void readStringTable(ByteBuffer data, String[] strings) {
int count = strings.length;
int i;
for (i = 0; i < count; i++) {
int nameLen = data.getInt();
String descriptor = getString(data, nameLen);
strings[i] = descriptorToDot(descriptor);
}
}
/*
* Handle a REcent ALlocation response.
*
* Message header (all values big-endian):
* (1b) message header len (to allow future expansion); includes itself
* (1b) entry header len
* (1b) stack frame len
* (2b) number of entries
* (4b) offset to string table from start of message
* (2b) number of class name strings
* (2b) number of method name strings
* (2b) number of source file name strings
* For each entry:
* (4b) total allocation size
* (2b) threadId
* (2b) allocated object's class name index
* (1b) stack depth
* For each stack frame:
* (2b) method's class name
* (2b) method name
* (2b) method source file
* (2b) line number, clipped to 32767; -2 if native; -1 if no source
* (xb) class name strings
* (xb) method name strings
* (xb) source file strings
*
* As with other DDM traffic, strings are sent as a 4-byte length
* followed by UTF-16 data.
*/
private void handleREAL(Client client, ByteBuffer data) {
Log.e("ddm-heap", "*** Received " + name(CHUNK_REAL));
int messageHdrLen, entryHdrLen, stackFrameLen;
int numEntries, offsetToStrings;
int numClassNames, numMethodNames, numFileNames;
/*
* Read the header.
*/
messageHdrLen = (data.get() & 0xff);
entryHdrLen = (data.get() & 0xff);
stackFrameLen = (data.get() & 0xff);
numEntries = (data.getShort() & 0xffff);
offsetToStrings = data.getInt();
numClassNames = (data.getShort() & 0xffff);
numMethodNames = (data.getShort() & 0xffff);
numFileNames = (data.getShort() & 0xffff);
/*
* Skip forward to the strings and read them.
*/
data.position(offsetToStrings);
String[] classNames = new String[numClassNames];
String[] methodNames = new String[numMethodNames];
String[] fileNames = new String[numFileNames];
readStringTable(data, classNames);
readStringTable(data, methodNames);
//System.out.println("METHODS: "
// + java.util.Arrays.deepToString(methodNames));
readStringTable(data, fileNames);
/*
* Skip back to a point just past the header and start reading
* entries.
*/
data.position(messageHdrLen);
ArrayList<AllocationInfo> list = new ArrayList<AllocationInfo>(numEntries);
for (int i = 0; i < numEntries; i++) {
int totalSize;
int threadId, classNameIndex, stackDepth;
totalSize = data.getInt();
threadId = (data.getShort() & 0xffff);
classNameIndex = (data.getShort() & 0xffff);
stackDepth = (data.get() & 0xff);
/* we've consumed 9 bytes; gobble up any extra */
for (int skip = 9; skip < entryHdrLen; skip++)
data.get();
StackTraceElement[] steArray = new StackTraceElement[stackDepth];
/*
* Pull out the stack trace.
*/
for (int sti = 0; sti < stackDepth; sti++) {
int methodClassNameIndex, methodNameIndex;
int methodSourceFileIndex;
short lineNumber;
String methodClassName, methodName, methodSourceFile;
methodClassNameIndex = (data.getShort() & 0xffff);
methodNameIndex = (data.getShort() & 0xffff);
methodSourceFileIndex = (data.getShort() & 0xffff);
lineNumber = data.getShort();
methodClassName = classNames[methodClassNameIndex];
methodName = methodNames[methodNameIndex];
methodSourceFile = fileNames[methodSourceFileIndex];
steArray[sti] = new StackTraceElement(methodClassName,
methodName, methodSourceFile, lineNumber);
/* we've consumed 8 bytes; gobble up any extra */
for (int skip = 9; skip < stackFrameLen; skip++)
data.get();
}
list.add(new AllocationInfo(classNames[classNameIndex],
totalSize, (short) threadId, steArray));
}
// sort biggest allocations first.
Collections.sort(list);
client.getClientData().setAllocations(list.toArray(new AllocationInfo[numEntries]));
}
/*
* For debugging: dump the contents of an AllocRecord array.
*
* The array starts with the oldest known allocation and ends with
* the most recent allocation.
*/
@SuppressWarnings("unused")
private static void dumpRecords(AllocationInfo[] records) {
System.out.println("Found " + records.length + " records:");
for (AllocationInfo rec: records) {
System.out.println("tid=" + rec.getThreadId() + " "
+ rec.getAllocatedClass() + " (" + rec.getSize() + " bytes)");
for (StackTraceElement ste: rec.getStackTrace()) {
if (ste.isNativeMethod()) {
System.out.println(" " + ste.getClassName()
+ "." + ste.getMethodName()
+ " (Native method)");
} else {
System.out.println(" " + ste.getClassName()
+ "." + ste.getMethodName()
+ " (" + ste.getFileName()
+ ":" + ste.getLineNumber() + ")");
}
}
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
/**
* Handle the "hello" chunk (HELO).
*/
final class HandleHello extends ChunkHandler {
public static final int CHUNK_HELO = ChunkHandler.type("HELO");
private static final HandleHello mInst = new HandleHello();
private HandleHello() {}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {
mt.registerChunkHandler(CHUNK_HELO, mInst);
}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {
Log.d("ddm-hello", "Now ready: " + client);
}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {
Log.d("ddm-hello", "Now disconnected: " + client);
}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
Log.d("ddm-hello", "handling " + ChunkHandler.name(type));
if (type == CHUNK_HELO) {
assert isReply;
handleHELO(client, data);
} else {
handleUnknownChunk(client, type, data, isReply, msgId);
}
}
/*
* Handle a reply to our HELO message.
*/
private static void handleHELO(Client client, ByteBuffer data) {
int version, pid, vmIdentLen, appNameLen;
String vmIdent, appName;
version = data.getInt();
pid = data.getInt();
vmIdentLen = data.getInt();
appNameLen = data.getInt();
vmIdent = getString(data, vmIdentLen);
appName = getString(data, appNameLen);
Log.d("ddm-hello", "HELO: v=" + version + ", pid=" + pid
+ ", vm='" + vmIdent + "', app='" + appName + "'");
ClientData cd = client.getClientData();
synchronized (cd) {
if (cd.getPid() == pid) {
cd.setVmIdentifier(vmIdent);
cd.setClientDescription(appName);
cd.isDdmAware(true);
} else {
Log.e("ddm-hello", "Received pid (" + pid + ") does not match client pid ("
+ cd.getPid() + ")");
}
}
client = checkDebuggerPortForAppName(client, appName);
if (client != null) {
client.update(Client.CHANGE_NAME);
}
}
/**
* Send a HELO request to the client.
*/
public static void sendHELO(Client client, int serverProtocolVersion)
throws IOException
{
ByteBuffer rawBuf = allocBuffer(4);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
buf.putInt(serverProtocolVersion);
finishChunkPacket(packet, CHUNK_HELO, buf.position());
Log.d("ddm-hello", "Sending " + name(CHUNK_HELO)
+ " ID=0x" + Integer.toHexString(packet.getId()));
client.sendAndConsume(packet, mInst);
}
}

View File

@@ -0,0 +1,305 @@
/*
* 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 java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Handle thread status updates.
*/
final class HandleNativeHeap extends ChunkHandler {
public static final int CHUNK_NHGT = type("NHGT"); // $NON-NLS-1$
public static final int CHUNK_NHSG = type("NHSG"); // $NON-NLS-1$
public static final int CHUNK_NHST = type("NHST"); // $NON-NLS-1$
public static final int CHUNK_NHEN = type("NHEN"); // $NON-NLS-1$
private static final HandleNativeHeap mInst = new HandleNativeHeap();
private HandleNativeHeap() {
}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {
mt.registerChunkHandler(CHUNK_NHGT, mInst);
mt.registerChunkHandler(CHUNK_NHSG, mInst);
mt.registerChunkHandler(CHUNK_NHST, mInst);
mt.registerChunkHandler(CHUNK_NHEN, mInst);
}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
Log.d("ddm-nativeheap", "handling " + ChunkHandler.name(type));
if (type == CHUNK_NHGT) {
handleNHGT(client, data);
} else if (type == CHUNK_NHST) {
// start chunk before any NHSG chunk(s)
client.getClientData().getNativeHeapData().clearHeapData();
} else if (type == CHUNK_NHEN) {
// end chunk after NHSG chunk(s)
client.getClientData().getNativeHeapData().sealHeapData();
} else if (type == CHUNK_NHSG) {
handleNHSG(client, data);
} else {
handleUnknownChunk(client, type, data, isReply, msgId);
}
client.update(Client.CHANGE_NATIVE_HEAP_DATA);
}
/**
* Send an NHGT (Native Thread GeT) request to the client.
*/
public static void sendNHGT(Client client) throws IOException {
ByteBuffer rawBuf = allocBuffer(0);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
// no data in request message
finishChunkPacket(packet, CHUNK_NHGT, buf.position());
Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHGT));
client.sendAndConsume(packet, mInst);
rawBuf = allocBuffer(2);
packet = new JdwpPacket(rawBuf);
buf = getChunkDataBuf(rawBuf);
buf.put((byte)HandleHeap.WHEN_GC);
buf.put((byte)HandleHeap.WHAT_OBJ);
finishChunkPacket(packet, CHUNK_NHSG, buf.position());
Log.d("ddm-nativeheap", "Sending " + name(CHUNK_NHSG));
client.sendAndConsume(packet, mInst);
}
/*
* Handle our native heap data.
*/
private void handleNHGT(Client client, ByteBuffer data) {
ClientData cd = client.getClientData();
Log.d("ddm-nativeheap", "NHGT: " + data.limit() + " bytes");
// TODO - process incoming data and save in "cd"
byte[] copy = new byte[data.limit()];
data.get(copy);
// clear the previous run
cd.clearNativeAllocationInfo();
ByteBuffer buffer = ByteBuffer.wrap(copy);
buffer.order(ByteOrder.LITTLE_ENDIAN);
// read the header
// typedef struct Header {
// uint32_t mapSize;
// uint32_t allocSize;
// uint32_t allocInfoSize;
// uint32_t totalMemory;
// uint32_t backtraceSize;
// };
int mapSize = buffer.getInt();
int allocSize = buffer.getInt();
int allocInfoSize = buffer.getInt();
int totalMemory = buffer.getInt();
int backtraceSize = buffer.getInt();
Log.d("ddms", "mapSize: " + mapSize);
Log.d("ddms", "allocSize: " + allocSize);
Log.d("ddms", "allocInfoSize: " + allocInfoSize);
Log.d("ddms", "totalMemory: " + totalMemory);
cd.setTotalNativeMemory(totalMemory);
// this means that updates aren't turned on.
if (allocInfoSize == 0)
return;
if (mapSize > 0) {
byte[] maps = new byte[mapSize];
buffer.get(maps, 0, mapSize);
parseMaps(cd, maps);
}
int iterations = allocSize / allocInfoSize;
for (int i = 0 ; i < iterations ; i++) {
NativeAllocationInfo info = new NativeAllocationInfo(
buffer.getInt() /* size */,
buffer.getInt() /* allocations */);
for (int j = 0 ; j < backtraceSize ; j++) {
long addr = ((long)buffer.getInt()) & 0x00000000ffffffffL;
info.addStackCallAddress(addr);;
}
cd.addNativeAllocation(info);
}
}
private void handleNHSG(Client client, ByteBuffer data) {
byte dataCopy[] = new byte[data.limit()];
data.rewind();
data.get(dataCopy);
data = ByteBuffer.wrap(dataCopy);
client.getClientData().getNativeHeapData().addHeapData(data);
if (true) {
return;
}
// WORK IN PROGRESS
// Log.e("ddm-nativeheap", "NHSG: ----------------------------------");
// Log.e("ddm-nativeheap", "NHSG: " + data.limit() + " bytes");
byte[] copy = new byte[data.limit()];
data.get(copy);
ByteBuffer buffer = ByteBuffer.wrap(copy);
buffer.order(ByteOrder.BIG_ENDIAN);
int id = buffer.getInt();
int unitsize = (int) buffer.get();
long startAddress = (long) buffer.getInt() & 0x00000000ffffffffL;
int offset = buffer.getInt();
int allocationUnitCount = buffer.getInt();
// Log.e("ddm-nativeheap", "id: " + id);
// Log.e("ddm-nativeheap", "unitsize: " + unitsize);
// Log.e("ddm-nativeheap", "startAddress: 0x" + Long.toHexString(startAddress));
// Log.e("ddm-nativeheap", "offset: " + offset);
// Log.e("ddm-nativeheap", "allocationUnitCount: " + allocationUnitCount);
// Log.e("ddm-nativeheap", "end: 0x" +
// Long.toHexString(startAddress + unitsize * allocationUnitCount));
// read the usage
while (buffer.position() < buffer.limit()) {
int eState = (int)buffer.get() & 0x000000ff;
int eLen = ((int)buffer.get() & 0x000000ff) + 1;
//Log.e("ddm-nativeheap", "solidity: " + (eState & 0x7) + " - kind: "
// + ((eState >> 3) & 0x7) + " - len: " + eLen);
}
// count += unitsize * allocationUnitCount;
// Log.e("ddm-nativeheap", "count = " + count);
}
private void parseMaps(ClientData cd, byte[] maps) {
InputStreamReader input = new InputStreamReader(new ByteArrayInputStream(maps));
BufferedReader reader = new BufferedReader(input);
String line;
try {
// most libraries are defined on several lines, so we need to make sure we parse
// all the library lines and only add the library at the end
long startAddr = 0;
long endAddr = 0;
String library = null;
while ((line = reader.readLine()) != null) {
Log.d("ddms", "line: " + line);
if (line.length() < 16) {
continue;
}
try {
long tmpStart = Long.parseLong(line.substring(0, 8), 16);
long tmpEnd = Long.parseLong(line.substring(9, 17), 16);
/*
* only check for library addresses as defined in
* //device/config/prelink-linux-arm.map
*/
if (tmpStart >= 0x0000000080000000L && tmpStart <= 0x00000000BFFFFFFFL) {
int index = line.indexOf('/');
if (index == -1)
continue;
String tmpLib = line.substring(index);
if (library == null ||
(library != null && tmpLib.equals(library) == false)) {
if (library != null) {
cd.addNativeLibraryMapInfo(startAddr, endAddr, library);
Log.d("ddms", library + "(" + Long.toHexString(startAddr) +
" - " + Long.toHexString(endAddr) + ")");
}
// now init the new library
library = tmpLib;
startAddr = tmpStart;
endAddr = tmpEnd;
} else {
// add the new end
endAddr = tmpEnd;
}
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
if (library != null) {
cd.addNativeLibraryMapInfo(startAddr, endAddr, library);
Log.d("ddms", library + "(" + Long.toHexString(startAddr) +
" - " + Long.toHexString(endAddr) + ")");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.LogLevel;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Handle thread status updates.
*/
final class HandleTest extends ChunkHandler {
public static final int CHUNK_TEST = type("TEST");
private static final HandleTest mInst = new HandleTest();
private HandleTest() {}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {
mt.registerChunkHandler(CHUNK_TEST, mInst);
}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
Log.d("ddm-test", "handling " + ChunkHandler.name(type));
if (type == CHUNK_TEST) {
handleTEST(client, data);
} else {
handleUnknownChunk(client, type, data, isReply, msgId);
}
}
/*
* Handle a thread creation message.
*/
private void handleTEST(Client client, ByteBuffer data)
{
/*
* Can't call data.array() on a read-only ByteBuffer, so we make
* a copy.
*/
byte[] copy = new byte[data.limit()];
data.get(copy);
Log.d("ddm-test", "Received:");
Log.hexDump("ddm-test", LogLevel.DEBUG, copy, 0, copy.length);
}
}

View File

@@ -0,0 +1,379 @@
/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
/**
* Handle thread status updates.
*/
final class HandleThread extends ChunkHandler {
public static final int CHUNK_THEN = type("THEN");
public static final int CHUNK_THCR = type("THCR");
public static final int CHUNK_THDE = type("THDE");
public static final int CHUNK_THST = type("THST");
public static final int CHUNK_THNM = type("THNM");
public static final int CHUNK_STKL = type("STKL");
private static final HandleThread mInst = new HandleThread();
// only read/written by requestThreadUpdates()
private static volatile boolean mThreadStatusReqRunning = false;
private static volatile boolean mThreadStackTraceReqRunning = false;
private HandleThread() {}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {
mt.registerChunkHandler(CHUNK_THCR, mInst);
mt.registerChunkHandler(CHUNK_THDE, mInst);
mt.registerChunkHandler(CHUNK_THST, mInst);
mt.registerChunkHandler(CHUNK_THNM, mInst);
mt.registerChunkHandler(CHUNK_STKL, mInst);
}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {
Log.d("ddm-thread", "Now ready: " + client);
if (client.isThreadUpdateEnabled())
sendTHEN(client, true);
}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
Log.d("ddm-thread", "handling " + ChunkHandler.name(type));
if (type == CHUNK_THCR) {
handleTHCR(client, data);
} else if (type == CHUNK_THDE) {
handleTHDE(client, data);
} else if (type == CHUNK_THST) {
handleTHST(client, data);
} else if (type == CHUNK_THNM) {
handleTHNM(client, data);
} else if (type == CHUNK_STKL) {
handleSTKL(client, data);
} else {
handleUnknownChunk(client, type, data, isReply, msgId);
}
}
/*
* Handle a thread creation message.
*
* We should be tolerant of receiving a duplicate create message. (It
* shouldn't happen with the current implementation.)
*/
private void handleTHCR(Client client, ByteBuffer data) {
int threadId, nameLen;
String name;
threadId = data.getInt();
nameLen = data.getInt();
name = getString(data, nameLen);
Log.v("ddm-thread", "THCR: " + threadId + " '" + name + "'");
client.getClientData().addThread(threadId, name);
client.update(Client.CHANGE_THREAD_DATA);
}
/*
* Handle a thread death message.
*/
private void handleTHDE(Client client, ByteBuffer data) {
int threadId;
threadId = data.getInt();
Log.v("ddm-thread", "THDE: " + threadId);
client.getClientData().removeThread(threadId);
client.update(Client.CHANGE_THREAD_DATA);
}
/*
* Handle a thread status update message.
*
* Response has:
* (1b) header len
* (1b) bytes per entry
* (2b) thread count
* Then, for each thread:
* (4b) threadId (matches value from THCR)
* (1b) thread status
* (4b) tid
* (4b) utime
* (4b) stime
*/
private void handleTHST(Client client, ByteBuffer data) {
int headerLen, bytesPerEntry, extraPerEntry;
int threadCount;
headerLen = (data.get() & 0xff);
bytesPerEntry = (data.get() & 0xff);
threadCount = data.getShort();
headerLen -= 4; // we've read 4 bytes
while (headerLen-- > 0)
data.get();
extraPerEntry = bytesPerEntry - 18; // we want 18 bytes
Log.v("ddm-thread", "THST: threadCount=" + threadCount);
/*
* For each thread, extract the data, find the appropriate
* client, and add it to the ClientData.
*/
for (int i = 0; i < threadCount; i++) {
int threadId, status, tid, utime, stime;
boolean isDaemon = false;
threadId = data.getInt();
status = data.get();
tid = data.getInt();
utime = data.getInt();
stime = data.getInt();
if (bytesPerEntry >= 18)
isDaemon = (data.get() != 0);
Log.v("ddm-thread", " id=" + threadId
+ ", status=" + status + ", tid=" + tid
+ ", utime=" + utime + ", stime=" + stime);
ClientData cd = client.getClientData();
ThreadInfo threadInfo = cd.getThread(threadId);
if (threadInfo != null)
threadInfo.updateThread(status, tid, utime, stime, isDaemon);
else
Log.i("ddms", "Thread with id=" + threadId + " not found");
// slurp up any extra
for (int slurp = extraPerEntry; slurp > 0; slurp--)
data.get();
}
client.update(Client.CHANGE_THREAD_DATA);
}
/*
* Handle a THNM (THread NaMe) message. We get one of these after
* somebody calls Thread.setName() on a running thread.
*/
private void handleTHNM(Client client, ByteBuffer data) {
int threadId, nameLen;
String name;
threadId = data.getInt();
nameLen = data.getInt();
name = getString(data, nameLen);
Log.v("ddm-thread", "THNM: " + threadId + " '" + name + "'");
ThreadInfo threadInfo = client.getClientData().getThread(threadId);
if (threadInfo != null) {
threadInfo.setThreadName(name);
client.update(Client.CHANGE_THREAD_DATA);
} else {
Log.i("ddms", "Thread with id=" + threadId + " not found");
}
}
/**
* Parse an incoming STKL.
*/
private void handleSTKL(Client client, ByteBuffer data) {
StackTraceElement[] trace;
int i, threadId, stackDepth;
@SuppressWarnings("unused")
int future;
future = data.getInt();
threadId = data.getInt();
Log.v("ddms", "STKL: " + threadId);
/* un-serialize the StackTraceElement[] */
stackDepth = data.getInt();
trace = new StackTraceElement[stackDepth];
for (i = 0; i < stackDepth; i++) {
String className, methodName, fileName;
int len, lineNumber;
len = data.getInt();
className = getString(data, len);
len = data.getInt();
methodName = getString(data, len);
len = data.getInt();
if (len == 0) {
fileName = null;
} else {
fileName = getString(data, len);
}
lineNumber = data.getInt();
trace[i] = new StackTraceElement(className, methodName, fileName,
lineNumber);
}
ThreadInfo threadInfo = client.getClientData().getThread(threadId);
if (threadInfo != null) {
threadInfo.setStackCall(trace);
client.update(Client.CHANGE_THREAD_STACKTRACE);
} else {
Log.d("STKL", String.format(
"Got stackcall for thread %1$d, which does not exists (anymore?).", //$NON-NLS-1$
threadId));
}
}
/**
* Send a THEN (THread notification ENable) request to the client.
*/
public static void sendTHEN(Client client, boolean enable)
throws IOException {
ByteBuffer rawBuf = allocBuffer(1);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
if (enable)
buf.put((byte)1);
else
buf.put((byte)0);
finishChunkPacket(packet, CHUNK_THEN, buf.position());
Log.d("ddm-thread", "Sending " + name(CHUNK_THEN) + ": " + enable);
client.sendAndConsume(packet, mInst);
}
/**
* Send a STKL (STacK List) request to the client. The VM will suspend
* the target thread, obtain its stack, and return it. If the thread
* is no longer running, a failure result will be returned.
*/
public static void sendSTKL(Client client, int threadId)
throws IOException {
if (false) {
Log.i("ddm-thread", "would send STKL " + threadId);
return;
}
ByteBuffer rawBuf = allocBuffer(4);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
buf.putInt(threadId);
finishChunkPacket(packet, CHUNK_STKL, buf.position());
Log.d("ddm-thread", "Sending " + name(CHUNK_STKL) + ": " + threadId);
client.sendAndConsume(packet, mInst);
}
/**
* This is called periodically from the UI thread. To avoid locking
* the UI while we request the updates, we create a new thread.
*
*/
static void requestThreadUpdate(final Client client) {
if (client.isDdmAware() && client.isThreadUpdateEnabled()) {
if (mThreadStatusReqRunning) {
Log.w("ddms", "Waiting for previous thread update req to finish");
return;
}
new Thread("Thread Status Req") {
@Override
public void run() {
mThreadStatusReqRunning = true;
try {
sendTHST(client);
} catch (IOException ioe) {
Log.i("ddms", "Unable to request thread updates from "
+ client + ": " + ioe.getMessage());
} finally {
mThreadStatusReqRunning = false;
}
}
}.start();
}
}
static void requestThreadStackCallRefresh(final Client client, final int threadId) {
if (client.isDdmAware() && client.isThreadUpdateEnabled()) {
if (mThreadStackTraceReqRunning ) {
Log.w("ddms", "Waiting for previous thread stack call req to finish");
return;
}
new Thread("Thread Status Req") {
@Override
public void run() {
mThreadStackTraceReqRunning = true;
try {
sendSTKL(client, threadId);
} catch (IOException ioe) {
Log.i("ddms", "Unable to request thread stack call updates from "
+ client + ": " + ioe.getMessage());
} finally {
mThreadStackTraceReqRunning = false;
}
}
}.start();
}
}
/*
* Send a THST request to the specified client.
*/
private static void sendTHST(Client client) throws IOException {
ByteBuffer rawBuf = allocBuffer(0);
JdwpPacket packet = new JdwpPacket(rawBuf);
ByteBuffer buf = getChunkDataBuf(rawBuf);
// nothing much to say
finishChunkPacket(packet, CHUNK_THST, buf.position());
Log.d("ddm-thread", "Sending " + name(CHUNK_THST));
client.sendAndConsume(packet, mInst);
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
/**
* Handle the "wait" chunk (WAIT). These are sent up when the client is
* waiting for something, e.g. for a debugger to attach.
*/
final class HandleWait extends ChunkHandler {
public static final int CHUNK_WAIT = ChunkHandler.type("WAIT");
private static final HandleWait mInst = new HandleWait();
private HandleWait() {}
/**
* Register for the packets we expect to get from the client.
*/
public static void register(MonitorThread mt) {
mt.registerChunkHandler(CHUNK_WAIT, mInst);
}
/**
* Client is ready.
*/
@Override
public void clientReady(Client client) throws IOException {}
/**
* Client went away.
*/
@Override
public void clientDisconnected(Client client) {}
/**
* Chunk handler entry point.
*/
@Override
public void handleChunk(Client client, int type, ByteBuffer data, boolean isReply, int msgId) {
Log.d("ddm-wait", "handling " + ChunkHandler.name(type));
if (type == CHUNK_WAIT) {
assert !isReply;
handleWAIT(client, data);
} else {
handleUnknownChunk(client, type, data, isReply, msgId);
}
}
/*
* Handle a reply to our WAIT message.
*/
private static void handleWAIT(Client client, ByteBuffer data) {
byte reason;
reason = data.get();
Log.i("ddm-wait", "WAIT: reason=" + reason);
ClientData cd = client.getClientData();
synchronized (cd) {
cd.setDebuggerConnectionStatus(ClientData.DEBUGGER_WAITING);
}
client.update(Client.CHANGE_DEBUGGER_INTEREST);
}
}

View File

@@ -0,0 +1,446 @@
/*
* 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 java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.ParseException;
/**
* Describes the types and locations of objects in a segment of a heap.
*/
public final class HeapSegment implements Comparable<HeapSegment> {
/**
* Describes an object/region encoded in the HPSG data.
*/
public static class HeapSegmentElement implements Comparable<HeapSegmentElement> {
/*
* Solidity values, which must match the values in
* the HPSG data.
*/
/** The element describes a free block. */
public static int SOLIDITY_FREE = 0;
/** The element is strongly-reachable. */
public static int SOLIDITY_HARD = 1;
/** The element is softly-reachable. */
public static int SOLIDITY_SOFT = 2;
/** The element is weakly-reachable. */
public static int SOLIDITY_WEAK = 3;
/** The element is phantom-reachable. */
public static int SOLIDITY_PHANTOM = 4;
/** The element is pending finalization. */
public static int SOLIDITY_FINALIZABLE = 5;
/** The element is not reachable, and is about to be swept/freed. */
public static int SOLIDITY_SWEEP = 6;
/** The reachability of the object is unknown. */
public static int SOLIDITY_INVALID = -1;
/*
* Kind values, which must match the values in
* the HPSG data.
*/
/** The element describes a data object. */
public static int KIND_OBJECT = 0;
/** The element describes a class object. */
public static int KIND_CLASS_OBJECT = 1;
/** The element describes an array of 1-byte elements. */
public static int KIND_ARRAY_1 = 2;
/** The element describes an array of 2-byte elements. */
public static int KIND_ARRAY_2 = 3;
/** The element describes an array of 4-byte elements. */
public static int KIND_ARRAY_4 = 4;
/** The element describes an array of 8-byte elements. */
public static int KIND_ARRAY_8 = 5;
/** The element describes an unknown type of object. */
public static int KIND_UNKNOWN = 6;
/** The element describes a native object. */
public static int KIND_NATIVE = 7;
/** The object kind is unknown or unspecified. */
public static int KIND_INVALID = -1;
/**
* A bit in the HPSG data that indicates that an element should
* be combined with the element that follows, typically because
* an element is too large to be described by a single element.
*/
private static int PARTIAL_MASK = 1 << 7;
/**
* Describes the reachability/solidity of the element. Must
* be set to one of the SOLIDITY_* values.
*/
private int mSolidity;
/**
* Describes the type/kind of the element. Must be set to one
* of the KIND_* values.
*/
private int mKind;
/**
* Describes the length of the element, in bytes.
*/
private int mLength;
/**
* Creates an uninitialized element.
*/
public HeapSegmentElement() {
setSolidity(SOLIDITY_INVALID);
setKind(KIND_INVALID);
setLength(-1);
}
/**
* Create an element describing the entry at the current
* position of hpsgData.
*
* @param hs The heap segment to pull the entry from.
* @throws BufferUnderflowException if there is not a whole entry
* following the current position
* of hpsgData.
* @throws ParseException if the provided data is malformed.
*/
public HeapSegmentElement(HeapSegment hs)
throws BufferUnderflowException, ParseException {
set(hs);
}
/**
* Replace the element with the entry at the current position of
* hpsgData.
*
* @param hs The heap segment to pull the entry from.
* @return this object.
* @throws BufferUnderflowException if there is not a whole entry
* following the current position of
* hpsgData.
* @throws ParseException if the provided data is malformed.
*/
public HeapSegmentElement set(HeapSegment hs)
throws BufferUnderflowException, ParseException {
/* TODO: Maybe keep track of the virtual address of each element
* so that they can be examined independently.
*/
ByteBuffer data = hs.mUsageData;
int eState = (int)data.get() & 0x000000ff;
int eLen = ((int)data.get() & 0x000000ff) + 1;
while ((eState & PARTIAL_MASK) != 0) {
/* If the partial bit was set, the next byte should describe
* the same object as the current one.
*/
int nextState = (int)data.get() & 0x000000ff;
if ((nextState & ~PARTIAL_MASK) != (eState & ~PARTIAL_MASK)) {
throw new ParseException("State mismatch", data.position());
}
eState = nextState;
eLen += ((int)data.get() & 0x000000ff) + 1;
}
setSolidity(eState & 0x7);
setKind((eState >> 3) & 0x7);
setLength(eLen * hs.mAllocationUnitSize);
return this;
}
public int getSolidity() {
return mSolidity;
}
public void setSolidity(int solidity) {
this.mSolidity = solidity;
}
public int getKind() {
return mKind;
}
public void setKind(int kind) {
this.mKind = kind;
}
public int getLength() {
return mLength;
}
public void setLength(int length) {
this.mLength = length;
}
public int compareTo(HeapSegmentElement other) {
if (mLength != other.mLength) {
return mLength < other.mLength ? -1 : 1;
}
return 0;
}
}
//* The ID of the heap that this segment belongs to.
protected int mHeapId;
//* The size of an allocation unit, in bytes. (e.g., 8 bytes)
protected int mAllocationUnitSize;
//* The virtual address of the start of this segment.
protected long mStartAddress;
//* The offset of this pices from mStartAddress, in bytes.
protected int mOffset;
//* The number of allocation units described in this segment.
protected int mAllocationUnitCount;
//* The raw data that describes the contents of this segment.
protected ByteBuffer mUsageData;
//* mStartAddress is set to this value when the segment becomes invalid.
private final static long INVALID_START_ADDRESS = -1;
/**
* Create a new HeapSegment based on the raw contents
* of an HPSG chunk.
*
* @param hpsgData The raw data from an HPSG chunk.
* @throws BufferUnderflowException if hpsgData is too small
* to hold the HPSG chunk header data.
*/
public HeapSegment(ByteBuffer hpsgData) throws BufferUnderflowException {
/* Read the HPSG chunk header.
* These get*() calls may throw a BufferUnderflowException
* if the underlying data isn't big enough.
*/
hpsgData.order(ByteOrder.BIG_ENDIAN);
mHeapId = hpsgData.getInt();
mAllocationUnitSize = (int) hpsgData.get();
mStartAddress = (long) hpsgData.getInt() & 0x00000000ffffffffL;
mOffset = hpsgData.getInt();
mAllocationUnitCount = hpsgData.getInt();
// Hold onto the remainder of the data.
mUsageData = hpsgData.slice();
mUsageData.order(ByteOrder.BIG_ENDIAN); // doesn't actually matter
// Validate the data.
//xxx do it
//xxx make sure the number of elements matches mAllocationUnitCount.
//xxx make sure the last element doesn't have P set
}
/**
* See if this segment still contains data, and has not been
* appended to another segment.
*
* @return true if this segment has not been appended to
* another segment.
*/
public boolean isValid() {
return mStartAddress != INVALID_START_ADDRESS;
}
/**
* See if <code>other</code> comes immediately after this segment.
*
* @param other The HeapSegment to check.
* @return true if <code>other</code> comes immediately after this
* segment.
*/
public boolean canAppend(HeapSegment other) {
return isValid() && other.isValid() && mHeapId == other.mHeapId &&
mAllocationUnitSize == other.mAllocationUnitSize &&
getEndAddress() == other.getStartAddress();
}
/**
* Append the contents of <code>other</code> to this segment
* if it describes the segment immediately after this one.
*
* @param other The segment to append to this segment, if possible.
* If appended, <code>other</code> will be invalid
* when this method returns.
* @return true if <code>other</code> was successfully appended to
* this segment.
*/
public boolean append(HeapSegment other) {
if (canAppend(other)) {
/* Preserve the position. The mark is not preserved,
* but we don't use it anyway.
*/
int pos = mUsageData.position();
// Guarantee that we have enough room for the new data.
if (mUsageData.capacity() - mUsageData.limit() <
other.mUsageData.limit()) {
/* Grow more than necessary in case another append()
* is about to happen.
*/
int newSize = mUsageData.limit() + other.mUsageData.limit();
ByteBuffer newData = ByteBuffer.allocate(newSize * 2);
mUsageData.rewind();
newData.put(mUsageData);
mUsageData = newData;
}
// Copy the data from the other segment and restore the position.
other.mUsageData.rewind();
mUsageData.put(other.mUsageData);
mUsageData.position(pos);
// Fix this segment's header to cover the new data.
mAllocationUnitCount += other.mAllocationUnitCount;
// Mark the other segment as invalid.
other.mStartAddress = INVALID_START_ADDRESS;
other.mUsageData = null;
return true;
} else {
return false;
}
}
public long getStartAddress() {
return mStartAddress + mOffset;
}
public int getLength() {
return mAllocationUnitSize * mAllocationUnitCount;
}
public long getEndAddress() {
return getStartAddress() + getLength();
}
public void rewindElements() {
if (mUsageData != null) {
mUsageData.rewind();
}
}
public HeapSegmentElement getNextElement(HeapSegmentElement reuse) {
try {
if (reuse != null) {
return reuse.set(this);
} else {
return new HeapSegmentElement(this);
}
} catch (BufferUnderflowException ex) {
/* Normal "end of buffer" situation.
*/
} catch (ParseException ex) {
/* Malformed data.
*/
//TODO: we should catch this in the constructor
}
return null;
}
/*
* Method overrides for Comparable
*/
@Override
public boolean equals(Object o) {
if (o instanceof HeapSegment) {
return compareTo((HeapSegment) o) == 0;
}
return false;
}
@Override
public int hashCode() {
return mHeapId * 31 +
mAllocationUnitSize * 31 +
(int) mStartAddress * 31 +
mOffset * 31 +
mAllocationUnitCount * 31 +
mUsageData.hashCode();
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("HeapSegment { heap ").append(mHeapId)
.append(", start 0x")
.append(Integer.toHexString((int) getStartAddress()))
.append(", length ").append(getLength())
.append(" }");
return str.toString();
}
public int compareTo(HeapSegment other) {
if (mHeapId != other.mHeapId) {
return mHeapId < other.mHeapId ? -1 : 1;
}
if (getStartAddress() != other.getStartAddress()) {
return getStartAddress() < other.getStartAddress() ? -1 : 1;
}
/* If two segments have the same start address, the rest of
* the fields should be equal. Go through the motions, though.
* Note that we re-check the components of getStartAddress()
* (mStartAddress and mOffset) to make sure that all fields in
* an equal segment are equal.
*/
if (mAllocationUnitSize != other.mAllocationUnitSize) {
return mAllocationUnitSize < other.mAllocationUnitSize ? -1 : 1;
}
if (mStartAddress != other.mStartAddress) {
return mStartAddress < other.mStartAddress ? -1 : 1;
}
if (mOffset != other.mOffset) {
return mOffset < other.mOffset ? -1 : 1;
}
if (mAllocationUnitCount != other.mAllocationUnitCount) {
return mAllocationUnitCount < other.mAllocationUnitCount ? -1 : 1;
}
if (mUsageData != other.mUsageData) {
return mUsageData.compareTo(other.mUsageData);
}
return 0;
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright (C) 2008 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.Device.DeviceState;
import com.android.ddmlib.log.LogReceiver;
import java.io.IOException;
import java.util.Map;
/**
* A Device. It can be a physical device or an emulator.
*/
public interface IDevice {
public final static String PROP_BUILD_VERSION = "ro.build.version.release";
public final static String PROP_BUILD_VERSION_NUMBER = "ro.build.version.sdk";
public final static String PROP_DEBUGGABLE = "ro.debuggable";
/** Serial number of the first connected emulator. */
public final static String FIRST_EMULATOR_SN = "emulator-5554"; //$NON-NLS-1$
/** Device change bit mask: {@link DeviceState} change. */
public static final int CHANGE_STATE = 0x0001;
/** Device change bit mask: {@link Client} list change. */
public static final int CHANGE_CLIENT_LIST = 0x0002;
/** Device change bit mask: build info change. */
public static final int CHANGE_BUILD_INFO = 0x0004;
/**
* Returns the serial number of the device.
*/
public String getSerialNumber();
/**
* Returns the name of the AVD the emulator is running.
* <p/>This is only valid if {@link #isEmulator()} returns true.
* <p/>If the emulator is not running any AVD (for instance it's running from an Android source
* tree build), this method will return "<code>&lt;build&gt;</code>".
* @return the name of the AVD or <code>null</code> if there isn't any.
*/
public String getAvdName();
/**
* Returns the state of the device.
*/
public DeviceState getState();
/**
* Returns the device properties. It contains the whole output of 'getprop'
*/
public Map<String, String> getProperties();
/**
* Returns the number of property for this device.
*/
public int getPropertyCount();
/**
* Returns a property value.
* @param name the name of the value to return.
* @return the value or <code>null</code> if the property does not exist.
*/
public String getProperty(String name);
/**
* Returns if the device is ready.
* @return <code>true</code> if {@link #getState()} returns {@link DeviceState#ONLINE}.
*/
public boolean isOnline();
/**
* Returns <code>true</code> if the device is an emulator.
*/
public boolean isEmulator();
/**
* Returns if the device is offline.
* @return <code>true</code> if {@link #getState()} returns {@link DeviceState#OFFLINE}.
*/
public boolean isOffline();
/**
* Returns if the device is in bootloader mode.
* @return <code>true</code> if {@link #getState()} returns {@link DeviceState#BOOTLOADER}.
*/
public boolean isBootLoader();
/**
* Returns whether the {@link Device} has {@link Client}s.
*/
public boolean hasClients();
/**
* Returns the array of clients.
*/
public Client[] getClients();
/**
* Returns a {@link Client} by its application name.
* @param applicationName the name of the application
* @return the <code>Client</code> object or <code>null</code> if no match was found.
*/
public Client getClient(String applicationName);
/**
* Returns a {@link SyncService} object to push / pull files to and from the device.
* @return <code>null</code> if the SyncService couldn't be created.
*/
public SyncService getSyncService();
/**
* Returns a {@link FileListingService} for this device.
*/
public FileListingService getFileListingService();
/**
* Takes a screen shot of the device and returns it as a {@link RawImage}.
* @return the screenshot as a <code>RawImage</code> or <code>null</code> if
* something went wrong.
* @throws IOException
*/
public RawImage getScreenshot() throws IOException;
/**
* Executes a shell command on the device, and sends the result to a receiver.
* @param command The command to execute
* @param receiver The receiver object getting the result from the command.
* @throws IOException
*/
public void executeShellCommand(String command,
IShellOutputReceiver receiver) throws IOException;
/**
* Runs the event log service and outputs the event log to the {@link LogReceiver}.
* @param receiver the receiver to receive the event log entries.
* @throws IOException
*/
public void runEventLogService(LogReceiver receiver) throws IOException;
/**
* Runs the log service for the given log and outputs the log to the {@link LogReceiver}.
* @param logname the logname of the log to read from.
* @param receiver the receiver to receive the event log entries.
* @throws IOException
*/
public void runLogService(String logname, LogReceiver receiver) throws IOException;
/**
* Creates a port forwarding between a local and a remote port.
* @param localPort the local port to forward
* @param remotePort the remote port.
* @return <code>true</code> if success.
*/
public boolean createForward(int localPort, int remotePort);
/**
* Removes a port forwarding between a local and a remote port.
* @param localPort the local port to forward
* @param remotePort the remote port.
* @return <code>true</code> if success.
*/
public boolean removeForward(int localPort, int remotePort);
/**
* Returns the name of the client by pid or <code>null</code> if pid is unknown
* @param pid the pid of the client.
*/
public String getClientName(int pid);
}

View File

@@ -0,0 +1,44 @@
/*
* 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;
/**
* Classes which implement this interface provide methods that deal with out from a remote shell
* command on a device/emulator.
*/
public interface IShellOutputReceiver {
/**
* Called every time some new data is available.
* @param data The new data.
* @param offset The offset at which the new data starts.
* @param length The length of the new data.
*/
public void addOutput(byte[] data, int offset, int length);
/**
* Called at the end of the process execution (unless the process was
* canceled). This allows the receiver to terminate and flush whatever
* data was not yet processed.
*/
public void flush();
/**
* Cancel method to stop the execution of the remote shell command.
* @return true to cancel the execution of the command.
*/
public boolean isCancelled();
};

View File

@@ -0,0 +1,29 @@
/*
* 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;
/**
* Classes which implement this interface provide a method that returns a stack trace.
*/
public interface IStackTraceInfo {
/**
* Returns the stack trace. This can be <code>null</code>.
*/
public StackTraceElement[] getStackTrace();
}

View File

@@ -0,0 +1,371 @@
/* //device/tools/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java
**
** Copyright 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SocketChannel;
/**
* A JDWP packet, sitting at the start of a ByteBuffer somewhere.
*
* This allows us to wrap a "pointer" to the data with the results of
* decoding the packet.
*
* None of the operations here are synchronized. If multiple threads will
* be accessing the same ByteBuffers, external sync will be required.
*
* Use the constructor to create an empty packet, or "findPacket()" to
* wrap a JdwpPacket around existing data.
*/
final class JdwpPacket {
// header len
public static final int JDWP_HEADER_LEN = 11;
// results from findHandshake
public static final int HANDSHAKE_GOOD = 1;
public static final int HANDSHAKE_NOTYET = 2;
public static final int HANDSHAKE_BAD = 3;
// our cmdSet/cmd
private static final int DDMS_CMD_SET = 0xc7; // 'G' + 128
private static final int DDMS_CMD = 0x01;
// "flags" field
private static final int REPLY_PACKET = 0x80;
// this is sent and expected at the start of a JDWP connection
private static final byte[] mHandshake = {
'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'
};
public static final int HANDSHAKE_LEN = mHandshake.length;
private ByteBuffer mBuffer;
private int mLength, mId, mFlags, mCmdSet, mCmd, mErrCode;
private boolean mIsNew;
private static int mSerialId = 0x40000000;
/**
* Create a new, empty packet, in "buf".
*/
JdwpPacket(ByteBuffer buf) {
mBuffer = buf;
mIsNew = true;
}
/**
* Finish a packet created with newPacket().
*
* This always creates a command packet, with the next serial number
* in sequence.
*
* We have to take "payloadLength" as an argument because we can't
* see the position in the "slice" returned by getPayload(). We could
* fish it out of the chunk header, but it's legal for there to be
* more than one chunk in a JDWP packet.
*
* On exit, "position" points to the end of the data.
*/
void finishPacket(int payloadLength) {
assert mIsNew;
ByteOrder oldOrder = mBuffer.order();
mBuffer.order(ChunkHandler.CHUNK_ORDER);
mLength = JDWP_HEADER_LEN + payloadLength;
mId = getNextSerial();
mFlags = 0;
mCmdSet = DDMS_CMD_SET;
mCmd = DDMS_CMD;
mBuffer.putInt(0x00, mLength);
mBuffer.putInt(0x04, mId);
mBuffer.put(0x08, (byte) mFlags);
mBuffer.put(0x09, (byte) mCmdSet);
mBuffer.put(0x0a, (byte) mCmd);
mBuffer.order(oldOrder);
mBuffer.position(mLength);
}
/**
* Get the next serial number. This creates a unique serial number
* across all connections, not just for the current connection. This
* is a useful property when debugging, but isn't necessary.
*
* We can't synchronize on an int, so we use a sync method.
*/
private static synchronized int getNextSerial() {
return mSerialId++;
}
/**
* Return a slice of the byte buffer, positioned past the JDWP header
* to the start of the chunk header. The buffer's limit will be set
* to the size of the payload if the size is known; if this is a
* packet under construction the limit will be set to the end of the
* buffer.
*
* Doesn't examine the packet at all -- works on empty buffers.
*/
ByteBuffer getPayload() {
ByteBuffer buf;
int oldPosn = mBuffer.position();
mBuffer.position(JDWP_HEADER_LEN);
buf = mBuffer.slice(); // goes from position to limit
mBuffer.position(oldPosn);
if (mLength > 0)
buf.limit(mLength - JDWP_HEADER_LEN);
else
assert mIsNew;
buf.order(ChunkHandler.CHUNK_ORDER);
return buf;
}
/**
* Returns "true" if this JDWP packet has a JDWP command type.
*
* This never returns "true" for reply packets.
*/
boolean isDdmPacket() {
return (mFlags & REPLY_PACKET) == 0 &&
mCmdSet == DDMS_CMD_SET &&
mCmd == DDMS_CMD;
}
/**
* Returns "true" if this JDWP packet is tagged as a reply.
*/
boolean isReply() {
return (mFlags & REPLY_PACKET) != 0;
}
/**
* Returns "true" if this JDWP packet is a reply with a nonzero
* error code.
*/
boolean isError() {
return isReply() && mErrCode != 0;
}
/**
* Returns "true" if this JDWP packet has no data.
*/
boolean isEmpty() {
return (mLength == JDWP_HEADER_LEN);
}
/**
* Return the packet's ID. For a reply packet, this allows us to
* match the reply with the original request.
*/
int getId() {
return mId;
}
/**
* Return the length of a packet. This includes the header, so an
* empty packet is 11 bytes long.
*/
int getLength() {
return mLength;
}
/**
* Write our packet to "chan". Consumes the packet as part of the
* write.
*
* The JDWP packet starts at offset 0 and ends at mBuffer.position().
*/
void writeAndConsume(SocketChannel chan) throws IOException {
int oldLimit;
//Log.i("ddms", "writeAndConsume: pos=" + mBuffer.position()
// + ", limit=" + mBuffer.limit());
assert mLength > 0;
mBuffer.flip(); // limit<-posn, posn<-0
oldLimit = mBuffer.limit();
mBuffer.limit(mLength);
while (mBuffer.position() != mBuffer.limit()) {
chan.write(mBuffer);
}
// position should now be at end of packet
assert mBuffer.position() == mLength;
mBuffer.limit(oldLimit);
mBuffer.compact(); // shift posn...limit, posn<-pending data
//Log.i("ddms", " : pos=" + mBuffer.position()
// + ", limit=" + mBuffer.limit());
}
/**
* "Move" the packet data out of the buffer we're sitting on and into
* buf at the current position.
*/
void movePacket(ByteBuffer buf) {
Log.v("ddms", "moving " + mLength + " bytes");
int oldPosn = mBuffer.position();
mBuffer.position(0);
mBuffer.limit(mLength);
buf.put(mBuffer);
mBuffer.position(mLength);
mBuffer.limit(oldPosn);
mBuffer.compact(); // shift posn...limit, posn<-pending data
}
/**
* Consume the JDWP packet.
*
* On entry and exit, "position" is the #of bytes in the buffer.
*/
void consume()
{
//Log.d("ddms", "consuming " + mLength + " bytes");
//Log.d("ddms", " posn=" + mBuffer.position()
// + ", limit=" + mBuffer.limit());
/*
* The "flip" call sets "limit" equal to the position (usually the
* end of data) and "position" equal to zero.
*
* compact() copies everything from "position" and "limit" to the
* start of the buffer, sets "position" to the end of data, and
* sets "limit" to the capacity.
*
* On entry, "position" is set to the amount of data in the buffer
* and "limit" is set to the capacity. We want to call flip()
* so that position..limit spans our data, advance "position" past
* the current packet, then compact.
*/
mBuffer.flip(); // limit<-posn, posn<-0
mBuffer.position(mLength);
mBuffer.compact(); // shift posn...limit, posn<-pending data
mLength = 0;
//Log.d("ddms", " after compact, posn=" + mBuffer.position()
// + ", limit=" + mBuffer.limit());
}
/**
* Find the JDWP packet at the start of "buf". The start is known,
* but the length has to be parsed out.
*
* On entry, the packet data in "buf" must start at offset 0 and end
* at "position". "limit" should be set to the buffer capacity. This
* method does not alter "buf"s attributes.
*
* Returns a new JdwpPacket if a full one is found in the buffer. If
* not, returns null. Throws an exception if the data doesn't look like
* a valid JDWP packet.
*/
static JdwpPacket findPacket(ByteBuffer buf) {
int count = buf.position();
int length, id, flags, cmdSet, cmd;
if (count < JDWP_HEADER_LEN)
return null;
ByteOrder oldOrder = buf.order();
buf.order(ChunkHandler.CHUNK_ORDER);
length = buf.getInt(0x00);
id = buf.getInt(0x04);
flags = buf.get(0x08) & 0xff;
cmdSet = buf.get(0x09) & 0xff;
cmd = buf.get(0x0a) & 0xff;
buf.order(oldOrder);
if (length < JDWP_HEADER_LEN)
throw new BadPacketException();
if (count < length)
return null;
JdwpPacket pkt = new JdwpPacket(buf);
//pkt.mBuffer = buf;
pkt.mLength = length;
pkt.mId = id;
pkt.mFlags = flags;
if ((flags & REPLY_PACKET) == 0) {
pkt.mCmdSet = cmdSet;
pkt.mCmd = cmd;
pkt.mErrCode = -1;
} else {
pkt.mCmdSet = -1;
pkt.mCmd = -1;
pkt.mErrCode = cmdSet | (cmd << 8);
}
return pkt;
}
/**
* Like findPacket(), but when we're expecting the JDWP handshake.
*
* Returns one of:
* HANDSHAKE_GOOD - found handshake, looks good
* HANDSHAKE_BAD - found enough data, but it's wrong
* HANDSHAKE_NOTYET - not enough data has been read yet
*/
static int findHandshake(ByteBuffer buf) {
int count = buf.position();
int i;
if (count < mHandshake.length)
return HANDSHAKE_NOTYET;
for (i = mHandshake.length -1; i >= 0; --i) {
if (buf.get(i) != mHandshake[i])
return HANDSHAKE_BAD;
}
return HANDSHAKE_GOOD;
}
/**
* Remove the handshake string from the buffer.
*
* On entry and exit, "position" is the #of bytes in the buffer.
*/
static void consumeHandshake(ByteBuffer buf) {
// in theory, nothing else can have arrived, so this is overkill
buf.flip(); // limit<-posn, posn<-0
buf.position(mHandshake.length);
buf.compact(); // shift posn...limit, posn<-pending data
}
/**
* Copy the handshake string into the output buffer.
*
* On exit, "buf"s position will be advanced.
*/
static void putHandshake(ByteBuffer buf) {
buf.put(mHandshake);
}
}

View File

@@ -0,0 +1,351 @@
/*
* 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 java.io.PrintWriter;
import java.io.StringWriter;
/**
* Log class that mirrors the API in main Android sources.
* <p/>Default behavior outputs the log to {@link System#out}. Use
* {@link #setLogOutput(com.android.ddmlib.Log.ILogOutput)} to redirect the log somewhere else.
*/
public final class Log {
/**
* Log Level enum.
*/
public enum LogLevel {
VERBOSE(2, "verbose", 'V'), //$NON-NLS-1$
DEBUG(3, "debug", 'D'), //$NON-NLS-1$
INFO(4, "info", 'I'), //$NON-NLS-1$
WARN(5, "warn", 'W'), //$NON-NLS-1$
ERROR(6, "error", 'E'), //$NON-NLS-1$
ASSERT(7, "assert", 'A'); //$NON-NLS-1$
private int mPriorityLevel;
private String mStringValue;
private char mPriorityLetter;
LogLevel(int intPriority, String stringValue, char priorityChar) {
mPriorityLevel = intPriority;
mStringValue = stringValue;
mPriorityLetter = priorityChar;
}
public static LogLevel getByString(String value) {
for (LogLevel mode : values()) {
if (mode.mStringValue.equals(value)) {
return mode;
}
}
return null;
}
/**
* Returns the {@link LogLevel} enum matching the specified letter.
* @param letter the letter matching a <code>LogLevel</code> enum
* @return a <code>LogLevel</code> object or <code>null</code> if no match were found.
*/
public static LogLevel getByLetter(char letter) {
for (LogLevel mode : values()) {
if (mode.mPriorityLetter == letter) {
return mode;
}
}
return null;
}
/**
* Returns the {@link LogLevel} enum matching the specified letter.
* <p/>
* The letter is passed as a {@link String} argument, but only the first character
* is used.
* @param letter the letter matching a <code>LogLevel</code> enum
* @return a <code>LogLevel</code> object or <code>null</code> if no match were found.
*/
public static LogLevel getByLetterString(String letter) {
if (letter.length() > 0) {
return getByLetter(letter.charAt(0));
}
return null;
}
/**
* Returns the letter identifying the priority of the {@link LogLevel}.
*/
public char getPriorityLetter() {
return mPriorityLetter;
}
/**
* Returns the numerical value of the priority.
*/
public int getPriority() {
return mPriorityLevel;
}
/**
* Returns a non translated string representing the LogLevel.
*/
public String getStringValue() {
return mStringValue;
}
}
/**
* Classes which implement this interface provides methods that deal with outputting log
* messages.
*/
public interface ILogOutput {
/**
* Sent when a log message needs to be printed.
* @param logLevel The {@link LogLevel} enum representing the priority of the message.
* @param tag The tag associated with the message.
* @param message The message to display.
*/
public void printLog(LogLevel logLevel, String tag, String message);
/**
* Sent when a log message needs to be printed, and, if possible, displayed to the user
* in a dialog box.
* @param logLevel The {@link LogLevel} enum representing the priority of the message.
* @param tag The tag associated with the message.
* @param message The message to display.
*/
public void printAndPromptLog(LogLevel logLevel, String tag, String message);
}
private static LogLevel mLevel = DdmPreferences.getLogLevel();
private static ILogOutput sLogOutput;
private static final char[] mSpaceLine = new char[72];
private static final char[] mHexDigit = new char[]
{ '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
static {
/* prep for hex dump */
int i = mSpaceLine.length-1;
while (i >= 0)
mSpaceLine[i--] = ' ';
mSpaceLine[0] = mSpaceLine[1] = mSpaceLine[2] = mSpaceLine[3] = '0';
mSpaceLine[4] = '-';
}
static final class Config {
static final boolean LOGV = true;
static final boolean LOGD = true;
};
private Log() {}
/**
* Outputs a {@link LogLevel#VERBOSE} level message.
* @param tag The tag associated with the message.
* @param message The message to output.
*/
public static void v(String tag, String message) {
println(LogLevel.VERBOSE, tag, message);
}
/**
* Outputs a {@link LogLevel#DEBUG} level message.
* @param tag The tag associated with the message.
* @param message The message to output.
*/
public static void d(String tag, String message) {
println(LogLevel.DEBUG, tag, message);
}
/**
* Outputs a {@link LogLevel#INFO} level message.
* @param tag The tag associated with the message.
* @param message The message to output.
*/
public static void i(String tag, String message) {
println(LogLevel.INFO, tag, message);
}
/**
* Outputs a {@link LogLevel#WARN} level message.
* @param tag The tag associated with the message.
* @param message The message to output.
*/
public static void w(String tag, String message) {
println(LogLevel.WARN, tag, message);
}
/**
* Outputs a {@link LogLevel#ERROR} level message.
* @param tag The tag associated with the message.
* @param message The message to output.
*/
public static void e(String tag, String message) {
println(LogLevel.ERROR, tag, message);
}
/**
* Outputs a log message and attempts to display it in a dialog.
* @param tag The tag associated with the message.
* @param message The message to output.
*/
public static void logAndDisplay(LogLevel logLevel, String tag, String message) {
if (sLogOutput != null) {
sLogOutput.printAndPromptLog(logLevel, tag, message);
} else {
println(logLevel, tag, message);
}
}
/**
* Outputs a {@link LogLevel#ERROR} level {@link Throwable} information.
* @param tag The tag associated with the message.
* @param throwable The {@link Throwable} to output.
*/
public static void e(String tag, Throwable throwable) {
if (throwable != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
println(LogLevel.ERROR, tag, throwable.getMessage() + '\n' + sw.toString());
}
}
static void setLevel(LogLevel logLevel) {
mLevel = logLevel;
}
/**
* Sets the {@link ILogOutput} to use to print the logs. If not set, {@link System#out}
* will be used.
* @param logOutput The {@link ILogOutput} to use to print the log.
*/
public static void setLogOutput(ILogOutput logOutput) {
sLogOutput = logOutput;
}
/**
* Show hex dump.
* <p/>
* Local addition. Output looks like:
* 1230- 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 0123456789abcdef
* <p/>
* Uses no string concatenation; creates one String object per line.
*/
static void hexDump(String tag, LogLevel level, byte[] data, int offset, int length) {
int kHexOffset = 6;
int kAscOffset = 55;
char[] line = new char[mSpaceLine.length];
int addr, baseAddr, count;
int i, ch;
boolean needErase = true;
//Log.w(tag, "HEX DUMP: off=" + offset + ", length=" + length);
baseAddr = 0;
while (length != 0) {
if (length > 16) {
// full line
count = 16;
} else {
// partial line; re-copy blanks to clear end
count = length;
needErase = true;
}
if (needErase) {
System.arraycopy(mSpaceLine, 0, line, 0, mSpaceLine.length);
needErase = false;
}
// output the address (currently limited to 4 hex digits)
addr = baseAddr;
addr &= 0xffff;
ch = 3;
while (addr != 0) {
line[ch] = mHexDigit[addr & 0x0f];
ch--;
addr >>>= 4;
}
// output hex digits and ASCII chars
ch = kHexOffset;
for (i = 0; i < count; i++) {
byte val = data[offset + i];
line[ch++] = mHexDigit[(val >>> 4) & 0x0f];
line[ch++] = mHexDigit[val & 0x0f];
ch++;
if (val >= 0x20 && val < 0x7f)
line[kAscOffset + i] = (char) val;
else
line[kAscOffset + i] = '.';
}
println(level, tag, new String(line));
// advance to next chunk of data
length -= count;
offset += count;
baseAddr += count;
}
}
/**
* Dump the entire contents of a byte array with DEBUG priority.
*/
static void hexDump(byte[] data) {
hexDump("ddms", LogLevel.DEBUG, data, 0, data.length);
}
/* currently prints to stdout; could write to a log window */
private static void println(LogLevel logLevel, String tag, String message) {
if (logLevel.getPriority() >= mLevel.getPriority()) {
if (sLogOutput != null) {
sLogOutput.printLog(logLevel, tag, message);
} else {
printLog(logLevel, tag, message);
}
}
}
/**
* Prints a log message.
* @param logLevel
* @param tag
* @param message
*/
public static void printLog(LogLevel logLevel, String tag, String message) {
long msec;
msec = System.currentTimeMillis();
String outMessage = String.format("%02d:%02d %c/%s: %s\n",
(msec / 60000) % 60, (msec / 1000) % 60,
logLevel.getPriorityLetter(), tag, message);
System.out.print(outMessage);
}
}

View File

@@ -0,0 +1,780 @@
/*
* 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.DebugPortManager.IDebugPortProvider;
import com.android.ddmlib.Log.LogLevel;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.NotYetBoundException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* Monitor open connections.
*/
final class MonitorThread extends Thread {
// For broadcasts to message handlers
//private static final int CLIENT_CONNECTED = 1;
private static final int CLIENT_READY = 2;
private static final int CLIENT_DISCONNECTED = 3;
private volatile boolean mQuit = false;
// List of clients we're paying attention to
private ArrayList<Client> mClientList;
// The almighty mux
private Selector mSelector;
// Map chunk types to handlers
private HashMap<Integer, ChunkHandler> mHandlerMap;
// port for "debug selected"
private ServerSocketChannel mDebugSelectedChan;
private int mNewDebugSelectedPort;
private int mDebugSelectedPort = -1;
/**
* "Selected" client setup to answer debugging connection to the mNewDebugSelectedPort port.
*/
private Client mSelectedClient = null;
// singleton
private static MonitorThread mInstance;
/**
* Generic constructor.
*/
private MonitorThread() {
super("Monitor");
mClientList = new ArrayList<Client>();
mHandlerMap = new HashMap<Integer, ChunkHandler>();
mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort();
}
/**
* Creates and return the singleton instance of the client monitor thread.
*/
static MonitorThread createInstance() {
return mInstance = new MonitorThread();
}
/**
* Get singleton instance of the client monitor thread.
*/
static MonitorThread getInstance() {
return mInstance;
}
/**
* Sets or changes the port number for "debug selected".
*/
synchronized void setDebugSelectedPort(int port) throws IllegalStateException {
if (mInstance == null) {
return;
}
if (AndroidDebugBridge.getClientSupport() == false) {
return;
}
if (mDebugSelectedChan != null) {
Log.d("ddms", "Changing debug-selected port to " + port);
mNewDebugSelectedPort = port;
wakeup();
} else {
// we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically
// opened on the first run loop.
mNewDebugSelectedPort = port;
}
}
/**
* Sets the client to accept debugger connection on the custom "Selected debug port".
* @param selectedClient the client. Can be null.
*/
synchronized void setSelectedClient(Client selectedClient) {
if (mInstance == null) {
return;
}
if (mSelectedClient != selectedClient) {
Client oldClient = mSelectedClient;
mSelectedClient = selectedClient;
if (oldClient != null) {
oldClient.update(Client.CHANGE_PORT);
}
if (mSelectedClient != null) {
mSelectedClient.update(Client.CHANGE_PORT);
}
}
}
/**
* Returns the client accepting debugger connection on the custom "Selected debug port".
*/
Client getSelectedClient() {
return mSelectedClient;
}
/**
* Returns "true" if we want to retry connections to clients if we get a bad
* JDWP handshake back, "false" if we want to just mark them as bad and
* leave them alone.
*/
boolean getRetryOnBadHandshake() {
return true; // TODO? make configurable
}
/**
* Get an array of known clients.
*/
Client[] getClients() {
synchronized (mClientList) {
return mClientList.toArray(new Client[0]);
}
}
/**
* Register "handler" as the handler for type "type".
*/
synchronized void registerChunkHandler(int type, ChunkHandler handler) {
if (mInstance == null) {
return;
}
synchronized (mHandlerMap) {
if (mHandlerMap.get(type) == null) {
mHandlerMap.put(type, handler);
}
}
}
/**
* Watch for activity from clients and debuggers.
*/
@Override
public void run() {
Log.d("ddms", "Monitor is up");
// create a selector
try {
mSelector = Selector.open();
} catch (IOException ioe) {
Log.logAndDisplay(LogLevel.ERROR, "ddms",
"Failed to initialize Monitor Thread: " + ioe.getMessage());
return;
}
while (!mQuit) {
try {
/*
* sync with new registrations: we wait until addClient is done before going through
* and doing mSelector.select() again.
* @see {@link #addClient(Client)}
*/
synchronized (mClientList) {
}
// (re-)open the "debug selected" port, if it's not opened yet or
// if the port changed.
try {
if (AndroidDebugBridge.getClientSupport()) {
if ((mDebugSelectedChan == null ||
mNewDebugSelectedPort != mDebugSelectedPort) &&
mNewDebugSelectedPort != -1) {
if (reopenDebugSelectedPort()) {
mDebugSelectedPort = mNewDebugSelectedPort;
}
}
}
} catch (IOException ioe) {
Log.e("ddms",
"Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort);
Log.e("ddms", ioe);
mNewDebugSelectedPort = mDebugSelectedPort; // no retry
}
int count;
try {
count = mSelector.select();
} catch (IOException ioe) {
ioe.printStackTrace();
continue;
} catch (CancelledKeyException cke) {
continue;
}
if (count == 0) {
// somebody called wakeup() ?
// Log.i("ddms", "selector looping");
continue;
}
Set<SelectionKey> keys = mSelector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
try {
if (key.attachment() instanceof Client) {
processClientActivity(key);
}
else if (key.attachment() instanceof Debugger) {
processDebuggerActivity(key);
}
else if (key.attachment() instanceof MonitorThread) {
processDebugSelectedActivity(key);
}
else {
Log.e("ddms", "unknown activity key");
}
} catch (Exception e) {
// we don't want to have our thread be killed because of any uncaught
// exception, so we intercept all here.
Log.e("ddms", "Exception during activity from Selector.");
Log.e("ddms", e);
}
}
} catch (Exception e) {
// we don't want to have our thread be killed because of any uncaught
// exception, so we intercept all here.
Log.e("ddms", "Exception MonitorThread.run()");
Log.e("ddms", e);
}
}
}
/**
* Returns the port on which the selected client listen for debugger
*/
int getDebugSelectedPort() {
return mDebugSelectedPort;
}
/*
* Something happened. Figure out what.
*/
private void processClientActivity(SelectionKey key) {
Client client = (Client)key.attachment();
try {
if (key.isReadable() == false || key.isValid() == false) {
Log.d("ddms", "Invalid key from " + client + ". Dropping client.");
dropClient(client, true /* notify */);
return;
}
client.read();
/*
* See if we have a full packet in the buffer. It's possible we have
* more than one packet, so we have to loop.
*/
JdwpPacket packet = client.getJdwpPacket();
while (packet != null) {
if (packet.isDdmPacket()) {
// unsolicited DDM request - hand it off
assert !packet.isReply();
callHandler(client, packet, null);
packet.consume();
} else if (packet.isReply()
&& client.isResponseToUs(packet.getId()) != null) {
// reply to earlier DDM request
ChunkHandler handler = client
.isResponseToUs(packet.getId());
if (packet.isError())
client.packetFailed(packet);
else if (packet.isEmpty())
Log.d("ddms", "Got empty reply for 0x"
+ Integer.toHexString(packet.getId())
+ " from " + client);
else
callHandler(client, packet, handler);
packet.consume();
client.removeRequestId(packet.getId());
} else {
Log.v("ddms", "Forwarding client "
+ (packet.isReply() ? "reply" : "event") + " 0x"
+ Integer.toHexString(packet.getId()) + " to "
+ client.getDebugger());
client.forwardPacketToDebugger(packet);
}
// find next
packet = client.getJdwpPacket();
}
} catch (CancelledKeyException e) {
// key was canceled probably due to a disconnected client before we could
// read stuff coming from the client, so we drop it.
dropClient(client, true /* notify */);
} catch (IOException ex) {
// something closed down, no need to print anything. The client is simply dropped.
dropClient(client, true /* notify */);
} catch (Exception ex) {
Log.e("ddms", ex);
/* close the client; automatically un-registers from selector */
dropClient(client, true /* notify */);
if (ex instanceof BufferOverflowException) {
Log.w("ddms",
"Client data packet exceeded maximum buffer size "
+ client);
} else {
// don't know what this is, display it
Log.e("ddms", ex);
}
}
}
/*
* Process an incoming DDM packet. If this is a reply to an earlier request,
* "handler" will be set to the handler responsible for the original
* request. The spec allows a JDWP message to include multiple DDM chunks.
*/
private void callHandler(Client client, JdwpPacket packet,
ChunkHandler handler) {
// on first DDM packet received, broadcast a "ready" message
if (!client.ddmSeen())
broadcast(CLIENT_READY, client);
ByteBuffer buf = packet.getPayload();
int type, length;
boolean reply = true;
type = buf.getInt();
length = buf.getInt();
if (handler == null) {
// not a reply, figure out who wants it
synchronized (mHandlerMap) {
handler = mHandlerMap.get(type);
reply = false;
}
}
if (handler == null) {
Log.w("ddms", "Received unsupported chunk type "
+ ChunkHandler.name(type) + " (len=" + length + ")");
} else {
Log.d("ddms", "Calling handler for " + ChunkHandler.name(type)
+ " [" + handler + "] (len=" + length + ")");
ByteBuffer ibuf = buf.slice();
ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O
roBuf.order(ChunkHandler.CHUNK_ORDER);
// do the handling of the chunk synchronized on the client list
// to be sure there's no concurrency issue when we look for HOME
// in hasApp()
synchronized (mClientList) {
handler.handleChunk(client, type, roBuf, reply, packet.getId());
}
}
}
/**
* Drops a client from the monitor.
* <p/>This will lock the {@link Client} list of the {@link Device} running <var>client</var>.
* @param client
* @param notify
*/
synchronized void dropClient(Client client, boolean notify) {
if (mInstance == null) {
return;
}
synchronized (mClientList) {
if (mClientList.remove(client) == false) {
return;
}
}
client.close(notify);
broadcast(CLIENT_DISCONNECTED, client);
/*
* http://forum.java.sun.com/thread.jspa?threadID=726715&start=0
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504
*/
wakeup();
}
/*
* Process activity from one of the debugger sockets. This could be a new
* connection or a data packet.
*/
private void processDebuggerActivity(SelectionKey key) {
Debugger dbg = (Debugger)key.attachment();
try {
if (key.isAcceptable()) {
try {
acceptNewDebugger(dbg, null);
} catch (IOException ioe) {
Log.w("ddms", "debugger accept() failed");
ioe.printStackTrace();
}
} else if (key.isReadable()) {
processDebuggerData(key);
} else {
Log.d("ddm-debugger", "key in unknown state");
}
} catch (CancelledKeyException cke) {
// key has been cancelled we can ignore that.
}
}
/*
* Accept a new connection from a debugger. If successful, register it with
* the Selector.
*/
private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan)
throws IOException {
synchronized (mClientList) {
SocketChannel chan;
if (acceptChan == null)
chan = dbg.accept();
else
chan = dbg.accept(acceptChan);
if (chan != null) {
chan.socket().setTcpNoDelay(true);
wakeup();
try {
chan.register(mSelector, SelectionKey.OP_READ, dbg);
} catch (IOException ioe) {
// failed, drop the connection
dbg.closeData();
throw ioe;
} catch (RuntimeException re) {
// failed, drop the connection
dbg.closeData();
throw re;
}
} else {
Log.i("ddms", "ignoring duplicate debugger");
// new connection already closed
}
}
}
/*
* We have incoming data from the debugger. Forward it to the client.
*/
private void processDebuggerData(SelectionKey key) {
Debugger dbg = (Debugger)key.attachment();
try {
/*
* Read pending data.
*/
dbg.read();
/*
* See if we have a full packet in the buffer. It's possible we have
* more than one packet, so we have to loop.
*/
JdwpPacket packet = dbg.getJdwpPacket();
while (packet != null) {
Log.v("ddms", "Forwarding dbg req 0x"
+ Integer.toHexString(packet.getId()) + " to "
+ dbg.getClient());
dbg.forwardPacketToClient(packet);
packet = dbg.getJdwpPacket();
}
} catch (IOException ioe) {
/*
* Close data connection; automatically un-registers dbg from
* selector. The failure could be caused by the debugger going away,
* or by the client going away and failing to accept our data.
* Either way, the debugger connection does not need to exist any
* longer. We also need to recycle the connection to the client, so
* that the VM sees the debugger disconnect. For a DDM-aware client
* this won't be necessary, and we can just send a "debugger
* disconnected" message.
*/
Log.i("ddms", "Closing connection to debugger " + dbg);
dbg.closeData();
Client client = dbg.getClient();
if (client.isDdmAware()) {
// TODO: soft-disconnect DDM-aware clients
Log.i("ddms", " (recycling client connection as well)");
// we should drop the client, but also attempt to reopen it.
// This is done by the DeviceMonitor.
client.getDevice().getMonitor().addClientToDropAndReopen(client,
IDebugPortProvider.NO_STATIC_PORT);
} else {
Log.i("ddms", " (recycling client connection as well)");
// we should drop the client, but also attempt to reopen it.
// This is done by the DeviceMonitor.
client.getDevice().getMonitor().addClientToDropAndReopen(client,
IDebugPortProvider.NO_STATIC_PORT);
}
}
}
/*
* Tell the thread that something has changed.
*/
private void wakeup() {
mSelector.wakeup();
}
/**
* Tell the thread to stop. Called from UI thread.
*/
synchronized void quit() {
mQuit = true;
wakeup();
Log.d("ddms", "Waiting for Monitor thread");
try {
this.join();
// since we're quitting, lets drop all the client and disconnect
// the DebugSelectedPort
synchronized (mClientList) {
for (Client c : mClientList) {
c.close(false /* notify */);
broadcast(CLIENT_DISCONNECTED, c);
}
mClientList.clear();
}
if (mDebugSelectedChan != null) {
mDebugSelectedChan.close();
mDebugSelectedChan.socket().close();
mDebugSelectedChan = null;
}
mSelector.close();
} catch (InterruptedException ie) {
ie.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
mInstance = null;
}
/**
* Add a new Client to the list of things we monitor. Also adds the client's
* channel and the client's debugger listener to the selection list. This
* should only be called from one thread (the VMWatcherThread) to avoid a
* race between "alreadyOpen" and Client creation.
*/
synchronized void addClient(Client client) {
if (mInstance == null) {
return;
}
Log.d("ddms", "Adding new client " + client);
synchronized (mClientList) {
mClientList.add(client);
/*
* Register the Client's socket channel with the selector. We attach
* the Client to the SelectionKey. If you try to register a new
* channel with the Selector while it is waiting for I/O, you will
* block. The solution is to call wakeup() and then hold a lock to
* ensure that the registration happens before the Selector goes
* back to sleep.
*/
try {
wakeup();
client.register(mSelector);
Debugger dbg = client.getDebugger();
if (dbg != null) {
dbg.registerListener(mSelector);
}
} catch (IOException ioe) {
// not really expecting this to happen
ioe.printStackTrace();
}
}
}
/*
* Broadcast an event to all message handlers.
*/
private void broadcast(int event, Client client) {
Log.d("ddms", "broadcast " + event + ": " + client);
/*
* The handler objects appear once in mHandlerMap for each message they
* handle. We want to notify them once each, so we convert the HashMap
* to a HashSet before we iterate.
*/
HashSet<ChunkHandler> set;
synchronized (mHandlerMap) {
Collection<ChunkHandler> values = mHandlerMap.values();
set = new HashSet<ChunkHandler>(values);
}
Iterator<ChunkHandler> iter = set.iterator();
while (iter.hasNext()) {
ChunkHandler handler = iter.next();
switch (event) {
case CLIENT_READY:
try {
handler.clientReady(client);
} catch (IOException ioe) {
// Something failed with the client. It should
// fall out of the list the next time we try to
// do something with it, so we discard the
// exception here and assume cleanup will happen
// later. May need to propagate farther. The
// trouble is that not all values for "event" may
// actually throw an exception.
Log.w("ddms",
"Got exception while broadcasting 'ready'");
return;
}
break;
case CLIENT_DISCONNECTED:
handler.clientDisconnected(client);
break;
default:
throw new UnsupportedOperationException();
}
}
}
/**
* Opens (or reopens) the "debug selected" port and listen for connections.
* @return true if the port was opened successfully.
* @throws IOException
*/
private boolean reopenDebugSelectedPort() throws IOException {
Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort);
if (mDebugSelectedChan != null) {
mDebugSelectedChan.close();
}
mDebugSelectedChan = ServerSocketChannel.open();
mDebugSelectedChan.configureBlocking(false); // required for Selector
InetSocketAddress addr = new InetSocketAddress(
InetAddress.getByName("localhost"), //$NON-NLS-1$
mNewDebugSelectedPort);
mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR
try {
mDebugSelectedChan.socket().bind(addr);
if (mSelectedClient != null) {
mSelectedClient.update(Client.CHANGE_PORT);
}
mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this);
return true;
} catch (java.net.BindException e) {
displayDebugSelectedBindError(mNewDebugSelectedPort);
// do not attempt to reopen it.
mDebugSelectedChan = null;
mNewDebugSelectedPort = -1;
return false;
}
}
/*
* We have some activity on the "debug selected" port. Handle it.
*/
private void processDebugSelectedActivity(SelectionKey key) {
assert key.isAcceptable();
ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel();
/*
* Find the debugger associated with the currently-selected client.
*/
if (mSelectedClient != null) {
Debugger dbg = mSelectedClient.getDebugger();
if (dbg != null) {
Log.i("ddms", "Accepting connection on 'debug selected' port");
try {
acceptNewDebugger(dbg, acceptChan);
} catch (IOException ioe) {
// client should be gone, keep going
}
return;
}
}
Log.w("ddms",
"Connection on 'debug selected' port, but none selected");
try {
SocketChannel chan = acceptChan.accept();
chan.close();
} catch (IOException ioe) {
// not expected; client should be gone, keep going
} catch (NotYetBoundException e) {
displayDebugSelectedBindError(mDebugSelectedPort);
}
}
private void displayDebugSelectedBindError(int port) {
String message = String.format(
"Could not open Selected VM debug port (%1$d). Make sure you do not have another instance of DDMS or of the eclipse plugin running. If it's being used by something else, choose a new port number in the preferences.",
port);
Log.logAndDisplay(LogLevel.ERROR, "ddms", message);
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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 java.io.UnsupportedEncodingException;
import java.util.ArrayList;
/**
* Base implementation of {@link IShellOutputReceiver}, that takes the raw data coming from the
* socket, and convert it into {@link String} objects.
* <p/>Additionally, it splits the string by lines.
* <p/>Classes extending it must implement {@link #processNewLines(String[])} which receives
* new parsed lines as they become available.
*/
public abstract class MultiLineReceiver implements IShellOutputReceiver {
private boolean mTrimLines = true;
/** unfinished message line, stored for next packet */
private String mUnfinishedLine = null;
private final ArrayList<String> mArray = new ArrayList<String>();
/**
* Set the trim lines flag.
* @param trim hether the lines are trimmed, or not.
*/
public void setTrimLine(boolean trim) {
mTrimLines = trim;
}
/* (non-Javadoc)
* @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(
* byte[], int, int)
*/
public final void addOutput(byte[] data, int offset, int length) {
if (isCancelled() == false) {
String s = null;
try {
s = new String(data, offset, length, "ISO-8859-1"); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
// normal encoding didn't work, try the default one
s = new String(data, offset,length);
}
// ok we've got a string
if (s != null) {
// if we had an unfinished line we add it.
if (mUnfinishedLine != null) {
s = mUnfinishedLine + s;
mUnfinishedLine = null;
}
// now we split the lines
mArray.clear();
int start = 0;
do {
int index = s.indexOf("\r\n", start); //$NON-NLS-1$
// if \r\n was not found, this is an unfinished line
// and we store it to be processed for the next packet
if (index == -1) {
mUnfinishedLine = s.substring(start);
break;
}
// so we found a \r\n;
// extract the line
String line = s.substring(start, index);
if (mTrimLines) {
line = line.trim();
}
mArray.add(line);
// move start to after the \r\n we found
start = index + 2;
} while (true);
if (mArray.size() > 0) {
// at this point we've split all the lines.
// make the array
String[] lines = mArray.toArray(new String[mArray.size()]);
// send it for final processing
processNewLines(lines);
}
}
}
}
/* (non-Javadoc)
* @see com.android.ddmlib.adb.IShellOutputReceiver#flush()
*/
public final void flush() {
if (mUnfinishedLine != null) {
processNewLines(new String[] { mUnfinishedLine });
}
done();
}
/**
* Terminates the process. This is called after the last lines have been through
* {@link #processNewLines(String[])}.
*/
public void done() {
// do nothing.
}
/**
* Called when new lines are being received by the remote process.
* <p/>It is guaranteed that the lines are complete when they are given to this method.
* @param lines The array containing the new lines.
*/
public abstract void processNewLines(String[] lines);
}

View File

@@ -0,0 +1,277 @@
/*
* 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 java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Stores native allocation information.
* <p/>Contains number of allocations, their size and the stack trace.
* <p/>Note: the ddmlib does not resolve the stack trace automatically. While this class provides
* storage for resolved stack trace, this is merely for convenience.
*/
public final class NativeAllocationInfo {
/* constants for flag bits */
private static final int FLAG_ZYGOTE_CHILD = (1<<31);
private static final int FLAG_MASK = (FLAG_ZYGOTE_CHILD);
/**
* list of alloc functions that are filtered out when attempting to display
* a relevant method responsible for an allocation
*/
private static ArrayList<String> sAllocFunctionFilter;
static {
sAllocFunctionFilter = new ArrayList<String>();
sAllocFunctionFilter.add("malloc"); //$NON-NLS-1$
sAllocFunctionFilter.add("calloc"); //$NON-NLS-1$
sAllocFunctionFilter.add("realloc"); //$NON-NLS-1$
sAllocFunctionFilter.add("get_backtrace"); //$NON-NLS-1$
sAllocFunctionFilter.add("get_hash"); //$NON-NLS-1$
sAllocFunctionFilter.add("??"); //$NON-NLS-1$
sAllocFunctionFilter.add("internal_free"); //$NON-NLS-1$
sAllocFunctionFilter.add("operator new"); //$NON-NLS-1$
sAllocFunctionFilter.add("leak_free"); //$NON-NLS-1$
sAllocFunctionFilter.add("chk_free"); //$NON-NLS-1$
sAllocFunctionFilter.add("chk_memalign"); //$NON-NLS-1$
sAllocFunctionFilter.add("Malloc"); //$NON-NLS-1$
}
private final int mSize;
private final boolean mIsZygoteChild;
private final int mAllocations;
private final ArrayList<Long> mStackCallAddresses = new ArrayList<Long>();
private ArrayList<NativeStackCallInfo> mResolvedStackCall = null;
private boolean mIsStackCallResolved = false;
/**
* Constructs a new {@link NativeAllocationInfo}.
* @param size The size of the allocations.
* @param allocations the allocation count
*/
NativeAllocationInfo(int size, int allocations) {
this.mSize = size & ~FLAG_MASK;
this.mIsZygoteChild = ((size & FLAG_ZYGOTE_CHILD) != 0);
this.mAllocations = allocations;
}
/**
* Adds a stack call address for this allocation.
* @param address The address to add.
*/
void addStackCallAddress(long address) {
mStackCallAddresses.add(address);
}
/**
* Returns the total size of this allocation.
*/
public int getSize() {
return mSize;
}
/**
* Returns whether the allocation happened in a child of the zygote
* process.
*/
public boolean isZygoteChild() {
return mIsZygoteChild;
}
/**
* Returns the allocation count.
*/
public int getAllocationCount() {
return mAllocations;
}
/**
* Returns whether the stack call addresses have been resolved into
* {@link NativeStackCallInfo} objects.
*/
public boolean isStackCallResolved() {
return mIsStackCallResolved;
}
/**
* Returns the stack call of this allocation as raw addresses.
* @return the list of addresses where the allocation happened.
*/
public Long[] getStackCallAddresses() {
return mStackCallAddresses.toArray(new Long[mStackCallAddresses.size()]);
}
/**
* Sets the resolved stack call for this allocation.
* <p/>
* If <code>resolvedStackCall</code> is non <code>null</code> then
* {@link #isStackCallResolved()} will return <code>true</code> after this call.
* @param resolvedStackCall The list of {@link NativeStackCallInfo}.
*/
public synchronized void setResolvedStackCall(List<NativeStackCallInfo> resolvedStackCall) {
if (mResolvedStackCall == null) {
mResolvedStackCall = new ArrayList<NativeStackCallInfo>();
} else {
mResolvedStackCall.clear();
}
mResolvedStackCall.addAll(resolvedStackCall);
mIsStackCallResolved = mResolvedStackCall.size() != 0;
}
/**
* Returns the resolved stack call.
* @return An array of {@link NativeStackCallInfo} or <code>null</code> if the stack call
* was not resolved.
* @see #setResolvedStackCall(ArrayList)
* @see #isStackCallResolved()
*/
public synchronized NativeStackCallInfo[] getResolvedStackCall() {
if (mIsStackCallResolved) {
return mResolvedStackCall.toArray(new NativeStackCallInfo[mResolvedStackCall.size()]);
}
return null;
}
/**
* Indicates whether some other object is "equal to" this one.
* @param obj the reference object with which to compare.
* @return <code>true</code> if this object is equal to the obj argument;
* <code>false</code> otherwise.
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (obj instanceof NativeAllocationInfo) {
NativeAllocationInfo mi = (NativeAllocationInfo)obj;
// quick compare of size, alloc, and stackcall size
if (mSize != mi.mSize || mAllocations != mi.mAllocations ||
mStackCallAddresses.size() != mi.mStackCallAddresses.size()) {
return false;
}
// compare the stack addresses
int count = mStackCallAddresses.size();
for (int i = 0 ; i < count ; i++) {
long a = mStackCallAddresses.get(i);
long b = mi.mStackCallAddresses.get(i);
if (a != b) {
return false;
}
}
return true;
}
return false;
}
/**
* Returns a string representation of the object.
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("Allocations: ");
buffer.append(mAllocations);
buffer.append("\n"); //$NON-NLS-1$
buffer.append("Size: ");
buffer.append(mSize);
buffer.append("\n"); //$NON-NLS-1$
buffer.append("Total Size: ");
buffer.append(mSize * mAllocations);
buffer.append("\n"); //$NON-NLS-1$
Iterator<Long> addrIterator = mStackCallAddresses.iterator();
Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator();
while (sourceIterator.hasNext()) {
long addr = addrIterator.next();
NativeStackCallInfo source = sourceIterator.next();
if (addr == 0)
continue;
if (source.getLineNumber() != -1) {
buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s:%5$d\n", addr,
source.getLibraryName(), source.getMethodName(),
source.getSourceFile(), source.getLineNumber()));
} else {
buffer.append(String.format("\t%1$08x\t%2$s --- %3$s --- %4$s\n", addr,
source.getLibraryName(), source.getMethodName(), source.getSourceFile()));
}
}
return buffer.toString();
}
/**
* Returns the first {@link NativeStackCallInfo} that is relevant.
* <p/>
* A relevant <code>NativeStackCallInfo</code> is a stack call that is not deep in the
* lower level of the libc, but the actual method that performed the allocation.
* @return a <code>NativeStackCallInfo</code> or <code>null</code> if the stack call has not
* been processed from the raw addresses.
* @see #setResolvedStackCall(ArrayList)
* @see #isStackCallResolved()
*/
public synchronized NativeStackCallInfo getRelevantStackCallInfo() {
if (mIsStackCallResolved && mResolvedStackCall != null) {
Iterator<NativeStackCallInfo> sourceIterator = mResolvedStackCall.iterator();
Iterator<Long> addrIterator = mStackCallAddresses.iterator();
while (sourceIterator.hasNext() && addrIterator.hasNext()) {
long addr = addrIterator.next();
NativeStackCallInfo info = sourceIterator.next();
if (addr != 0 && info != null) {
if (isRelevant(info.getMethodName())) {
return info;
}
}
}
// couldnt find a relevant one, so we'll return the first one if it
// exists.
if (mResolvedStackCall.size() > 0)
return mResolvedStackCall.get(0);
}
return null;
}
/**
* Returns true if the method name is relevant.
* @param methodName the method name to test.
*/
private boolean isRelevant(String methodName) {
for (String filter : sAllocFunctionFilter) {
if (methodName.contains(filter)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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;
/**
* Memory address to library mapping for native libraries.
* <p/>
* Each instance represents a single native library and its start and end memory addresses.
*/
public final class NativeLibraryMapInfo {
private long mStartAddr;
private long mEndAddr;
private String mLibrary;
/**
* Constructs a new native library map info.
* @param startAddr The start address of the library.
* @param endAddr The end address of the library.
* @param library The name of the library.
*/
NativeLibraryMapInfo(long startAddr, long endAddr, String library) {
this.mStartAddr = startAddr;
this.mEndAddr = endAddr;
this.mLibrary = library;
}
/**
* Returns the name of the library.
*/
public String getLibraryName() {
return mLibrary;
}
/**
* Returns the start address of the library.
*/
public long getStartAddress() {
return mStartAddr;
}
/**
* Returns the end address of the library.
*/
public long getEndAddress() {
return mEndAddr;
}
/**
* Returns whether the specified address is inside the library.
* @param address The address to test.
* @return <code>true</code> if the address is between the start and end address of the library.
* @see #getStartAddress()
* @see #getEndAddress()
*/
public boolean isWithinLibrary(long address) {
return address >= mStartAddr && address <= mEndAddr;
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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 java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents a stack call. This is used to return all of the call
* information as one object.
*/
public final class NativeStackCallInfo {
private final static Pattern SOURCE_NAME_PATTERN = Pattern.compile("^(.+):(\\d+)$");
/** name of the library */
private String mLibrary;
/** name of the method */
private String mMethod;
/**
* name of the source file + line number in the format<br>
* &lt;sourcefile&gt;:&lt;linenumber&gt;
*/
private String mSourceFile;
private int mLineNumber = -1;
/**
* Basic constructor with library, method, and sourcefile information
*
* @param lib The name of the library
* @param method the name of the method
* @param sourceFile the name of the source file and the line number
* as "[sourcefile]:[fileNumber]"
*/
public NativeStackCallInfo(String lib, String method, String sourceFile) {
mLibrary = lib;
mMethod = method;
Matcher m = SOURCE_NAME_PATTERN.matcher(sourceFile);
if (m.matches()) {
mSourceFile = m.group(1);
try {
mLineNumber = Integer.parseInt(m.group(2));
} catch (NumberFormatException e) {
// do nothing, the line number will stay at -1
}
} else {
mSourceFile = sourceFile;
}
}
/**
* Returns the name of the library name.
*/
public String getLibraryName() {
return mLibrary;
}
/**
* Returns the name of the method.
*/
public String getMethodName() {
return mMethod;
}
/**
* Returns the name of the source file.
*/
public String getSourceFile() {
return mSourceFile;
}
/**
* Returns the line number, or -1 if unknown.
*/
public int getLineNumber() {
return mLineNumber;
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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;
/**
* Implementation of {@link IShellOutputReceiver} that does nothing.
* <p/>This can be used to execute a remote shell command when the output is not needed.
*/
public final class NullOutputReceiver implements IShellOutputReceiver {
private static NullOutputReceiver sReceiver = new NullOutputReceiver();
public static IShellOutputReceiver getReceiver() {
return sReceiver;
}
/* (non-Javadoc)
* @see com.android.ddmlib.adb.IShellOutputReceiver#addOutput(byte[], int, int)
*/
public void addOutput(byte[] data, int offset, int length) {
}
/* (non-Javadoc)
* @see com.android.ddmlib.adb.IShellOutputReceiver#flush()
*/
public void flush() {
}
/* (non-Javadoc)
* @see com.android.ddmlib.adb.IShellOutputReceiver#isCancelled()
*/
public boolean isCancelled() {
return false;
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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;
/**
* Data representing an image taken from a device frame buffer.
*/
public final class RawImage {
/**
* bit-per-pixel value.
*/
public int bpp;
public int size;
public int width;
public int height;
public byte[] data;
}

View File

@@ -0,0 +1,949 @@
/*
* 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.AdbHelper.AdbResponse;
import com.android.ddmlib.FileListingService.FileEntry;
import com.android.ddmlib.utils.ArrayHelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
/**
* Sync service class to push/pull to/from devices/emulators, through the debug bridge.
* <p/>
* To get a {@link SyncService} object, use {@link Device#getSyncService()}.
*/
public final class SyncService {
private final static byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' };
private final static byte[] ID_FAIL = { 'F', 'A', 'I', 'L' };
private final static byte[] ID_STAT = { 'S', 'T', 'A', 'T' };
private final static byte[] ID_RECV = { 'R', 'E', 'C', 'V' };
private final static byte[] ID_DATA = { 'D', 'A', 'T', 'A' };
private final static byte[] ID_DONE = { 'D', 'O', 'N', 'E' };
private final static byte[] ID_SEND = { 'S', 'E', 'N', 'D' };
// private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' };
// private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' };
private final static NullSyncProgresMonitor sNullSyncProgressMonitor =
new NullSyncProgresMonitor();
private final static int S_ISOCK = 0xC000; // type: symbolic link
private final static int S_IFLNK = 0xA000; // type: symbolic link
private final static int S_IFREG = 0x8000; // type: regular file
private final static int S_IFBLK = 0x6000; // type: block device
private final static int S_IFDIR = 0x4000; // type: directory
private final static int S_IFCHR = 0x2000; // type: character device
private final static int S_IFIFO = 0x1000; // type: fifo
/*
private final static int S_ISUID = 0x0800; // set-uid bit
private final static int S_ISGID = 0x0400; // set-gid bit
private final static int S_ISVTX = 0x0200; // sticky bit
private final static int S_IRWXU = 0x01C0; // user permissions
private final static int S_IRUSR = 0x0100; // user: read
private final static int S_IWUSR = 0x0080; // user: write
private final static int S_IXUSR = 0x0040; // user: execute
private final static int S_IRWXG = 0x0038; // group permissions
private final static int S_IRGRP = 0x0020; // group: read
private final static int S_IWGRP = 0x0010; // group: write
private final static int S_IXGRP = 0x0008; // group: execute
private final static int S_IRWXO = 0x0007; // other permissions
private final static int S_IROTH = 0x0004; // other: read
private final static int S_IWOTH = 0x0002; // other: write
private final static int S_IXOTH = 0x0001; // other: execute
*/
private final static int SYNC_DATA_MAX = 64*1024;
private final static int REMOTE_PATH_MAX_LENGTH = 1024;
/** Result code for transfer success. */
public static final int RESULT_OK = 0;
/** Result code for canceled transfer */
public static final int RESULT_CANCELED = 1;
/** Result code for unknown error */
public static final int RESULT_UNKNOWN_ERROR = 2;
/** Result code for network connection error */
public static final int RESULT_CONNECTION_ERROR = 3;
/** Result code for unknown remote object during a pull */
public static final int RESULT_NO_REMOTE_OBJECT = 4;
/** Result code when attempting to pull multiple files into a file */
public static final int RESULT_TARGET_IS_FILE = 5;
/** Result code when attempting to pull multiple into a directory that does not exist. */
public static final int RESULT_NO_DIR_TARGET = 6;
/** Result code for wrong encoding on the remote path. */
public static final int RESULT_REMOTE_PATH_ENCODING = 7;
/** Result code for remote path that is too long. */
public static final int RESULT_REMOTE_PATH_LENGTH = 8;
/** Result code for error while writing local file. */
public static final int RESULT_FILE_WRITE_ERROR = 9;
/** Result code for error while reading local file. */
public static final int RESULT_FILE_READ_ERROR = 10;
/** Result code for attempting to push a file that does not exist. */
public static final int RESULT_NO_LOCAL_FILE = 11;
/** Result code for attempting to push a directory. */
public static final int RESULT_LOCAL_IS_DIRECTORY = 12;
/** Result code for when the target path of a multi file push is a file. */
public static final int RESULT_REMOTE_IS_FILE = 13;
/** Result code for receiving too much data from the remove device at once */
public static final int RESULT_BUFFER_OVERRUN = 14;
/**
* A file transfer result.
* <p/>
* This contains a code, and an optional string
*/
public static class SyncResult {
private int mCode;
private String mMessage;
SyncResult(int code, String message) {
mCode = code;
mMessage = message;
}
SyncResult(int code, Exception e) {
this(code, e.getMessage());
}
SyncResult(int code) {
this(code, errorCodeToString(code));
}
public int getCode() {
return mCode;
}
public String getMessage() {
return mMessage;
}
}
/**
* Classes which implement this interface provide methods that deal
* with displaying transfer progress.
*/
public interface ISyncProgressMonitor {
/**
* Sent when the transfer starts
* @param totalWork the total amount of work.
*/
public void start(int totalWork);
/**
* Sent when the transfer is finished or interrupted.
*/
public void stop();
/**
* Sent to query for possible cancellation.
* @return true if the transfer should be stopped.
*/
public boolean isCanceled();
/**
* Sent when a sub task is started.
* @param name the name of the sub task.
*/
public void startSubTask(String name);
/**
* Sent when some progress have been made.
* @param work the amount of work done.
*/
public void advance(int work);
}
/**
* A Sync progress monitor that does nothing
*/
private static class NullSyncProgresMonitor implements ISyncProgressMonitor {
public void advance(int work) {
}
public boolean isCanceled() {
return false;
}
public void start(int totalWork) {
}
public void startSubTask(String name) {
}
public void stop() {
}
}
private InetSocketAddress mAddress;
private Device mDevice;
private SocketChannel mChannel;
/**
* Buffer used to send data. Allocated when needed and reused afterward.
*/
private byte[] mBuffer;
/**
* Creates a Sync service object.
* @param address The address to connect to
* @param device the {@link Device} that the service connects to.
*/
SyncService(InetSocketAddress address, Device device) {
mAddress = address;
mDevice = device;
}
/**
* Opens the sync connection. This must be called before any calls to push[File] / pull[File].
* @return true if the connection opened, false otherwise.
*/
boolean openSync() {
try {
mChannel = SocketChannel.open(mAddress);
mChannel.configureBlocking(false);
// target a specific device
AdbHelper.setDevice(mChannel, mDevice);
byte[] request = AdbHelper.formAdbRequest("sync:"); // $NON-NLS-1$
AdbHelper.write(mChannel, request, -1, AdbHelper.STD_TIMEOUT);
AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */);
if (!resp.ioSuccess || !resp.okay) {
Log.w("ddms",
"Got timeout or unhappy response from ADB sync req: "
+ resp.message);
mChannel.close();
mChannel = null;
return false;
}
} catch (IOException e) {
if (mChannel != null) {
try {
mChannel.close();
} catch (IOException e1) {
// we do nothing, since we'll return false just below
}
mChannel = null;
return false;
}
}
return true;
}
/**
* Closes the connection.
*/
public void close() {
if (mChannel != null) {
try {
mChannel.close();
} catch (IOException e) {
// nothing to be done really...
}
mChannel = null;
}
}
/**
* Returns a sync progress monitor that does nothing. This allows background tasks that don't
* want/need to display ui, to pass a valid {@link ISyncProgressMonitor}.
* <p/>This object can be reused multiple times and can be used by concurrent threads.
*/
public static ISyncProgressMonitor getNullProgressMonitor() {
return sNullSyncProgressMonitor;
}
/**
* Converts an error code into a non-localized string
* @param code the error code;
*/
private static String errorCodeToString(int code) {
switch (code) {
case RESULT_OK:
return "Success.";
case RESULT_CANCELED:
return "Tranfert canceled by the user.";
case RESULT_UNKNOWN_ERROR:
return "Unknown Error.";
case RESULT_CONNECTION_ERROR:
return "Adb Connection Error.";
case RESULT_NO_REMOTE_OBJECT:
return "Remote object doesn't exist!";
case RESULT_TARGET_IS_FILE:
return "Target object is a file.";
case RESULT_NO_DIR_TARGET:
return "Target directory doesn't exist.";
case RESULT_REMOTE_PATH_ENCODING:
return "Remote Path encoding is not supported.";
case RESULT_REMOTE_PATH_LENGTH:
return "Remove path is too long.";
case RESULT_FILE_WRITE_ERROR:
return "Writing local file failed!";
case RESULT_FILE_READ_ERROR:
return "Reading local file failed!";
case RESULT_NO_LOCAL_FILE:
return "Local file doesn't exist.";
case RESULT_LOCAL_IS_DIRECTORY:
return "Local path is a directory.";
case RESULT_REMOTE_IS_FILE:
return "Remote path is a file.";
case RESULT_BUFFER_OVERRUN:
return "Receiving too much data.";
}
throw new RuntimeException();
}
/**
* Pulls file(s) or folder(s).
* @param entries the remote item(s) to pull
* @param localPath The local destination. If the entries count is > 1 or
* if the unique entry is a folder, this should be a folder.
* @param monitor The progress monitor. Cannot be null.
* @return a {@link SyncResult} object with a code and an optional message.
*
* @see FileListingService.FileEntry
* @see #getNullProgressMonitor()
*/
public SyncResult pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) {
// first we check the destination is a directory and exists
File f = new File(localPath);
if (f.exists() == false) {
return new SyncResult(RESULT_NO_DIR_TARGET);
}
if (f.isDirectory() == false) {
return new SyncResult(RESULT_TARGET_IS_FILE);
}
// get a FileListingService object
FileListingService fls = new FileListingService(mDevice);
// compute the number of file to move
int total = getTotalRemoteFileSize(entries, fls);
// start the monitor
monitor.start(total);
SyncResult result = doPull(entries, localPath, fls, monitor);
monitor.stop();
return result;
}
/**
* Pulls a single file.
* @param remote the remote file
* @param localFilename The local destination.
* @param monitor The progress monitor. Cannot be null.
* @return a {@link SyncResult} object with a code and an optional message.
*
* @see FileListingService.FileEntry
* @see #getNullProgressMonitor()
*/
public SyncResult pullFile(FileEntry remote, String localFilename,
ISyncProgressMonitor monitor) {
int total = remote.getSizeValue();
monitor.start(total);
SyncResult result = doPullFile(remote.getFullPath(), localFilename, monitor);
monitor.stop();
return result;
}
/**
* Push several files.
* @param local An array of loca files to push
* @param remote the remote {@link FileEntry} representing a directory.
* @param monitor The progress monitor. Cannot be null.
* @return a {@link SyncResult} object with a code and an optional message.
*/
public SyncResult push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) {
if (remote.isDirectory() == false) {
return new SyncResult(RESULT_REMOTE_IS_FILE);
}
// make a list of File from the list of String
ArrayList<File> files = new ArrayList<File>();
for (String path : local) {
files.add(new File(path));
}
// get the total count of the bytes to transfer
File[] fileArray = files.toArray(new File[files.size()]);
int total = getTotalLocalFileSize(fileArray);
monitor.start(total);
SyncResult result = doPush(fileArray, remote.getFullPath(), monitor);
monitor.stop();
return result;
}
/**
* Push a single file.
* @param local the local filepath.
* @param remote The remote filepath.
* @param monitor The progress monitor. Cannot be null.
* @return a {@link SyncResult} object with a code and an optional message.
*/
public SyncResult pushFile(String local, String remote, ISyncProgressMonitor monitor) {
File f = new File(local);
if (f.exists() == false) {
return new SyncResult(RESULT_NO_LOCAL_FILE);
}
if (f.isDirectory()) {
return new SyncResult(RESULT_LOCAL_IS_DIRECTORY);
}
monitor.start((int)f.length());
SyncResult result = doPushFile(local, remote, monitor);
monitor.stop();
return result;
}
/**
* compute the recursive file size of all the files in the list. Folder
* have a weight of 1.
* @param entries
* @param fls
* @return
*/
private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) {
int count = 0;
for (FileEntry e : entries) {
int type = e.getType();
if (type == FileListingService.TYPE_DIRECTORY) {
// get the children
FileEntry[] children = fls.getChildren(e, false, null);
count += getTotalRemoteFileSize(children, fls) + 1;
} else if (type == FileListingService.TYPE_FILE) {
count += e.getSizeValue();
}
}
return count;
}
/**
* compute the recursive file size of all the files in the list. Folder
* have a weight of 1.
* This does not check for circular links.
* @param files
* @return
*/
private int getTotalLocalFileSize(File[] files) {
int count = 0;
for (File f : files) {
if (f.exists()) {
if (f.isDirectory()) {
return getTotalLocalFileSize(f.listFiles()) + 1;
} else if (f.isFile()) {
count += f.length();
}
}
}
return count;
}
/**
* Pulls multiple files/folders recursively.
* @param entries The list of entry to pull
* @param localPath the localpath to a directory
* @param fileListingService a FileListingService object to browse through remote directories.
* @param monitor the progress monitor. Must be started already.
* @return a {@link SyncResult} object with a code and an optional message.
*/
private SyncResult doPull(FileEntry[] entries, String localPath,
FileListingService fileListingService,
ISyncProgressMonitor monitor) {
for (FileEntry e : entries) {
// check if we're cancelled
if (monitor.isCanceled() == true) {
return new SyncResult(RESULT_CANCELED);
}
// get type (we only pull directory and files for now)
int type = e.getType();
if (type == FileListingService.TYPE_DIRECTORY) {
monitor.startSubTask(e.getFullPath());
String dest = localPath + File.separator + e.getName();
// make the directory
File d = new File(dest);
d.mkdir();
// then recursively call the content. Since we did a ls command
// to get the number of files, we can use the cache
FileEntry[] children = fileListingService.getChildren(e, true, null);
SyncResult result = doPull(children, dest, fileListingService, monitor);
if (result.mCode != RESULT_OK) {
return result;
}
monitor.advance(1);
} else if (type == FileListingService.TYPE_FILE) {
monitor.startSubTask(e.getFullPath());
String dest = localPath + File.separator + e.getName();
SyncResult result = doPullFile(e.getFullPath(), dest, monitor);
if (result.mCode != RESULT_OK) {
return result;
}
}
}
return new SyncResult(RESULT_OK);
}
/**
* Pulls a remote file
* @param remotePath the remote file (length max is 1024)
* @param localPath the local destination
* @param monitor the monitor. The monitor must be started already.
* @return a {@link SyncResult} object with a code and an optional message.
*/
private SyncResult doPullFile(String remotePath, String localPath,
ISyncProgressMonitor monitor) {
byte[] msg = null;
byte[] pullResult = new byte[8];
try {
byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
return new SyncResult(RESULT_REMOTE_PATH_LENGTH);
}
// create the full request message
msg = createFileReq(ID_RECV, remotePathContent);
// and send it.
AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
// read the result, in a byte array containing 2 ints
// (id, size)
AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT);
// check we have the proper data back
if (checkResult(pullResult, ID_DATA) == false &&
checkResult(pullResult, ID_DONE) == false) {
return new SyncResult(RESULT_CONNECTION_ERROR);
}
} catch (UnsupportedEncodingException e) {
return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e);
} catch (IOException e) {
return new SyncResult(RESULT_CONNECTION_ERROR, e);
}
// access the destination file
File f = new File(localPath);
// create the stream to write in the file. We use a new try/catch block to differentiate
// between file and network io exceptions.
FileOutputStream fos = null;
try {
fos = new FileOutputStream(f);
} catch (FileNotFoundException e) {
return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
}
// the buffer to read the data
byte[] data = new byte[SYNC_DATA_MAX];
// loop to get data until we're done.
while (true) {
// check if we're cancelled
if (monitor.isCanceled() == true) {
return new SyncResult(RESULT_CANCELED);
}
// if we're done, we stop the loop
if (checkResult(pullResult, ID_DONE)) {
break;
}
if (checkResult(pullResult, ID_DATA) == false) {
// hmm there's an error
return new SyncResult(RESULT_CONNECTION_ERROR);
}
int length = ArrayHelper.swap32bitFromArray(pullResult, 4);
if (length > SYNC_DATA_MAX) {
// buffer overrun!
// error and exit
return new SyncResult(RESULT_BUFFER_OVERRUN);
}
try {
// now read the length we received
AdbHelper.read(mChannel, data, length, AdbHelper.STD_TIMEOUT);
// get the header for the next packet.
AdbHelper.read(mChannel, pullResult, -1, AdbHelper.STD_TIMEOUT);
} catch (IOException e) {
return new SyncResult(RESULT_CONNECTION_ERROR, e);
}
// write the content in the file
try {
fos.write(data, 0, length);
} catch (IOException e) {
return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
}
monitor.advance(length);
}
try {
fos.flush();
} catch (IOException e) {
return new SyncResult(RESULT_FILE_WRITE_ERROR, e);
}
return new SyncResult(RESULT_OK);
}
/**
* Push multiple files
* @param fileArray
* @param remotePath
* @param monitor
* @return a {@link SyncResult} object with a code and an optional message.
*/
private SyncResult doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) {
for (File f : fileArray) {
// check if we're canceled
if (monitor.isCanceled() == true) {
return new SyncResult(RESULT_CANCELED);
}
if (f.exists()) {
if (f.isDirectory()) {
// append the name of the directory to the remote path
String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S
monitor.startSubTask(dest);
SyncResult result = doPush(f.listFiles(), dest, monitor);
if (result.mCode != RESULT_OK) {
return result;
}
monitor.advance(1);
} else if (f.isFile()) {
// append the name of the file to the remote path
String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S
monitor.startSubTask(remoteFile);
SyncResult result = doPushFile(f.getAbsolutePath(), remoteFile, monitor);
if (result.mCode != RESULT_OK) {
return result;
}
}
}
}
return new SyncResult(RESULT_OK);
}
/**
* Push a single file
* @param localPath the local file to push
* @param remotePath the remote file (length max is 1024)
* @param monitor the monitor. The monitor must be started already.
* @return a {@link SyncResult} object with a code and an optional message.
*/
private SyncResult doPushFile(String localPath, String remotePath,
ISyncProgressMonitor monitor) {
FileInputStream fis = null;
byte[] msg;
try {
byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);
if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
return new SyncResult(RESULT_REMOTE_PATH_LENGTH);
}
File f = new File(localPath);
// this shouldn't happen but still...
if (f.exists() == false) {
return new SyncResult(RESULT_NO_LOCAL_FILE);
}
// create the stream to read the file
fis = new FileInputStream(f);
// create the header for the action
msg = createSendFileReq(ID_SEND, remotePathContent, 0644);
} catch (UnsupportedEncodingException e) {
return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e);
} catch (FileNotFoundException e) {
return new SyncResult(RESULT_FILE_READ_ERROR, e);
}
// and send it. We use a custom try/catch block to make the difference between
// file and network IO exceptions.
try {
AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
} catch (IOException e) {
return new SyncResult(RESULT_CONNECTION_ERROR, e);
}
// create the buffer used to read.
// we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning.
if (mBuffer == null) {
mBuffer = new byte[SYNC_DATA_MAX + 8];
}
System.arraycopy(ID_DATA, 0, mBuffer, 0, ID_DATA.length);
// look while there is something to read
while (true) {
// check if we're canceled
if (monitor.isCanceled() == true) {
return new SyncResult(RESULT_CANCELED);
}
// read up to SYNC_DATA_MAX
int readCount = 0;
try {
readCount = fis.read(mBuffer, 8, SYNC_DATA_MAX);
} catch (IOException e) {
return new SyncResult(RESULT_FILE_READ_ERROR, e);
}
if (readCount == -1) {
// we reached the end of the file
break;
}
// now send the data to the device
// first write the amount read
ArrayHelper.swap32bitsToArray(readCount, mBuffer, 4);
// now write it
try {
AdbHelper.write(mChannel, mBuffer, readCount+8, AdbHelper.STD_TIMEOUT);
} catch (IOException e) {
return new SyncResult(RESULT_CONNECTION_ERROR, e);
}
// and advance the monitor
monitor.advance(readCount);
}
// close the local file
try {
fis.close();
} catch (IOException e) {
return new SyncResult(RESULT_FILE_READ_ERROR, e);
}
try {
// create the DONE message
long time = System.currentTimeMillis() / 1000;
msg = createReq(ID_DONE, (int)time);
// and send it.
AdbHelper.write(mChannel, msg, -1, AdbHelper.STD_TIMEOUT);
// read the result, in a byte array containing 2 ints
// (id, size)
byte[] result = new byte[8];
AdbHelper.read(mChannel, result, -1 /* full length */, AdbHelper.STD_TIMEOUT);
if (checkResult(result, ID_OKAY) == false) {
if (checkResult(result, ID_FAIL)) {
// read some error message...
int len = ArrayHelper.swap32bitFromArray(result, 4);
AdbHelper.read(mChannel, mBuffer, len, AdbHelper.STD_TIMEOUT);
// output the result?
String message = new String(mBuffer, 0, len);
Log.e("ddms", "transfer error: " + message);
return new SyncResult(RESULT_UNKNOWN_ERROR, message);
}
return new SyncResult(RESULT_UNKNOWN_ERROR);
}
} catch (IOException e) {
return new SyncResult(RESULT_CONNECTION_ERROR, e);
}
return new SyncResult(RESULT_OK);
}
/**
* Returns the mode of the remote file.
* @param path the remote file
* @return and Integer containing the mode if all went well or null
* otherwise
*/
private Integer readMode(String path) {
try {
// create the stat request message.
byte[] msg = createFileReq(ID_STAT, path);
AdbHelper.write(mChannel, msg, -1 /* full length */, AdbHelper.STD_TIMEOUT);
// read the result, in a byte array containing 4 ints
// (id, mode, size, time)
byte[] statResult = new byte[16];
AdbHelper.read(mChannel, statResult, -1 /* full length */, AdbHelper.STD_TIMEOUT);
// check we have the proper data back
if (checkResult(statResult, ID_STAT) == false) {
return null;
}
// we return the mode (2nd int in the array)
return ArrayHelper.swap32bitFromArray(statResult, 4);
} catch (IOException e) {
return null;
}
}
/**
* Create a command with a code and an int values
* @param command
* @param value
* @return
*/
private static byte[] createReq(byte[] command, int value) {
byte[] array = new byte[8];
System.arraycopy(command, 0, array, 0, 4);
ArrayHelper.swap32bitsToArray(value, array, 4);
return array;
}
/**
* Creates the data array for a stat request.
* @param command the 4 byte command (ID_STAT, ID_RECV, ...)
* @param path The path of the remote file on which to execute the command
* @return the byte[] to send to the device through adb
*/
private static byte[] createFileReq(byte[] command, String path) {
byte[] pathContent = null;
try {
pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
return null;
}
return createFileReq(command, pathContent);
}
/**
* Creates the data array for a file request. This creates an array with a 4 byte command + the
* remote file name.
* @param command the 4 byte command (ID_STAT, ID_RECV, ...).
* @param path The path, as a byte array, of the remote file on which to
* execute the command.
* @return the byte[] to send to the device through adb
*/
private static byte[] createFileReq(byte[] command, byte[] path) {
byte[] array = new byte[8 + path.length];
System.arraycopy(command, 0, array, 0, 4);
ArrayHelper.swap32bitsToArray(path.length, array, 4);
System.arraycopy(path, 0, array, 8, path.length);
return array;
}
private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) {
// make the mode into a string
String modeStr = "," + (mode & 0777); // $NON-NLS-1S
byte[] modeContent = null;
try {
modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
return null;
}
byte[] array = new byte[8 + path.length + modeContent.length];
System.arraycopy(command, 0, array, 0, 4);
ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4);
System.arraycopy(path, 0, array, 8, path.length);
System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length);
return array;
}
/**
* Checks the result array starts with the provided code
* @param result The result array to check
* @param code The 4 byte code.
* @return true if the code matches.
*/
private static boolean checkResult(byte[] result, byte[] code) {
if (result[0] != code[0] ||
result[1] != code[1] ||
result[2] != code[2] ||
result[3] != code[3]) {
return false;
}
return true;
}
private static int getFileType(int mode) {
if ((mode & S_ISOCK) == S_ISOCK) {
return FileListingService.TYPE_SOCKET;
}
if ((mode & S_IFLNK) == S_IFLNK) {
return FileListingService.TYPE_LINK;
}
if ((mode & S_IFREG) == S_IFREG) {
return FileListingService.TYPE_FILE;
}
if ((mode & S_IFBLK) == S_IFBLK) {
return FileListingService.TYPE_BLOCK;
}
if ((mode & S_IFDIR) == S_IFDIR) {
return FileListingService.TYPE_DIRECTORY;
}
if ((mode & S_IFCHR) == S_IFCHR) {
return FileListingService.TYPE_CHARACTER;
}
if ((mode & S_IFIFO) == S_IFIFO) {
return FileListingService.TYPE_FIFO;
}
return FileListingService.TYPE_OTHER;
}
}

View File

@@ -0,0 +1,139 @@
/*
* 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;
/**
* Holds a thread information.
*/
public final class ThreadInfo implements IStackTraceInfo {
private int mThreadId;
private String mThreadName;
private int mStatus;
private int mTid;
private int mUtime;
private int mStime;
private boolean mIsDaemon;
private StackTraceElement[] mTrace;
private long mTraceTime;
// priority?
// total CPU used?
// method at top of stack?
/**
* Construct with basic identification.
*/
ThreadInfo(int threadId, String threadName) {
mThreadId = threadId;
mThreadName = threadName;
mStatus = -1;
//mTid = mUtime = mStime = 0;
//mIsDaemon = false;
}
/**
* Set with the values we get from a THST chunk.
*/
void updateThread(int status, int tid, int utime, int stime, boolean isDaemon) {
mStatus = status;
mTid = tid;
mUtime = utime;
mStime = stime;
mIsDaemon = isDaemon;
}
/**
* Sets the stack call of the thread.
* @param trace stackcall information.
*/
void setStackCall(StackTraceElement[] trace) {
mTrace = trace;
mTraceTime = System.currentTimeMillis();
}
/**
* Returns the thread's ID.
*/
public int getThreadId() {
return mThreadId;
}
/**
* Returns the thread's name.
*/
public String getThreadName() {
return mThreadName;
}
void setThreadName(String name) {
mThreadName = name;
}
/**
* Returns the system tid.
*/
public int getTid() {
return mTid;
}
/**
* Returns the VM thread status.
*/
public int getStatus() {
return mStatus;
}
/**
* Returns the cumulative user time.
*/
public int getUtime() {
return mUtime;
}
/**
* Returns the cumulative system time.
*/
public int getStime() {
return mStime;
}
/**
* Returns whether this is a daemon thread.
*/
public boolean isDaemon() {
return mIsDaemon;
}
/*
* (non-Javadoc)
* @see com.android.ddmlib.IStackTraceInfo#getStackTrace()
*/
public StackTraceElement[] getStackTrace() {
return mTrace;
}
/**
* Returns the approximate time of the stacktrace data.
* @see #getStackTrace()
*/
public long getStackCallTime() {
return mTraceTime;
}
}

View File

@@ -0,0 +1,461 @@
/*
* Copyright (C) 2008 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.log;
import com.android.ddmlib.log.LogReceiver.LogEntry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents an event and its data.
*/
public class EventContainer {
/**
* Comparison method for {@link EventContainer#testValue(int, Object, com.android.ddmlib.log.EventContainer.CompareMethod)}
*
*/
public enum CompareMethod {
EQUAL_TO("equals", "=="),
LESSER_THAN("less than or equals to", "<="),
LESSER_THAN_STRICT("less than", "<"),
GREATER_THAN("greater than or equals to", ">="),
GREATER_THAN_STRICT("greater than", ">"),
BIT_CHECK("bit check", "&");
private final String mName;
private final String mTestString;
private CompareMethod(String name, String testString) {
mName = name;
mTestString = testString;
}
/**
* Returns the display string.
*/
@Override
public String toString() {
return mName;
}
/**
* Returns a short string representing the comparison.
*/
public String testString() {
return mTestString;
}
}
/**
* Type for event data.
*/
public static enum EventValueType {
UNKNOWN(0),
INT(1),
LONG(2),
STRING(3),
LIST(4),
TREE(5);
private final static Pattern STORAGE_PATTERN = Pattern.compile("^(\\d+)@(.*)$"); //$NON-NLS-1$
private int mValue;
/**
* Returns a {@link EventValueType} from an integer value, or <code>null</code> if no match
* was found.
* @param value the integer value.
*/
static EventValueType getEventValueType(int value) {
for (EventValueType type : values()) {
if (type.mValue == value) {
return type;
}
}
return null;
}
/**
* Returns a storage string for an {@link Object} of type supported by
* {@link EventValueType}.
* <p/>
* Strings created by this method can be reloaded with
* {@link #getObjectFromStorageString(String)}.
* <p/>
* NOTE: for now, only {@link #STRING}, {@link #INT}, and {@link #LONG} are supported.
* @param object the object to "convert" into a storage string.
* @return a string storing the object and its type or null if the type was not recognized.
*/
public static String getStorageString(Object object) {
if (object instanceof String) {
return STRING.mValue + "@" + (String)object; //$NON-NLS-1$
} else if (object instanceof Integer) {
return INT.mValue + "@" + object.toString(); //$NON-NLS-1$
} else if (object instanceof Long) {
return LONG.mValue + "@" + object.toString(); //$NON-NLS-1$
}
return null;
}
/**
* Creates an {@link Object} from a storage string created with
* {@link #getStorageString(Object)}.
* @param value the storage string
* @return an {@link Object} or null if the string or type were not recognized.
*/
public static Object getObjectFromStorageString(String value) {
Matcher m = STORAGE_PATTERN.matcher(value);
if (m.matches()) {
try {
EventValueType type = getEventValueType(Integer.parseInt(m.group(1)));
if (type == null) {
return null;
}
switch (type) {
case STRING:
return m.group(2);
case INT:
return Integer.valueOf(m.group(2));
case LONG:
return Long.valueOf(m.group(2));
}
} catch (NumberFormatException nfe) {
return null;
}
}
return null;
}
/**
* Returns the integer value of the enum.
*/
public int getValue() {
return mValue;
}
@Override
public String toString() {
return super.toString().toLowerCase();
}
private EventValueType(int value) {
mValue = value;
}
}
public int mTag;
public int pid; /* generating process's pid */
public int tid; /* generating process's tid */
public int sec; /* seconds since Epoch */
public int nsec; /* nanoseconds */
private Object mData;
/**
* Creates an {@link EventContainer} from a {@link LogEntry}.
* @param entry the LogEntry from which pid, tid, and time info is copied.
* @param tag the event tag value
* @param data the data of the EventContainer.
*/
EventContainer(LogEntry entry, int tag, Object data) {
getType(data);
mTag = tag;
mData = data;
pid = entry.pid;
tid = entry.tid;
sec = entry.sec;
nsec = entry.nsec;
}
/**
* Creates an {@link EventContainer} with raw data
*/
EventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) {
getType(data);
mTag = tag;
mData = data;
this.pid = pid;
this.tid = tid;
this.sec = sec;
this.nsec = nsec;
}
/**
* Returns the data as an int.
* @throws InvalidTypeException if the data type is not {@link EventValueType#INT}.
* @see #getType()
*/
public final Integer getInt() throws InvalidTypeException {
if (getType(mData) == EventValueType.INT) {
return (Integer)mData;
}
throw new InvalidTypeException();
}
/**
* Returns the data as a long.
* @throws InvalidTypeException if the data type is not {@link EventValueType#LONG}.
* @see #getType()
*/
public final Long getLong() throws InvalidTypeException {
if (getType(mData) == EventValueType.LONG) {
return (Long)mData;
}
throw new InvalidTypeException();
}
/**
* Returns the data as a String.
* @throws InvalidTypeException if the data type is not {@link EventValueType#STRING}.
* @see #getType()
*/
public final String getString() throws InvalidTypeException {
if (getType(mData) == EventValueType.STRING) {
return (String)mData;
}
throw new InvalidTypeException();
}
/**
* Returns a value by index. The return type is defined by its type.
* @param valueIndex the index of the value. If the data is not a list, this is ignored.
*/
public Object getValue(int valueIndex) {
return getValue(mData, valueIndex, true);
}
/**
* Returns a value by index as a double.
* @param valueIndex the index of the value. If the data is not a list, this is ignored.
* @throws InvalidTypeException if the data type is not {@link EventValueType#INT},
* {@link EventValueType#LONG}, {@link EventValueType#LIST}, or if the item in the
* list at index <code>valueIndex</code> is not of type {@link EventValueType#INT} or
* {@link EventValueType#LONG}.
* @see #getType()
*/
public double getValueAsDouble(int valueIndex) throws InvalidTypeException {
return getValueAsDouble(mData, valueIndex, true);
}
/**
* Returns a value by index as a String.
* @param valueIndex the index of the value. If the data is not a list, this is ignored.
* @throws InvalidTypeException if the data type is not {@link EventValueType#INT},
* {@link EventValueType#LONG}, {@link EventValueType#STRING}, {@link EventValueType#LIST},
* or if the item in the list at index <code>valueIndex</code> is not of type
* {@link EventValueType#INT}, {@link EventValueType#LONG}, or {@link EventValueType#STRING}
* @see #getType()
*/
public String getValueAsString(int valueIndex) throws InvalidTypeException {
return getValueAsString(mData, valueIndex, true);
}
/**
* Returns the type of the data.
*/
public EventValueType getType() {
return getType(mData);
}
/**
* Returns the type of an object.
*/
public final EventValueType getType(Object data) {
if (data instanceof Integer) {
return EventValueType.INT;
} else if (data instanceof Long) {
return EventValueType.LONG;
} else if (data instanceof String) {
return EventValueType.STRING;
} else if (data instanceof Object[]) {
// loop through the list to see if we have another list
Object[] objects = (Object[])data;
for (Object obj : objects) {
EventValueType type = getType(obj);
if (type == EventValueType.LIST || type == EventValueType.TREE) {
return EventValueType.TREE;
}
}
return EventValueType.LIST;
}
return EventValueType.UNKNOWN;
}
/**
* Checks that the <code>index</code>-th value of this event against a provided value.
* @param index the index of the value to test
* @param value the value to test against
* @param compareMethod the method of testing
* @return true if the test passed.
* @throws InvalidTypeException in case of type mismatch between the value to test and the value
* to test against, or if the compare method is incompatible with the type of the values.
* @see CompareMethod
*/
public boolean testValue(int index, Object value,
CompareMethod compareMethod) throws InvalidTypeException {
EventValueType type = getType(mData);
if (index > 0 && type != EventValueType.LIST) {
throw new InvalidTypeException();
}
Object data = mData;
if (type == EventValueType.LIST) {
data = ((Object[])mData)[index];
}
if (data.getClass().equals(data.getClass()) == false) {
throw new InvalidTypeException();
}
switch (compareMethod) {
case EQUAL_TO:
return data.equals(value);
case LESSER_THAN:
if (data instanceof Integer) {
return (((Integer)data).compareTo((Integer)value) <= 0);
} else if (data instanceof Long) {
return (((Long)data).compareTo((Long)value) <= 0);
}
// other types can't use this compare method.
throw new InvalidTypeException();
case LESSER_THAN_STRICT:
if (data instanceof Integer) {
return (((Integer)data).compareTo((Integer)value) < 0);
} else if (data instanceof Long) {
return (((Long)data).compareTo((Long)value) < 0);
}
// other types can't use this compare method.
throw new InvalidTypeException();
case GREATER_THAN:
if (data instanceof Integer) {
return (((Integer)data).compareTo((Integer)value) >= 0);
} else if (data instanceof Long) {
return (((Long)data).compareTo((Long)value) >= 0);
}
// other types can't use this compare method.
throw new InvalidTypeException();
case GREATER_THAN_STRICT:
if (data instanceof Integer) {
return (((Integer)data).compareTo((Integer)value) > 0);
} else if (data instanceof Long) {
return (((Long)data).compareTo((Long)value) > 0);
}
// other types can't use this compare method.
throw new InvalidTypeException();
case BIT_CHECK:
if (data instanceof Integer) {
return (((Integer)data).intValue() & ((Integer)value).intValue()) != 0;
} else if (data instanceof Long) {
return (((Long)data).longValue() & ((Long)value).longValue()) != 0;
}
// other types can't use this compare method.
throw new InvalidTypeException();
default :
throw new InvalidTypeException();
}
}
private final Object getValue(Object data, int valueIndex, boolean recursive) {
EventValueType type = getType(data);
switch (type) {
case INT:
case LONG:
case STRING:
return data;
case LIST:
if (recursive) {
Object[] list = (Object[]) data;
if (valueIndex >= 0 && valueIndex < list.length) {
return getValue(list[valueIndex], valueIndex, false);
}
}
}
return null;
}
private final double getValueAsDouble(Object data, int valueIndex, boolean recursive)
throws InvalidTypeException {
EventValueType type = getType(data);
switch (type) {
case INT:
return ((Integer)data).doubleValue();
case LONG:
return ((Long)data).doubleValue();
case STRING:
throw new InvalidTypeException();
case LIST:
if (recursive) {
Object[] list = (Object[]) data;
if (valueIndex >= 0 && valueIndex < list.length) {
return getValueAsDouble(list[valueIndex], valueIndex, false);
}
}
}
throw new InvalidTypeException();
}
private final String getValueAsString(Object data, int valueIndex, boolean recursive)
throws InvalidTypeException {
EventValueType type = getType(data);
switch (type) {
case INT:
return ((Integer)data).toString();
case LONG:
return ((Long)data).toString();
case STRING:
return (String)data;
case LIST:
if (recursive) {
Object[] list = (Object[]) data;
if (valueIndex >= 0 && valueIndex < list.length) {
return getValueAsString(list[valueIndex], valueIndex, false);
}
} else {
throw new InvalidTypeException(
"getValueAsString() doesn't support EventValueType.TREE");
}
}
throw new InvalidTypeException(
"getValueAsString() unsupported type:" + type);
}
}

View File

@@ -0,0 +1,577 @@
/*
* Copyright (C) 2008 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.log;
import com.android.ddmlib.Device;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
import com.android.ddmlib.log.EventContainer.EventValueType;
import com.android.ddmlib.log.EventValueDescription.ValueType;
import com.android.ddmlib.log.LogReceiver.LogEntry;
import com.android.ddmlib.utils.ArrayHelper;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parser for the "event" log.
*/
public final class EventLogParser {
/** Location of the tag map file on the device */
private final static String EVENT_TAG_MAP_FILE = "/system/etc/event-log-tags"; //$NON-NLS-1$
/**
* Event log entry types. These must match up with the declarations in
* java/android/android/util/EventLog.java.
*/
private final static int EVENT_TYPE_INT = 0;
private final static int EVENT_TYPE_LONG = 1;
private final static int EVENT_TYPE_STRING = 2;
private final static int EVENT_TYPE_LIST = 3;
private final static Pattern PATTERN_SIMPLE_TAG = Pattern.compile(
"^(\\d+)\\s+([A-Za-z0-9_]+)\\s*$"); //$NON-NLS-1$
private final static Pattern PATTERN_TAG_WITH_DESC = Pattern.compile(
"^(\\d+)\\s+([A-Za-z0-9_]+)\\s*(.*)\\s*$"); //$NON-NLS-1$
private final static Pattern PATTERN_DESCRIPTION = Pattern.compile(
"\\(([A-Za-z0-9_\\s]+)\\|(\\d+)(\\|\\d+){0,1}\\)"); //$NON-NLS-1$
private final static Pattern TEXT_LOG_LINE = Pattern.compile(
"(\\d\\d)-(\\d\\d)\\s(\\d\\d):(\\d\\d):(\\d\\d).(\\d{3})\\s+I/([a-zA-Z0-9_]+)\\s*\\(\\s*(\\d+)\\):\\s+(.*)"); //$NON-NLS-1$
private final TreeMap<Integer, String> mTagMap = new TreeMap<Integer, String>();
private final TreeMap<Integer, EventValueDescription[]> mValueDescriptionMap =
new TreeMap<Integer, EventValueDescription[]>();
public EventLogParser() {
}
/**
* Inits the parser for a specific Device.
* <p/>
* This methods reads the event-log-tags located on the device to find out
* what tags are being written to the event log and what their format is.
* @param device The device.
* @return <code>true</code> if success, <code>false</code> if failure or cancellation.
*/
public boolean init(Device device) {
// read the event tag map file on the device.
try {
device.executeShellCommand("cat " + EVENT_TAG_MAP_FILE, //$NON-NLS-1$
new MultiLineReceiver() {
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
processTagLine(line);
}
}
public boolean isCancelled() {
return false;
}
});
} catch (IOException e) {
return false;
}
return true;
}
/**
* Inits the parser with the content of a tag file.
* @param tagFileContent the lines of a tag file.
* @return <code>true</code> if success, <code>false</code> if failure.
*/
public boolean init(String[] tagFileContent) {
for (String line : tagFileContent) {
processTagLine(line);
}
return true;
}
/**
* Inits the parser with a specified event-log-tags file.
* @param filePath
* @return <code>true</code> if success, <code>false</code> if failure.
*/
public boolean init(String filePath) {
try {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line = null;
do {
line = reader.readLine();
if (line != null) {
processTagLine(line);
}
} while (line != null);
return true;
} catch (IOException e) {
return false;
}
}
/**
* Processes a line from the event-log-tags file.
* @param line the line to process
*/
private void processTagLine(String line) {
// ignore empty lines and comment lines
if (line.length() > 0 && line.charAt(0) != '#') {
Matcher m = PATTERN_TAG_WITH_DESC.matcher(line);
if (m.matches()) {
try {
int value = Integer.parseInt(m.group(1));
String name = m.group(2);
if (name != null && mTagMap.get(value) == null) {
mTagMap.put(value, name);
}
// special case for the GC tag. We ignore what is in the file,
// and take what the custom GcEventContainer class tells us.
// This is due to the event encoding several values on 2 longs.
// @see GcEventContainer
if (value == GcEventContainer.GC_EVENT_TAG) {
mValueDescriptionMap.put(value,
GcEventContainer.getValueDescriptions());
} else {
String description = m.group(3);
if (description != null && description.length() > 0) {
EventValueDescription[] desc =
processDescription(description);
if (desc != null) {
mValueDescriptionMap.put(value, desc);
}
}
}
} catch (NumberFormatException e) {
// failed to convert the number into a string. just ignore it.
}
} else {
m = PATTERN_SIMPLE_TAG.matcher(line);
if (m.matches()) {
int value = Integer.parseInt(m.group(1));
String name = m.group(2);
if (name != null && mTagMap.get(value) == null) {
mTagMap.put(value, name);
}
}
}
}
}
private EventValueDescription[] processDescription(String description) {
String[] descriptions = description.split("\\s*,\\s*"); //$NON-NLS-1$
ArrayList<EventValueDescription> list = new ArrayList<EventValueDescription>();
for (String desc : descriptions) {
Matcher m = PATTERN_DESCRIPTION.matcher(desc);
if (m.matches()) {
try {
String name = m.group(1);
String typeString = m.group(2);
int typeValue = Integer.parseInt(typeString);
EventValueType eventValueType = EventValueType.getEventValueType(typeValue);
if (eventValueType == null) {
// just ignore this description if the value is not recognized.
// TODO: log the error.
}
typeString = m.group(3);
if (typeString != null && typeString.length() > 0) {
//skip the |
typeString = typeString.substring(1);
typeValue = Integer.parseInt(typeString);
ValueType valueType = ValueType.getValueType(typeValue);
list.add(new EventValueDescription(name, eventValueType, valueType));
} else {
list.add(new EventValueDescription(name, eventValueType));
}
} catch (NumberFormatException nfe) {
// just ignore this description if one number is malformed.
// TODO: log the error.
} catch (InvalidValueTypeException e) {
// just ignore this description if data type and data unit don't match
// TODO: log the error.
}
} else {
Log.e("EventLogParser", //$NON-NLS-1$
String.format("Can't parse %1$s", description)); //$NON-NLS-1$
}
}
if (list.size() == 0) {
return null;
}
return list.toArray(new EventValueDescription[list.size()]);
}
public EventContainer parse(LogEntry entry) {
if (entry.len < 4) {
return null;
}
int inOffset = 0;
int tagValue = ArrayHelper.swap32bitFromArray(entry.data, inOffset);
inOffset += 4;
String tag = mTagMap.get(tagValue);
if (tag == null) {
Log.e("EventLogParser", String.format("unknown tag number: %1$d", tagValue));
}
ArrayList<Object> list = new ArrayList<Object>();
if (parseBinaryEvent(entry.data, inOffset, list) == -1) {
return null;
}
Object data;
if (list.size() == 1) {
data = list.get(0);
} else{
data = list.toArray();
}
EventContainer event = null;
if (tagValue == GcEventContainer.GC_EVENT_TAG) {
event = new GcEventContainer(entry, tagValue, data);
} else {
event = new EventContainer(entry, tagValue, data);
}
return event;
}
public EventContainer parse(String textLogLine) {
// line will look like
// 04-29 23:16:16.691 I/dvm_gc_info( 427): <data>
// where <data> is either
// [value1,value2...]
// or
// value
if (textLogLine.length() == 0) {
return null;
}
// parse the header first
Matcher m = TEXT_LOG_LINE.matcher(textLogLine);
if (m.matches()) {
try {
int month = Integer.parseInt(m.group(1));
int day = Integer.parseInt(m.group(2));
int hours = Integer.parseInt(m.group(3));
int minutes = Integer.parseInt(m.group(4));
int seconds = Integer.parseInt(m.group(5));
int milliseconds = Integer.parseInt(m.group(6));
// convert into seconds since epoch and nano-seconds.
Calendar cal = Calendar.getInstance();
cal.set(cal.get(Calendar.YEAR), month-1, day, hours, minutes, seconds);
int sec = (int)Math.floor(cal.getTimeInMillis()/1000);
int nsec = milliseconds * 1000000;
String tag = m.group(7);
// get the numerical tag value
int tagValue = -1;
Set<Entry<Integer, String>> tagSet = mTagMap.entrySet();
for (Entry<Integer, String> entry : tagSet) {
if (tag.equals(entry.getValue())) {
tagValue = entry.getKey();
break;
}
}
if (tagValue == -1) {
return null;
}
int pid = Integer.parseInt(m.group(8));
Object data = parseTextData(m.group(9), tagValue);
if (data == null) {
return null;
}
// now we can allocate and return the EventContainer
EventContainer event = null;
if (tagValue == GcEventContainer.GC_EVENT_TAG) {
event = new GcEventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
} else {
event = new EventContainer(tagValue, pid, -1 /* tid */, sec, nsec, data);
}
return event;
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
public Map<Integer, String> getTagMap() {
return mTagMap;
}
public Map<Integer, EventValueDescription[]> getEventInfoMap() {
return mValueDescriptionMap;
}
/**
* Recursively convert binary log data to printable form.
*
* This needs to be recursive because you can have lists of lists.
*
* If we run out of room, we stop processing immediately. It's important
* for us to check for space on every output element to avoid producing
* garbled output.
*
* Returns the amount read on success, -1 on failure.
*/
private static int parseBinaryEvent(byte[] eventData, int dataOffset, ArrayList<Object> list) {
if (eventData.length - dataOffset < 1)
return -1;
int offset = dataOffset;
int type = eventData[offset++];
//fprintf(stderr, "--- type=%d (rem len=%d)\n", type, eventDataLen);
switch (type) {
case EVENT_TYPE_INT: { /* 32-bit signed int */
int ival;
if (eventData.length - offset < 4)
return -1;
ival = ArrayHelper.swap32bitFromArray(eventData, offset);
offset += 4;
list.add(new Integer(ival));
}
break;
case EVENT_TYPE_LONG: { /* 64-bit signed long */
long lval;
if (eventData.length - offset < 8)
return -1;
lval = ArrayHelper.swap64bitFromArray(eventData, offset);
offset += 8;
list.add(new Long(lval));
}
break;
case EVENT_TYPE_STRING: { /* UTF-8 chars, not NULL-terminated */
int strLen;
if (eventData.length - offset < 4)
return -1;
strLen = ArrayHelper.swap32bitFromArray(eventData, offset);
offset += 4;
if (eventData.length - offset < strLen)
return -1;
// get the string
try {
String str = new String(eventData, offset, strLen, "UTF-8"); //$NON-NLS-1$
list.add(str);
} catch (UnsupportedEncodingException e) {
}
offset += strLen;
break;
}
case EVENT_TYPE_LIST: { /* N items, all different types */
if (eventData.length - offset < 1)
return -1;
int count = eventData[offset++];
// make a new temp list
ArrayList<Object> subList = new ArrayList<Object>();
for (int i = 0; i < count; i++) {
int result = parseBinaryEvent(eventData, offset, subList);
if (result == -1) {
return result;
}
offset += result;
}
list.add(subList.toArray());
}
break;
default:
Log.e("EventLogParser", //$NON-NLS-1$
String.format("Unknown binary event type %1$d", type)); //$NON-NLS-1$
return -1;
}
return offset - dataOffset;
}
private Object parseTextData(String data, int tagValue) {
// first, get the description of what we're supposed to parse
EventValueDescription[] desc = mValueDescriptionMap.get(tagValue);
if (desc == null) {
// TODO parse and create string values.
return null;
}
if (desc.length == 1) {
return getObjectFromString(data, desc[0].getEventValueType());
} else if (data.startsWith("[") && data.endsWith("]")) {
data = data.substring(1, data.length() - 1);
// get each individual values as String
String[] values = data.split(",");
if (tagValue == GcEventContainer.GC_EVENT_TAG) {
// special case for the GC event!
Object[] objects = new Object[2];
objects[0] = getObjectFromString(values[0], EventValueType.LONG);
objects[1] = getObjectFromString(values[1], EventValueType.LONG);
return objects;
} else {
// must be the same number as the number of descriptors.
if (values.length != desc.length) {
return null;
}
Object[] objects = new Object[values.length];
for (int i = 0 ; i < desc.length ; i++) {
Object obj = getObjectFromString(values[i], desc[i].getEventValueType());
if (obj == null) {
return null;
}
objects[i] = obj;
}
return objects;
}
}
return null;
}
private Object getObjectFromString(String value, EventValueType type) {
try {
switch (type) {
case INT:
return Integer.valueOf(value);
case LONG:
return Long.valueOf(value);
case STRING:
return value;
}
} catch (NumberFormatException e) {
// do nothing, we'll return null.
}
return null;
}
/**
* Recreates the event-log-tags at the specified file path.
* @param filePath the file path to write the file.
* @throws IOException
*/
public void saveTags(String filePath) throws IOException {
File destFile = new File(filePath);
destFile.createNewFile();
FileOutputStream fos = null;
try {
fos = new FileOutputStream(destFile);
for (Integer key : mTagMap.keySet()) {
// get the tag name
String tagName = mTagMap.get(key);
// get the value descriptions
EventValueDescription[] descriptors = mValueDescriptionMap.get(key);
String line = null;
if (descriptors != null) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("%1$d %2$s", key, tagName)); //$NON-NLS-1$
boolean first = true;
for (EventValueDescription evd : descriptors) {
if (first) {
sb.append(" ("); //$NON-NLS-1$
first = false;
} else {
sb.append(",("); //$NON-NLS-1$
}
sb.append(evd.getName());
sb.append("|"); //$NON-NLS-1$
sb.append(evd.getEventValueType().getValue());
sb.append("|"); //$NON-NLS-1$
sb.append(evd.getValueType().getValue());
sb.append("|)"); //$NON-NLS-1$
}
sb.append("\n"); //$NON-NLS-1$
line = sb.toString();
} else {
line = String.format("%1$d %2$s\n", key, tagName); //$NON-NLS-1$
}
byte[] buffer = line.getBytes();
fos.write(buffer);
}
} finally {
if (fos != null) {
fos.close();
}
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2008 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.log;
import com.android.ddmlib.log.EventContainer.EventValueType;
/**
* Describes an {@link EventContainer} value.
* <p/>
* This is a stand-alone object, not linked to a particular Event. It describes the value, by
* name, type ({@link EventValueType}), and (if needed) value unit ({@link ValueType}).
* <p/>
* The index of the value is not contained within this class, and is instead dependent on the
* index of this particular object in the array of {@link EventValueDescription} returned by
* {@link EventLogParser#getEventInfoMap()} when queried for a particular event tag.
*
*/
public final class EventValueDescription {
/**
* Represents the type of a numerical value. This is used to display values of vastly different
* type/range in graphs.
*/
public static enum ValueType {
NOT_APPLICABLE(0),
OBJECTS(1),
BYTES(2),
MILLISECONDS(3),
ALLOCATIONS(4),
ID(5),
PERCENT(6);
private int mValue;
/**
* Checks that the {@link EventValueType} is compatible with the {@link ValueType}.
* @param type the {@link EventValueType} to check.
* @throws InvalidValueTypeException if the types are not compatible.
*/
public void checkType(EventValueType type) throws InvalidValueTypeException {
if ((type != EventValueType.INT && type != EventValueType.LONG)
&& this != NOT_APPLICABLE) {
throw new InvalidValueTypeException(
String.format("%1$s doesn't support type %2$s", type, this));
}
}
/**
* Returns a {@link ValueType} from an integer value, or <code>null</code> if no match
* were found.
* @param value the integer value.
*/
public static ValueType getValueType(int value) {
for (ValueType type : values()) {
if (type.mValue == value) {
return type;
}
}
return null;
}
/**
* Returns the integer value of the enum.
*/
public int getValue() {
return mValue;
}
@Override
public String toString() {
return super.toString().toLowerCase();
}
private ValueType(int value) {
mValue = value;
}
}
private String mName;
private EventValueType mEventValueType;
private ValueType mValueType;
/**
* Builds a {@link EventValueDescription} with a name and a type.
* <p/>
* If the type is {@link EventValueType#INT} or {@link EventValueType#LONG}, the
* {@link #mValueType} is set to {@link ValueType#BYTES} by default. It set to
* {@link ValueType#NOT_APPLICABLE} for all other {@link EventValueType} values.
* @param name
* @param type
*/
EventValueDescription(String name, EventValueType type) {
mName = name;
mEventValueType = type;
if (mEventValueType == EventValueType.INT || mEventValueType == EventValueType.LONG) {
mValueType = ValueType.BYTES;
} else {
mValueType = ValueType.NOT_APPLICABLE;
}
}
/**
* Builds a {@link EventValueDescription} with a name and a type, and a {@link ValueType}.
* <p/>
* @param name
* @param type
* @param valueType
* @throws InvalidValueTypeException if type and valuetype are not compatible.
*
*/
EventValueDescription(String name, EventValueType type, ValueType valueType)
throws InvalidValueTypeException {
mName = name;
mEventValueType = type;
mValueType = valueType;
mValueType.checkType(mEventValueType);
}
/**
* @return the Name.
*/
public String getName() {
return mName;
}
/**
* @return the {@link EventValueType}.
*/
public EventValueType getEventValueType() {
return mEventValueType;
}
/**
* @return the {@link ValueType}.
*/
public ValueType getValueType() {
return mValueType;
}
@Override
public String toString() {
if (mValueType != ValueType.NOT_APPLICABLE) {
return String.format("%1$s (%2$s, %3$s)", mName, mEventValueType.toString(),
mValueType.toString());
}
return String.format("%1$s (%2$s)", mName, mEventValueType.toString());
}
/**
* Checks if the value is of the proper type for this receiver.
* @param value the value to check.
* @return true if the value is of the proper type for this receiver.
*/
public boolean checkForType(Object value) {
switch (mEventValueType) {
case INT:
return value instanceof Integer;
case LONG:
return value instanceof Long;
case STRING:
return value instanceof String;
case LIST:
return value instanceof Object[];
}
return false;
}
/**
* Returns an object of a valid type (based on the value returned by
* {@link #getEventValueType()}) from a String value.
* <p/>
* IMPORTANT {@link EventValueType#LIST} and {@link EventValueType#TREE} are not
* supported.
* @param value the value of the object expressed as a string.
* @return an object or null if the conversion could not be done.
*/
public Object getObjectFromString(String value) {
switch (mEventValueType) {
case INT:
try {
return Integer.valueOf(value);
} catch (NumberFormatException e) {
return null;
}
case LONG:
try {
return Long.valueOf(value);
} catch (NumberFormatException e) {
return null;
}
case STRING:
return value;
}
return null;
}
}

View File

@@ -0,0 +1,347 @@
/*
* Copyright (C) 2008 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.log;
import com.android.ddmlib.log.EventValueDescription.ValueType;
import com.android.ddmlib.log.LogReceiver.LogEntry;
/**
* Custom Event Container for the Gc event since this event doesn't simply output data in
* int or long format, but encodes several values on 4 longs.
* <p/>
* The array of {@link EventValueDescription}s parsed from the "event-log-tags" file must
* be ignored, and instead, the array returned from {@link #getValueDescriptions()} must be used.
*/
final class GcEventContainer extends EventContainer {
public final static int GC_EVENT_TAG = 20001;
private String processId;
private long gcTime;
private long bytesFreed;
private long objectsFreed;
private long actualSize;
private long allowedSize;
private long softLimit;
private long objectsAllocated;
private long bytesAllocated;
private long zActualSize;
private long zAllowedSize;
private long zObjectsAllocated;
private long zBytesAllocated;
private long dlmallocFootprint;
private long mallinfoTotalAllocatedSpace;
private long externalLimit;
private long externalBytesAllocated;
GcEventContainer(LogEntry entry, int tag, Object data) {
super(entry, tag, data);
init(data);
}
GcEventContainer(int tag, int pid, int tid, int sec, int nsec, Object data) {
super(tag, pid, tid, sec, nsec, data);
init(data);
}
/**
* @param data
*/
private void init(Object data) {
if (data instanceof Object[]) {
Object[] values = (Object[])data;
for (int i = 0; i < values.length; i++) {
if (values[i] instanceof Long) {
parseDvmHeapInfo((Long)values[i], i);
}
}
}
}
@Override
public EventValueType getType() {
return EventValueType.LIST;
}
@Override
public boolean testValue(int index, Object value, CompareMethod compareMethod)
throws InvalidTypeException {
// do a quick easy check on the type.
if (index == 0) {
if ((value instanceof String) == false) {
throw new InvalidTypeException();
}
} else if ((value instanceof Long) == false) {
throw new InvalidTypeException();
}
switch (compareMethod) {
case EQUAL_TO:
if (index == 0) {
return processId.equals(value);
} else {
return getValueAsLong(index) == ((Long)value).longValue();
}
case LESSER_THAN:
return getValueAsLong(index) <= ((Long)value).longValue();
case LESSER_THAN_STRICT:
return getValueAsLong(index) < ((Long)value).longValue();
case GREATER_THAN:
return getValueAsLong(index) >= ((Long)value).longValue();
case GREATER_THAN_STRICT:
return getValueAsLong(index) > ((Long)value).longValue();
case BIT_CHECK:
return (getValueAsLong(index) & ((Long)value).longValue()) != 0;
}
throw new ArrayIndexOutOfBoundsException();
}
@Override
public Object getValue(int valueIndex) {
if (valueIndex == 0) {
return processId;
}
try {
return new Long(getValueAsLong(valueIndex));
} catch (InvalidTypeException e) {
// this would only happened if valueIndex was 0, which we test above.
}
return null;
}
@Override
public double getValueAsDouble(int valueIndex) throws InvalidTypeException {
return (double)getValueAsLong(valueIndex);
}
@Override
public String getValueAsString(int valueIndex) {
switch (valueIndex) {
case 0:
return processId;
default:
try {
return Long.toString(getValueAsLong(valueIndex));
} catch (InvalidTypeException e) {
// we shouldn't stop there since we test, in this method first.
}
}
throw new ArrayIndexOutOfBoundsException();
}
/**
* Returns a custom array of {@link EventValueDescription} since the actual content of this
* event (list of (long, long) does not match the values encoded into those longs.
*/
static EventValueDescription[] getValueDescriptions() {
try {
return new EventValueDescription[] {
new EventValueDescription("Process Name", EventValueType.STRING),
new EventValueDescription("GC Time", EventValueType.LONG,
ValueType.MILLISECONDS),
new EventValueDescription("Freed Objects", EventValueType.LONG,
ValueType.OBJECTS),
new EventValueDescription("Freed Bytes", EventValueType.LONG, ValueType.BYTES),
new EventValueDescription("Soft Limit", EventValueType.LONG, ValueType.BYTES),
new EventValueDescription("Actual Size (aggregate)", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("Allowed Size (aggregate)", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("Allocated Objects (aggregate)",
EventValueType.LONG, ValueType.OBJECTS),
new EventValueDescription("Allocated Bytes (aggregate)", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("Actual Size", EventValueType.LONG, ValueType.BYTES),
new EventValueDescription("Allowed Size", EventValueType.LONG, ValueType.BYTES),
new EventValueDescription("Allocated Objects", EventValueType.LONG,
ValueType.OBJECTS),
new EventValueDescription("Allocated Bytes", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("Actual Size (zygote)", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("Allowed Size (zygote)", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("Allocated Objects (zygote)", EventValueType.LONG,
ValueType.OBJECTS),
new EventValueDescription("Allocated Bytes (zygote)", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("External Allocation Limit", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("External Bytes Allocated", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("dlmalloc Footprint", EventValueType.LONG,
ValueType.BYTES),
new EventValueDescription("Malloc Info: Total Allocated Space",
EventValueType.LONG, ValueType.BYTES),
};
} catch (InvalidValueTypeException e) {
// this shouldn't happen since we control manual the EventValueType and the ValueType
// values. For development purpose, we assert if this happens.
assert false;
}
// this shouldn't happen, but the compiler complains otherwise.
return null;
}
private void parseDvmHeapInfo(long data, int index) {
switch (index) {
case 0:
// [63 ] Must be zero
// [62-24] ASCII process identifier
// [23-12] GC time in ms
// [11- 0] Bytes freed
gcTime = float12ToInt((int)((data >> 12) & 0xFFFL));
bytesFreed = float12ToInt((int)(data & 0xFFFL));
// convert the long into an array, in the proper order so that we can convert the
// first 5 char into a string.
byte[] dataArray = new byte[8];
put64bitsToArray(data, dataArray, 0);
// get the name from the string
processId = new String(dataArray, 0, 5);
break;
case 1:
// [63-62] 10
// [61-60] Reserved; must be zero
// [59-48] Objects freed
// [47-36] Actual size (current footprint)
// [35-24] Allowed size (current hard max)
// [23-12] Objects allocated
// [11- 0] Bytes allocated
objectsFreed = float12ToInt((int)((data >> 48) & 0xFFFL));
actualSize = float12ToInt((int)((data >> 36) & 0xFFFL));
allowedSize = float12ToInt((int)((data >> 24) & 0xFFFL));
objectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL));
bytesAllocated = float12ToInt((int)(data & 0xFFFL));
break;
case 2:
// [63-62] 11
// [61-60] Reserved; must be zero
// [59-48] Soft limit (current soft max)
// [47-36] Actual size (current footprint)
// [35-24] Allowed size (current hard max)
// [23-12] Objects allocated
// [11- 0] Bytes allocated
softLimit = float12ToInt((int)((data >> 48) & 0xFFFL));
zActualSize = float12ToInt((int)((data >> 36) & 0xFFFL));
zAllowedSize = float12ToInt((int)((data >> 24) & 0xFFFL));
zObjectsAllocated = float12ToInt((int)((data >> 12) & 0xFFFL));
zBytesAllocated = float12ToInt((int)(data & 0xFFFL));
break;
case 3:
// [63-48] Reserved; must be zero
// [47-36] dlmallocFootprint
// [35-24] mallinfo: total allocated space
// [23-12] External byte limit
// [11- 0] External bytes allocated
dlmallocFootprint = float12ToInt((int)((data >> 36) & 0xFFFL));
mallinfoTotalAllocatedSpace = float12ToInt((int)((data >> 24) & 0xFFFL));
externalLimit = float12ToInt((int)((data >> 12) & 0xFFFL));
externalBytesAllocated = float12ToInt((int)(data & 0xFFFL));
break;
default:
break;
}
}
/**
* Converts a 12 bit float representation into an unsigned int (returned as a long)
* @param f12
*/
private static long float12ToInt(int f12) {
return (f12 & 0x1FF) << ((f12 >>> 9) * 4);
}
/**
* puts an unsigned value in an array.
* @param value The value to put.
* @param dest the destination array
* @param offset the offset in the array where to put the value.
* Array length must be at least offset + 8
*/
private static void put64bitsToArray(long value, byte[] dest, int offset) {
dest[offset + 7] = (byte)(value & 0x00000000000000FFL);
dest[offset + 6] = (byte)((value & 0x000000000000FF00L) >> 8);
dest[offset + 5] = (byte)((value & 0x0000000000FF0000L) >> 16);
dest[offset + 4] = (byte)((value & 0x00000000FF000000L) >> 24);
dest[offset + 3] = (byte)((value & 0x000000FF00000000L) >> 32);
dest[offset + 2] = (byte)((value & 0x0000FF0000000000L) >> 40);
dest[offset + 1] = (byte)((value & 0x00FF000000000000L) >> 48);
dest[offset + 0] = (byte)((value & 0xFF00000000000000L) >> 56);
}
/**
* Returns the long value of the <code>valueIndex</code>-th value.
* @param valueIndex the index of the value.
* @throws InvalidTypeException if index is 0 as it is a string value.
*/
private final long getValueAsLong(int valueIndex) throws InvalidTypeException {
switch (valueIndex) {
case 0:
throw new InvalidTypeException();
case 1:
return gcTime;
case 2:
return objectsFreed;
case 3:
return bytesFreed;
case 4:
return softLimit;
case 5:
return actualSize;
case 6:
return allowedSize;
case 7:
return objectsAllocated;
case 8:
return bytesAllocated;
case 9:
return actualSize - zActualSize;
case 10:
return allowedSize - zAllowedSize;
case 11:
return objectsAllocated - zObjectsAllocated;
case 12:
return bytesAllocated - zBytesAllocated;
case 13:
return zActualSize;
case 14:
return zAllowedSize;
case 15:
return zObjectsAllocated;
case 16:
return zBytesAllocated;
case 17:
return externalLimit;
case 18:
return externalBytesAllocated;
case 19:
return dlmallocFootprint;
case 20:
return mallinfoTotalAllocatedSpace;
}
throw new ArrayIndexOutOfBoundsException();
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2008 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.log;
import java.io.Serializable;
/**
* Exception thrown when accessing an {@link EventContainer} value with the wrong type.
*/
public final class InvalidTypeException extends Exception {
/**
* Needed by {@link Serializable}.
*/
private static final long serialVersionUID = 1L;
/**
* Constructs a new exception with the default detail message.
* @see java.lang.Exception
*/
public InvalidTypeException() {
super("Invalid Type");
}
/**
* Constructs a new exception with the specified detail message.
* @param message the detail message. The detail message is saved for later retrieval
* by the {@link Throwable#getMessage()} method.
* @see java.lang.Exception
*/
public InvalidTypeException(String message) {
super(message);
}
/**
* Constructs a new exception with the specified cause and a detail message of
* <code>(cause==null ? null : cause.toString())</code> (which typically contains
* the class and detail message of cause).
* @param cause the cause (which is saved for later retrieval by the
* {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @see java.lang.Exception
*/
public InvalidTypeException(Throwable cause) {
super(cause);
}
/**
* Constructs a new exception with the specified detail message and cause.
* @param message the detail message. The detail message is saved for later retrieval
* by the {@link Throwable#getMessage()} method.
* @param cause the cause (which is saved for later retrieval by the
* {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @see java.lang.Exception
*/
public InvalidTypeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2008 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.log;
import com.android.ddmlib.log.EventContainer.EventValueType;
import com.android.ddmlib.log.EventValueDescription.ValueType;
import java.io.Serializable;
/**
* Exception thrown when associating an {@link EventValueType} with an incompatible
* {@link ValueType}.
*/
public final class InvalidValueTypeException extends Exception {
/**
* Needed by {@link Serializable}.
*/
private static final long serialVersionUID = 1L;
/**
* Constructs a new exception with the default detail message.
* @see java.lang.Exception
*/
public InvalidValueTypeException() {
super("Invalid Type");
}
/**
* Constructs a new exception with the specified detail message.
* @param message the detail message. The detail message is saved for later retrieval
* by the {@link Throwable#getMessage()} method.
* @see java.lang.Exception
*/
public InvalidValueTypeException(String message) {
super(message);
}
/**
* Constructs a new exception with the specified cause and a detail message of
* <code>(cause==null ? null : cause.toString())</code> (which typically contains
* the class and detail message of cause).
* @param cause the cause (which is saved for later retrieval by the
* {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @see java.lang.Exception
*/
public InvalidValueTypeException(Throwable cause) {
super(cause);
}
/**
* Constructs a new exception with the specified detail message and cause.
* @param message the detail message. The detail message is saved for later retrieval
* by the {@link Throwable#getMessage()} method.
* @param cause the cause (which is saved for later retrieval by the
* {@link Throwable#getCause()} method). (A <code>null</code> value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @see java.lang.Exception
*/
public InvalidValueTypeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright (C) 2008 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.log;
import com.android.ddmlib.utils.ArrayHelper;
import java.security.InvalidParameterException;
/**
* Receiver able to provide low level parsing for device-side log services.
*/
public final class LogReceiver {
private final static int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry.
/**
* Represents a log entry and its raw data.
*/
public final static class LogEntry {
/*
* See //device/include/utils/logger.h
*/
/** 16bit unsigned: length of the payload. */
public int len; /* This is normally followed by a 16 bit padding */
/** pid of the process that generated this {@link LogEntry} */
public int pid;
/** tid of the process that generated this {@link LogEntry} */
public int tid;
/** Seconds since epoch. */
public int sec;
/** nanoseconds. */
public int nsec;
/** The entry's raw data. */
public byte[] data;
};
/**
* Classes which implement this interface provide a method that deals
* with {@link LogEntry} objects coming from log service through a {@link LogReceiver}.
* <p/>This interface provides two methods.
* <ul>
* <li>{@link #newEntry(com.android.ddmlib.log.LogReceiver.LogEntry)} provides a
* first level of parsing, extracting {@link LogEntry} objects out of the log service output.</li>
* <li>{@link #newData(byte[], int, int)} provides a way to receive the raw information
* coming directly from the log service.</li>
* </ul>
*/
public interface ILogListener {
/**
* Sent when a new {@link LogEntry} has been parsed by the {@link LogReceiver}.
* @param entry the new log entry.
*/
public void newEntry(LogEntry entry);
/**
* Sent when new raw data is coming from the log service.
* @param data the raw data buffer.
* @param offset the offset into the buffer signaling the beginning of the new data.
* @param length the length of the new data.
*/
public void newData(byte[] data, int offset, int length);
}
/** Current {@link LogEntry} being read, before sending it to the listener. */
private LogEntry mCurrentEntry;
/** Temp buffer to store partial entry headers. */
private byte[] mEntryHeaderBuffer = new byte[ENTRY_HEADER_SIZE];
/** Offset in the partial header buffer */
private int mEntryHeaderOffset = 0;
/** Offset in the partial entry data */
private int mEntryDataOffset = 0;
/** Listener waiting for receive fully read {@link LogEntry} objects */
private ILogListener mListener;
private boolean mIsCancelled = false;
/**
* Creates a {@link LogReceiver} with an {@link ILogListener}.
* <p/>
* The {@link ILogListener} will receive new log entries as they are parsed, in the form
* of {@link LogEntry} objects.
* @param listener the listener to receive new log entries.
*/
public LogReceiver(ILogListener listener) {
mListener = listener;
}
/**
* Parses new data coming from the log service.
* @param data the data buffer
* @param offset the offset into the buffer signaling the beginning of the new data.
* @param length the length of the new data.
*/
public void parseNewData(byte[] data, int offset, int length) {
// notify the listener of new raw data
if (mListener != null) {
mListener.newData(data, offset, length);
}
// loop while there is still data to be read and the receiver has not be cancelled.
while (length > 0 && mIsCancelled == false) {
// first check if we have no current entry.
if (mCurrentEntry == null) {
if (mEntryHeaderOffset + length < ENTRY_HEADER_SIZE) {
// if we don't have enough data to finish the header, save
// the data we have and return
System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, length);
mEntryHeaderOffset += length;
return;
} else {
// we have enough to fill the header, let's do it.
// did we store some part at the beginning of the header?
if (mEntryHeaderOffset != 0) {
// copy the rest of the entry header into the header buffer
int size = ENTRY_HEADER_SIZE - mEntryHeaderOffset;
System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset,
size);
// create the entry from the header buffer
mCurrentEntry = createEntry(mEntryHeaderBuffer, 0);
// since we used the whole entry header buffer, we reset the offset
mEntryHeaderOffset = 0;
// adjust current offset and remaining length to the beginning
// of the entry data
offset += size;
length -= size;
} else {
// create the entry directly from the data array
mCurrentEntry = createEntry(data, offset);
// adjust current offset and remaining length to the beginning
// of the entry data
offset += ENTRY_HEADER_SIZE;
length -= ENTRY_HEADER_SIZE;
}
}
}
// at this point, we have an entry, and offset/length have been updated to skip
// the entry header.
// if we have enough data for this entry or more, we'll need to end this entry
if (length >= mCurrentEntry.len - mEntryDataOffset) {
// compute and save the size of the data that we have to read for this entry,
// based on how much we may already have read.
int dataSize = mCurrentEntry.len - mEntryDataOffset;
// we only read what we need, and put it in the entry buffer.
System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, dataSize);
// notify the listener of a new entry
if (mListener != null) {
mListener.newEntry(mCurrentEntry);
}
// reset some flags: we have read 0 data of the current entry.
// and we have no current entry being read.
mEntryDataOffset = 0;
mCurrentEntry = null;
// and update the data buffer info to the end of the current entry / start
// of the next one.
offset += dataSize;
length -= dataSize;
} else {
// we don't have enough data to fill this entry, so we store what we have
// in the entry itself.
System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, length);
// save the amount read for the data.
mEntryDataOffset += length;
return;
}
}
}
/**
* Returns whether this receiver is canceling the remote service.
*/
public boolean isCancelled() {
return mIsCancelled;
}
/**
* Cancels the current remote service.
*/
public void cancel() {
mIsCancelled = true;
}
/**
* Creates a {@link LogEntry} from the array of bytes. This expects the data buffer size
* to be at least <code>offset + {@link #ENTRY_HEADER_SIZE}</code>.
* @param data the data buffer the entry is read from.
* @param offset the offset of the first byte from the buffer representing the entry.
* @return a new {@link LogEntry} or <code>null</code> if some error happened.
*/
private LogEntry createEntry(byte[] data, int offset) {
if (data.length < offset + ENTRY_HEADER_SIZE) {
throw new InvalidParameterException(
"Buffer not big enough to hold full LoggerEntry header");
}
// create the new entry and fill it.
LogEntry entry = new LogEntry();
entry.len = ArrayHelper.swapU16bitFromArray(data, offset);
// we've read only 16 bits, but since there's also a 16 bit padding,
// we can skip right over both.
offset += 4;
entry.pid = ArrayHelper.swap32bitFromArray(data, offset);
offset += 4;
entry.tid = ArrayHelper.swap32bitFromArray(data, offset);
offset += 4;
entry.sec = ArrayHelper.swap32bitFromArray(data, offset);
offset += 4;
entry.nsec = ArrayHelper.swap32bitFromArray(data, offset);
offset += 4;
// allocate the data
entry.data = new byte[entry.len];
return entry;
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2008 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.testrunner;
/**
* Receives event notifications during instrumentation test runs.
* Patterned after {@link junit.runner.TestRunListener}.
*/
public interface ITestRunListener {
/**
* Types of test failures.
*/
enum TestFailure {
/** Test failed due to unanticipated uncaught exception. */
ERROR,
/** Test failed due to a false assertion. */
FAILURE
}
/**
* Reports the start of a test run.
*
* @param testCount total number of tests in test run
*/
public void testRunStarted(int testCount);
/**
* Reports end of test run.
*
* @param elapsedTime device reported elapsed time, in milliseconds
*/
public void testRunEnded(long elapsedTime);
/**
* Reports test run stopped before completion.
*
* @param elapsedTime device reported elapsed time, in milliseconds
*/
public void testRunStopped(long elapsedTime);
/**
* Reports the start of an individual test case.
*
* @param test identifies the test
*/
public void testStarted(TestIdentifier test);
/**
* Reports the execution end of an individual test case.
* If {@link #testFailed} was not invoked, this test passed.
*
* @param test identifies the test
*/
public void testEnded(TestIdentifier test);
/**
* Reports the failure of a individual test case.
* Will be called between testStarted and testEnded.
*
* @param status failure type
* @param test identifies the test
* @param trace stack trace of failure
*/
public void testFailed(TestFailure status, TestIdentifier test, String trace);
/**
* Reports test run failed to execute due to a fatal error.
*/
public void testRunFailed(String errorMessage);
}

View File

@@ -0,0 +1,369 @@
/*
* Copyright (C) 2008 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.testrunner;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.Log;
import com.android.ddmlib.MultiLineReceiver;
/**
* Parses the 'raw output mode' results of an instrumentation test run from shell and informs a
* ITestRunListener of the results.
*
* <p>Expects the following output:
*
* <p>If fatal error occurred when attempted to run the tests:
* <pre> INSTRUMENTATION_FAILED: </pre>
*
* <p>Otherwise, expect a series of test results, each one containing a set of status key/value
* pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test
* run, expects that the elapsed test time in seconds will be displayed
*
* <p>For example:
* <pre>
* INSTRUMENTATION_STATUS_CODE: 1
* INSTRUMENTATION_STATUS: class=com.foo.FooTest
* INSTRUMENTATION_STATUS: test=testFoo
* INSTRUMENTATION_STATUS: numtests=2
* INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312
* com.foo.X
* INSTRUMENTATION_STATUS_CODE: -2
* ...
*
* Time: X
* </pre>
* <p>Note that the "value" portion of the key-value pair may wrap over several text lines
*/
public class InstrumentationResultParser extends MultiLineReceiver {
/** Relevant test status keys. */
private static class StatusKeys {
private static final String TEST = "test";
private static final String CLASS = "class";
private static final String STACK = "stack";
private static final String NUMTESTS = "numtests";
}
/** Test result status codes. */
private static class StatusCodes {
private static final int FAILURE = -2;
private static final int START = 1;
private static final int ERROR = -1;
private static final int OK = 0;
}
/** Prefixes used to identify output. */
private static class Prefixes {
private static final String STATUS = "INSTRUMENTATION_STATUS: ";
private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: ";
private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: ";
private static final String TIME_REPORT = "Time: ";
}
private final ITestRunListener mTestListener;
/**
* Test result data
*/
private static class TestResult {
private Integer mCode = null;
private String mTestName = null;
private String mTestClass = null;
private String mStackTrace = null;
private Integer mNumTests = null;
/** Returns true if all expected values have been parsed */
boolean isComplete() {
return mCode != null && mTestName != null && mTestClass != null;
}
}
/** Stores the status values for the test result currently being parsed */
private TestResult mCurrentTestResult = null;
/** Stores the current "key" portion of the status key-value being parsed. */
private String mCurrentKey = null;
/** Stores the current "value" portion of the status key-value being parsed. */
private StringBuilder mCurrentValue = null;
/** True if start of test has already been reported to listener. */
private boolean mTestStartReported = false;
/** The elapsed time of the test run, in milliseconds. */
private long mTestTime = 0;
/** True if current test run has been canceled by user. */
private boolean mIsCancelled = false;
private static final String LOG_TAG = "InstrumentationResultParser";
/**
* Creates the InstrumentationResultParser.
*
* @param listener informed of test results as the tests are executing
*/
public InstrumentationResultParser(ITestRunListener listener) {
mTestListener = listener;
}
/**
* Processes the instrumentation test output from shell.
*
* @see MultiLineReceiver#processNewLines
*/
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
parse(line);
}
}
/**
* Parse an individual output line. Expects a line that is one of:
* <ul>
* <li>
* The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE),
* and thus there is a new key=value pair to parse, and the previous key-value pair is
* finished.
* </li>
* <li>
* A continuation of the previous status (the "value" portion of the key has wrapped
* to the next line).
* </li>
* <li> A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED) </li>
* <li> A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT) </li>
* </ul>
*
* @param line Text output line
*/
private void parse(String line) {
if (line.startsWith(Prefixes.STATUS_CODE)) {
// Previous status key-value has been collected. Store it.
submitCurrentKeyValue();
parseStatusCode(line);
} else if (line.startsWith(Prefixes.STATUS)) {
// Previous status key-value has been collected. Store it.
submitCurrentKeyValue();
parseKey(line, Prefixes.STATUS.length());
} else if (line.startsWith(Prefixes.STATUS_FAILED)) {
Log.e(LOG_TAG, "test run failed " + line);
mTestListener.testRunFailed(line);
} else if (line.startsWith(Prefixes.TIME_REPORT)) {
parseTime(line, Prefixes.TIME_REPORT.length());
} else {
if (mCurrentValue != null) {
// this is a value that has wrapped to next line.
mCurrentValue.append("\r\n");
mCurrentValue.append(line);
} else {
Log.w(LOG_TAG, "unrecognized line " + line);
}
}
}
/**
* Stores the currently parsed key-value pair into mCurrentTestInfo.
*/
private void submitCurrentKeyValue() {
if (mCurrentKey != null && mCurrentValue != null) {
TestResult testInfo = getCurrentTestInfo();
String statusValue = mCurrentValue.toString();
if (mCurrentKey.equals(StatusKeys.CLASS)) {
testInfo.mTestClass = statusValue.trim();
}
else if (mCurrentKey.equals(StatusKeys.TEST)) {
testInfo.mTestName = statusValue.trim();
}
else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) {
try {
testInfo.mNumTests = Integer.parseInt(statusValue);
}
catch (NumberFormatException e) {
Log.e(LOG_TAG, "Unexpected integer number of tests, received " + statusValue);
}
}
else if (mCurrentKey.equals(StatusKeys.STACK)) {
testInfo.mStackTrace = statusValue;
}
mCurrentKey = null;
mCurrentValue = null;
}
}
private TestResult getCurrentTestInfo() {
if (mCurrentTestResult == null) {
mCurrentTestResult = new TestResult();
}
return mCurrentTestResult;
}
private void clearCurrentTestInfo() {
mCurrentTestResult = null;
}
/**
* Parses the key from the current line.
* Expects format of "key=value".
*
* @param line full line of text to parse
* @param keyStartPos the starting position of the key in the given line
*/
private void parseKey(String line, int keyStartPos) {
int endKeyPos = line.indexOf('=', keyStartPos);
if (endKeyPos != -1) {
mCurrentKey = line.substring(keyStartPos, endKeyPos).trim();
parseValue(line, endKeyPos+1);
}
}
/**
* Parses the start of a key=value pair.
*
* @param line - full line of text to parse
* @param valueStartPos - the starting position of the value in the given line
*/
private void parseValue(String line, int valueStartPos) {
mCurrentValue = new StringBuilder();
mCurrentValue.append(line.substring(valueStartPos));
}
/**
* Parses out a status code result.
*/
private void parseStatusCode(String line) {
String value = line.substring(Prefixes.STATUS_CODE.length()).trim();
TestResult testInfo = getCurrentTestInfo();
try {
testInfo.mCode = Integer.parseInt(value);
}
catch (NumberFormatException e) {
Log.e(LOG_TAG, "Expected integer status code, received: " + value);
}
// this means we're done with current test result bundle
reportResult(testInfo);
clearCurrentTestInfo();
}
/**
* Returns true if test run canceled.
*
* @see IShellOutputReceiver#isCancelled()
*/
public boolean isCancelled() {
return mIsCancelled;
}
/**
* Requests cancellation of test run.
*/
public void cancel() {
mIsCancelled = true;
}
/**
* Reports a test result to the test run listener. Must be called when a individual test
* result has been fully parsed.
*
* @param statusMap key-value status pairs of test result
*/
private void reportResult(TestResult testInfo) {
if (!testInfo.isComplete()) {
Log.e(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString());
return;
}
reportTestRunStarted(testInfo);
TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName);
switch (testInfo.mCode) {
case StatusCodes.START:
mTestListener.testStarted(testId);
break;
case StatusCodes.FAILURE:
mTestListener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
getTrace(testInfo));
mTestListener.testEnded(testId);
break;
case StatusCodes.ERROR:
mTestListener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
getTrace(testInfo));
mTestListener.testEnded(testId);
break;
case StatusCodes.OK:
mTestListener.testEnded(testId);
break;
default:
Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode);
mTestListener.testEnded(testId);
break;
}
}
/**
* Reports the start of a test run, and the total test count, if it has not been previously
* reported.
*
* @param testInfo current test status values
*/
private void reportTestRunStarted(TestResult testInfo) {
// if start test run not reported yet
if (!mTestStartReported && testInfo.mNumTests != null) {
mTestListener.testRunStarted(testInfo.mNumTests);
mTestStartReported = true;
}
}
/**
* Returns the stack trace of the current failed test, from the provided testInfo.
*/
private String getTrace(TestResult testInfo) {
if (testInfo.mStackTrace != null) {
return testInfo.mStackTrace;
}
else {
Log.e(LOG_TAG, "Could not find stack trace for failed test ");
return new Throwable("Unknown failure").toString();
}
}
/**
* Parses out and store the elapsed time.
*/
private void parseTime(String line, int startPos) {
String timeString = line.substring(startPos);
try {
float timeSeconds = Float.parseFloat(timeString);
mTestTime = (long)(timeSeconds * 1000);
}
catch (NumberFormatException e) {
Log.e(LOG_TAG, "Unexpected time format " + timeString);
}
}
/**
* Called by parent when adb session is complete.
*/
@Override
public void done() {
super.done();
mTestListener.testRunEnded(mTestTime);
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright (C) 2008 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.testrunner;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import java.io.IOException;
/**
* Runs a Android test command remotely and reports results.
*/
public class RemoteAndroidTestRunner {
private static final char CLASS_SEPARATOR = ',';
private static final char METHOD_SEPARATOR = '#';
private static final char RUNNER_SEPARATOR = '/';
private String mClassArg;
private final String mPackageName;
private final String mRunnerName;
private String mExtraArgs;
private boolean mLogOnlyMode;
private IDevice mRemoteDevice;
private InstrumentationResultParser mParser;
private static final String LOG_TAG = "RemoteAndroidTest";
private static final String DEFAULT_RUNNER_NAME =
"android.test.InstrumentationTestRunner";
/**
* Creates a remote Android test runner.
*
* @param packageName the Android application package that contains the tests to run
* @param runnerName the instrumentation test runner to execute. If null, will use default
* runner
* @param remoteDevice the Android device to execute tests on
*/
public RemoteAndroidTestRunner(String packageName,
String runnerName,
IDevice remoteDevice) {
mPackageName = packageName;
mRunnerName = runnerName;
mRemoteDevice = remoteDevice;
mClassArg = null;
mExtraArgs = "";
mLogOnlyMode = false;
}
/**
* Alternate constructor. Uses default instrumentation runner.
*
* @param packageName the Android application package that contains the tests to run
* @param remoteDevice the Android device to execute tests on
*/
public RemoteAndroidTestRunner(String packageName,
IDevice remoteDevice) {
this(packageName, null, remoteDevice);
}
/**
* Returns the application package name.
*/
public String getPackageName() {
return mPackageName;
}
/**
* Returns the runnerName.
*/
public String getRunnerName() {
if (mRunnerName == null) {
return DEFAULT_RUNNER_NAME;
}
return mRunnerName;
}
/**
* Returns the complete instrumentation component path.
*/
private String getRunnerPath() {
return getPackageName() + RUNNER_SEPARATOR + getRunnerName();
}
/**
* Sets to run only tests in this class
* Must be called before 'run'.
*
* @param className fully qualified class name (eg x.y.z)
*/
public void setClassName(String className) {
mClassArg = className;
}
/**
* Sets to run only tests in the provided classes
* Must be called before 'run'.
* <p>
* If providing more than one class, requires a InstrumentationTestRunner that supports
* the multiple class argument syntax.
*
* @param classNames array of fully qualified class names (eg x.y.z)
*/
public void setClassNames(String[] classNames) {
StringBuilder classArgBuilder = new StringBuilder();
for (int i=0; i < classNames.length; i++) {
if (i != 0) {
classArgBuilder.append(CLASS_SEPARATOR);
}
classArgBuilder.append(classNames[i]);
}
mClassArg = classArgBuilder.toString();
}
/**
* Sets to run only specified test method
* Must be called before 'run'.
*
* @param className fully qualified class name (eg x.y.z)
* @param testName method name
*/
public void setMethodName(String className, String testName) {
mClassArg = className + METHOD_SEPARATOR + testName;
}
/**
* Sets extra arguments to include in instrumentation command.
* Must be called before 'run'.
*
* @param instrumentationArgs must not be null
*/
public void setExtraArgs(String instrumentationArgs) {
if (instrumentationArgs == null) {
throw new IllegalArgumentException("instrumentationArgs cannot be null");
}
mExtraArgs = instrumentationArgs;
}
/**
* Returns the extra instrumentation arguments.
*/
public String getExtraArgs() {
return mExtraArgs;
}
/**
* Sets this test run to log only mode - skips test execution.
*/
public void setLogOnly(boolean logOnly) {
mLogOnlyMode = logOnly;
}
/**
* Execute this test run.
*
* @param listener listens for test results
*/
public void run(ITestRunListener listener) {
final String runCaseCommandStr = "am instrument -w -r "
+ getClassCmd() + " " + getLogCmd() + " " + getExtraArgs() + " " + getRunnerPath();
Log.d(LOG_TAG, runCaseCommandStr);
mParser = new InstrumentationResultParser(listener);
try {
mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser);
} catch (IOException e) {
Log.e(LOG_TAG, e);
listener.testRunFailed(e.toString());
}
}
/**
* Requests cancellation of this test run.
*/
public void cancel() {
if (mParser != null) {
mParser.cancel();
}
}
/**
* Returns the test class argument.
*/
private String getClassArg() {
return mClassArg;
}
/**
* Returns the full instrumentation command which specifies the test classes to execute.
* Returns an empty string if no classes were specified.
*/
private String getClassCmd() {
String classArg = getClassArg();
if (classArg != null) {
return "-e class " + classArg;
}
return "";
}
/**
* Returns the full command to enable log only mode - if specified. Otherwise returns an
* empty string.
*/
private String getLogCmd() {
if (mLogOnlyMode) {
return "-e log true";
}
else {
return "";
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2008 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.testrunner;
/**
* Identifies a parsed instrumentation test
*/
public class TestIdentifier {
private final String mClassName;
private final String mTestName;
/**
* Creates a test identifier
*
* @param className fully qualified class name of the test. Cannot be null.
* @param testName name of the test. Cannot be null.
*/
public TestIdentifier(String className, String testName) {
if (className == null || testName == null) {
throw new IllegalArgumentException("className and testName must " +
"be non-null");
}
mClassName = className;
mTestName = testName;
}
/**
* Returns the fully qualified class name of the test
*/
public String getClassName() {
return mClassName;
}
/**
* Returns the name of the test
*/
public String getTestName() {
return mTestName;
}
/**
* Tests equality by comparing class and method name
*/
@Override
public boolean equals(Object other) {
if (!(other instanceof TestIdentifier)) {
return false;
}
TestIdentifier otherTest = (TestIdentifier)other;
return getClassName().equals(otherTest.getClassName()) &&
getTestName().equals(otherTest.getTestName());
}
/**
* Generates hashCode based on class and method name.
*/
@Override
public int hashCode() {
return getClassName().hashCode() * 31 + getTestName().hashCode();
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.utils;
/**
* Utility class providing array to int/long conversion for data received from devices through adb.
*/
public final class ArrayHelper {
/**
* Swaps an unsigned value around, and puts the result in an array that can be sent to a device.
* @param value The value to swap.
* @param dest the destination array
* @param offset the offset in the array where to put the swapped value.
* Array length must be at least offset + 4
*/
public static void swap32bitsToArray(int value, byte[] dest, int offset) {
dest[offset] = (byte)(value & 0x000000FF);
dest[offset + 1] = (byte)((value & 0x0000FF00) >> 8);
dest[offset + 2] = (byte)((value & 0x00FF0000) >> 16);
dest[offset + 3] = (byte)((value & 0xFF000000) >> 24);
}
/**
* Reads a signed 32 bit integer from an array coming from a device.
* @param value the array containing the int
* @param offset the offset in the array at which the int starts
* @return the integer read from the array
*/
public static int swap32bitFromArray(byte[] value, int offset) {
int v = 0;
v |= ((int)value[offset]) & 0x000000FF;
v |= (((int)value[offset + 1]) & 0x000000FF) << 8;
v |= (((int)value[offset + 2]) & 0x000000FF) << 16;
v |= (((int)value[offset + 3]) & 0x000000FF) << 24;
return v;
}
/**
* Reads an unsigned 16 bit integer from an array coming from a device,
* and returns it as an 'int'
* @param value the array containing the 16 bit int (2 byte).
* @param offset the offset in the array at which the int starts
* Array length must be at least offset + 2
* @return the integer read from the array.
*/
public static int swapU16bitFromArray(byte[] value, int offset) {
int v = 0;
v |= ((int)value[offset]) & 0x000000FF;
v |= (((int)value[offset + 1]) & 0x000000FF) << 8;
return v;
}
/**
* Reads a signed 64 bit integer from an array coming from a device.
* @param value the array containing the int
* @param offset the offset in the array at which the int starts
* Array length must be at least offset + 8
* @return the integer read from the array
*/
public static long swap64bitFromArray(byte[] value, int offset) {
long v = 0;
v |= ((long)value[offset]) & 0x00000000000000FFL;
v |= (((long)value[offset + 1]) & 0x00000000000000FFL) << 8;
v |= (((long)value[offset + 2]) & 0x00000000000000FFL) << 16;
v |= (((long)value[offset + 3]) & 0x00000000000000FFL) << 24;
v |= (((long)value[offset + 4]) & 0x00000000000000FFL) << 32;
v |= (((long)value[offset + 5]) & 0x00000000000000FFL) << 40;
v |= (((long)value[offset + 6]) & 0x00000000000000FFL) << 48;
v |= (((long)value[offset + 7]) & 0x00000000000000FFL) << 56;
return v;
}
}