diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:29:09 -0800 |
commit | 55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch) | |
tree | ecd18b995aea8eeeb8b3823266280d41245bf0f7 /ddms/libs/ddmlib | |
parent | 82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff) | |
download | sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'ddms/libs/ddmlib')
56 files changed, 15758 insertions, 0 deletions
diff --git a/ddms/libs/ddmlib/.classpath b/ddms/libs/ddmlib/.classpath new file mode 100644 index 0000000..fb50116 --- /dev/null +++ b/ddms/libs/ddmlib/.classpath @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/ddms/libs/ddmlib/.project b/ddms/libs/ddmlib/.project new file mode 100644 index 0000000..fea25c7 --- /dev/null +++ b/ddms/libs/ddmlib/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddmlib</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/ddms/libs/ddmlib/Android.mk b/ddms/libs/ddmlib/Android.mk new file mode 100644 index 0000000..a49bdd2 --- /dev/null +++ b/ddms/libs/ddmlib/Android.mk @@ -0,0 +1,4 @@ +# Copyright 2007 The Android Open Source Project +# +DDMLIB_LOCAL_DIR := $(call my-dir) +include $(DDMLIB_LOCAL_DIR)/src/Android.mk diff --git a/ddms/libs/ddmlib/src/Android.mk b/ddms/libs/ddmlib/src/Android.mk new file mode 100644 index 0000000..da07f97 --- /dev/null +++ b/ddms/libs/ddmlib/src/Android.mk @@ -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) + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java new file mode 100644 index 0000000..42022fe --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AdbHelper.java @@ -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); + } + + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java new file mode 100644 index 0000000..c6d4b50 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AllocationInfo.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java new file mode 100644 index 0000000..795bf88 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/AndroidDebugBridge.java @@ -0,0 +1,1050 @@ +/* + * 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.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.Thread.State; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A connection to the host-side android debug bridge (adb) + * <p/>This is the central point to communicate with any devices, emulators, or the applications + * running on them. + * <p/><b>{@link #init(boolean)} must be called before anything is done.</b> + */ +public final class AndroidDebugBridge { + + /* + * Minimum and maximum version of adb supported. This correspond to + * ADB_SERVER_VERSION found in //device/tools/adb/adb.h + */ + + private final static int ADB_VERSION_MICRO_MIN = 20; + private final static int ADB_VERSION_MICRO_MAX = -1; + + private final static Pattern sAdbVersion = Pattern.compile( + "^.*(\\d+)\\.(\\d+)\\.(\\d+)$"); //$NON-NLS-1$ + + private final static String ADB = "adb"; //$NON-NLS-1$ + private final static String DDMS = "ddms"; //$NON-NLS-1$ + + // Where to find the ADB bridge. + final static String ADB_HOST = "127.0.0.1"; //$NON-NLS-1$ + final static int ADB_PORT = 5037; + + static InetAddress sHostAddr; + static InetSocketAddress sSocketAddr; + + static { + // built-in local address/port for ADB. + try { + sHostAddr = InetAddress.getByName(ADB_HOST); + sSocketAddr = new InetSocketAddress(sHostAddr, ADB_PORT); + } catch (UnknownHostException e) { + + } + } + + private static AndroidDebugBridge sThis; + private static boolean sClientSupport; + + /** Full path to adb. */ + private String mAdbOsLocation = null; + + private boolean mVersionCheck; + + private boolean mStarted = false; + + private DeviceMonitor mDeviceMonitor; + + private final static ArrayList<IDebugBridgeChangeListener> sBridgeListeners = + new ArrayList<IDebugBridgeChangeListener>(); + private final static ArrayList<IDeviceChangeListener> sDeviceListeners = + new ArrayList<IDeviceChangeListener>(); + private final static ArrayList<IClientChangeListener> sClientListeners = + new ArrayList<IClientChangeListener>(); + + // lock object for synchronization + private static final Object sLock = sBridgeListeners; + + /** + * Classes which implement this interface provide a method that deals + * with {@link AndroidDebugBridge} changes. + */ + public interface IDebugBridgeChangeListener { + /** + * Sent when a new {@link AndroidDebugBridge} is connected. + * <p/> + * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + */ + public void bridgeChanged(AndroidDebugBridge bridge); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link Device} addition, deletion, and changes. + */ + public interface IDeviceChangeListener { + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceConnected(Device device); + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + * <p/> + * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceDisconnected(Device device); + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + * <p/> + * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask describing what changed. It can contain any of the following + * values: {@link Device#CHANGE_BUILD_INFO}, {@link Device#CHANGE_STATE}, + * {@link Device#CHANGE_CLIENT_LIST} + */ + public void deviceChanged(Device device, int changeMask); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link Client} changes. + */ + public interface IClientChangeListener { + /** + * Sent when an existing client information changed. + * <p/> + * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_INTEREST}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + */ + public void clientChanged(Client client, int changeMask); + } + + /** + * Initializes the <code>ddm</code> library. + * <p/>This must be called once <b>before</b> any call to + * {@link #createBridge(String, boolean)}. + * <p>The library can be initialized in 2 ways: + * <ul> + * <li>Mode 1: <var>clientSupport</var> == <code>true</code>.<br>The library monitors the + * devices and the applications running on them. It will connect to each application, as a + * debugger of sort, to be able to interact with them through JDWP packets.</li> + * <li>Mode 2: <var>clientSupport</var> == <code>false</code>.<br>The library only monitors + * devices. The applications are left untouched, letting other tools built on + * <code>ddmlib</code> to connect a debugger to them.</li> + * </ul> + * <p/><b>Only one tool can run in mode 1 at the same time.</b> + * <p/>Note that mode 1 does not prevent debugging of applications running on devices. Mode 1 + * lets debuggers connect to <code>ddmlib</code> which acts as a proxy between the debuggers and + * the applications to debug. See {@link Client#getDebuggerListenPort()}. + * <p/>The preferences of <code>ddmlib</code> should also be initialized with whatever default + * values were changed from the default values. + * <p/>When the application quits, {@link #terminate()} should be called. + * @param clientSupport Indicates whether the library should enable the monitoring and + * interaction with applications running on the devices. + * @see AndroidDebugBridge#createBridge(String, boolean) + * @see DdmPreferences + */ + public static void init(boolean clientSupport) { + sClientSupport = clientSupport; + + MonitorThread monitorThread = MonitorThread.createInstance(); + monitorThread.start(); + + HandleHello.register(monitorThread); + HandleAppName.register(monitorThread); + HandleTest.register(monitorThread); + HandleThread.register(monitorThread); + HandleHeap.register(monitorThread); + HandleWait.register(monitorThread); + } + + /** + * Terminates the ddm library. This must be called upon application termination. + */ + public static void terminate() { + // kill the monitoring services + if (sThis != null && sThis.mDeviceMonitor != null) { + sThis.mDeviceMonitor.stop(); + sThis.mDeviceMonitor = null; + } + + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.quit(); + } + } + + /** + * Returns whether the ddmlib is setup to support monitoring and interacting with + * {@link Client}s running on the {@link Device}s. + */ + static boolean getClientSupport() { + return sClientSupport; + } + + /** + * Creates a {@link AndroidDebugBridge} that is not linked to any particular executable. + * <p/>This bridge will expect adb to be running. It will not be able to start/stop/restart + * adb. + * <p/>If a bridge has already been started, it is directly returned with no changes (similar + * to calling {@link #getBridge()}). + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge() { + synchronized (sLock) { + if (sThis != null) { + return sThis; + } + + try { + sThis = new AndroidDebugBridge(); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + + /** + * Creates a new debug bridge from the location of the command line tool. + * <p/> + * Any existing server will be disconnected, unless the location is the same and + * <code>forceNewBridge</code> is set to false. + * @param osLocation the location of the command line tool 'adb' + * @param forceNewBridge force creation of a new bridge even if one with the same location + * already exists. + * @return a connected bridge. + */ + public static AndroidDebugBridge createBridge(String osLocation, boolean forceNewBridge) { + synchronized (sLock) { + if (sThis != null) { + if (sThis.mAdbOsLocation != null && sThis.mAdbOsLocation.equals(osLocation) && + forceNewBridge == false) { + return sThis; + } else { + // stop the current server + sThis.stop(); + } + } + + try { + sThis = new AndroidDebugBridge(osLocation); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + + return sThis; + } + } + + /** + * Returns the current debug bridge. Can be <code>null</code> if none were created. + */ + public static AndroidDebugBridge getBridge() { + return sThis; + } + + /** + * Disconnects the current debug bridge, and destroy the object. + * <p/> + * A new object will have to be created with {@link #createBridge(String, boolean)}. + */ + public static void disconnectBridge() { + synchronized (sLock) { + if (sThis != null) { + sThis.stop(); + sThis = null; + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners. + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is connected, by sending it one of the messages defined + * in the {@link IDebugBridgeChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + if (sBridgeListeners.contains(listener) == false) { + sBridgeListeners.add(listener); + if (sThis != null) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a new + * {@link AndroidDebugBridge} is started. + * @param listener The listener which should no longer be notified. + */ + public static void removeDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + sBridgeListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link Device} + * is connected, disconnected, or when its properties or its {@link Client} list changed, + * by sending it one of the messages defined in the {@link IDeviceChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + if (sDeviceListeners.contains(listener) == false) { + sDeviceListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link Device} is connected, disconnected, or when its properties or its {@link Client} + * list changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + sDeviceListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link Client} + * property changed, by sending it one of the messages defined in the + * {@link IClientChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + if (sClientListeners.contains(listener) == false) { + sClientListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link Client} property changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeClientChangeListener(IClientChangeListener listener) { + synchronized (sLock) { + sClientListeners.remove(listener); + } + } + + + /** + * Returns the devices. + * @see #hasInitialDeviceList() + */ + public Device[] getDevices() { + synchronized (sLock) { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getDevices(); + } + } + + return new Device[0]; + } + + /** + * Returns whether the bridge has acquired the initial list from adb after being created. + * <p/>Calling {@link #getDevices()} right after {@link #createBridge(String, boolean)} will + * generally result in an empty list. This is due to the internal asynchronous communication + * mechanism with <code>adb</code> that does not guarantee that the {@link Device} list has been + * built before the call to {@link #getDevices()}. + * <p/>The recommended way to get the list of {@link Device} objects is to create a + * {@link IDeviceChangeListener} object. + */ + public boolean hasInitialDeviceList() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.hasInitialDeviceList(); + } + + return false; + } + + /** + * Sets the client to accept debugger connection on the custom "Selected debug port". + * @param selectedClient the client. Can be null. + */ + public void setSelectedClient(Client selectedClient) { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (monitorThread != null) { + monitorThread.setSelectedClient(selectedClient); + } + } + + /** + * Returns whether the {@link AndroidDebugBridge} object is still connected to the adb daemon. + */ + public boolean isConnected() { + MonitorThread monitorThread = MonitorThread.getInstance(); + if (mDeviceMonitor != null && monitorThread != null) { + return mDeviceMonitor.isMonitoring() && monitorThread.getState() != State.TERMINATED; + } + return false; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to connect + * to the adb daemon. + */ + public int getConnectionAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getConnectionAttemptCount(); + } + return -1; + } + + /** + * Returns the number of times the {@link AndroidDebugBridge} object attempted to restart + * the adb daemon. + */ + public int getRestartAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getRestartAttemptCount(); + } + return -1; + } + + /** + * Creates a new bridge. + * @param osLocation the location of the command line tool + * @throws InvalidParameterException + */ + private AndroidDebugBridge(String osLocation) throws InvalidParameterException { + if (osLocation == null || osLocation.length() == 0) { + throw new InvalidParameterException(); + } + mAdbOsLocation = osLocation; + + checkAdbVersion(); + } + + /** + * Creates a new bridge not linked to any particular adb executable. + */ + private AndroidDebugBridge() { + } + + /** + * Queries adb for its version number and checks it against {@link #MIN_VERSION_NUMBER} and + * {@link #MAX_VERSION_NUMBER} + */ + private void checkAdbVersion() { + // default is bad check + mVersionCheck = false; + + if (mAdbOsLocation == null) { + return; + } + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "version"; //$NON-NLS-1$ + Log.d(DDMS, String.format("Checking '%1$s version'", mAdbOsLocation)); //$NON-NLS-1$ + Process process = Runtime.getRuntime().exec(command); + + ArrayList<String> errorOutput = new ArrayList<String>(); + ArrayList<String> stdOutput = new ArrayList<String>(); + int status = grabProcessOutput(process, errorOutput, stdOutput, + true /* waitForReaders */); + + if (status != 0) { + StringBuilder builder = new StringBuilder("'adb version' failed!"); //$NON-NLS-1$ + for (String error : errorOutput) { + builder.append('\n'); + builder.append(error); + } + Log.logAndDisplay(LogLevel.ERROR, "adb", builder.toString()); + } + + // check both stdout and stderr + boolean versionFound = false; + for (String line : stdOutput) { + versionFound = scanVersionLine(line); + if (versionFound) { + break; + } + } + if (!versionFound) { + for (String line : errorOutput) { + versionFound = scanVersionLine(line); + if (versionFound) { + break; + } + } + } + + if (!versionFound) { + // if we get here, we failed to parse the output. + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Failed to parse the output of 'adb version'"); //$NON-NLS-1$ + } + + } catch (IOException e) { + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Failed to get the adb version: " + e.getMessage()); //$NON-NLS-1$ + } catch (InterruptedException e) { + } finally { + + } + } + + /** + * Scans a line resulting from 'adb version' for a potential version number. + * <p/> + * If a version number is found, it checks the version number against what is expected + * by this version of ddms. + * <p/> + * Returns true when a version number has been found so that we can stop scanning, + * whether the version number is in the acceptable range or not. + * + * @param line The line to scan. + * @return True if a version number was found (whether it is acceptable or not). + */ + private boolean scanVersionLine(String line) { + if (line != null) { + Matcher matcher = sAdbVersion.matcher(line); + if (matcher.matches()) { + int majorVersion = Integer.parseInt(matcher.group(1)); + int minorVersion = Integer.parseInt(matcher.group(2)); + int microVersion = Integer.parseInt(matcher.group(3)); + + // check only the micro version for now. + if (microVersion < ADB_VERSION_MICRO_MIN) { + String message = String.format( + "Required minimum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$ + + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$ + majorVersion, minorVersion, ADB_VERSION_MICRO_MIN, + microVersion); + Log.logAndDisplay(LogLevel.ERROR, ADB, message); + } else if (ADB_VERSION_MICRO_MAX != -1 && + microVersion > ADB_VERSION_MICRO_MAX) { + String message = String.format( + "Required maximum version of adb: %1$d.%2$d.%3$d." //$NON-NLS-1$ + + "Current version is %1$d.%2$d.%4$d", //$NON-NLS-1$ + majorVersion, minorVersion, ADB_VERSION_MICRO_MAX, + microVersion); + Log.logAndDisplay(LogLevel.ERROR, ADB, message); + } else { + mVersionCheck = true; + } + + return true; + } + } + return false; + } + + /** + * Starts the debug bridge. + * @return true if success. + */ + boolean start() { + if (mAdbOsLocation != null && (mVersionCheck == false || startAdb() == false)) { + return false; + } + + mStarted = true; + + // now that the bridge is connected, we start the underlying services. + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + + return true; + } + + /** + * Kills the debug bridge. + * @return true if success + */ + boolean stop() { + // if we haven't started we return false; + if (mStarted == false) { + return false; + } + + // kill the monitoring services + mDeviceMonitor.stop(); + mDeviceMonitor = null; + + if (stopAdb() == false) { + return false; + } + + mStarted = false; + return true; + } + + /** + * Restarts adb, but not the services around it. + * @return true if success. + */ + public boolean restart() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot restart adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + if (mVersionCheck == false) { + Log.logAndDisplay(LogLevel.ERROR, ADB, + "Attempting to restart adb, but version check failed!"); //$NON-NLS-1$ + return false; + } + synchronized (this) { + stopAdb(); + + boolean restart = startAdb(); + + if (restart && mDeviceMonitor == null) { + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + } + + return restart; + } + } + + /** + * Notify the listener of a new {@link Device}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the new <code>Device</code>. + * @see #getLock() + */ + void deviceConnected(Device device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceConnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a disconnected {@link Device}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the disconnected <code>Device</code>. + * @see #getLock() + */ + void deviceDisconnected(Device device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceDisconnected(device); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link Device}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified <code>Device</code>. + * @see #getLock() + */ + void deviceChanged(Device device, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceChanged(device, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Notify the listener of a modified {@link Client}. + * <p/> + * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link Device} as well as + * {@link #getDevices()} which use internal locks. + * <p/> + * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link Device} which is also inside a synchronized block, should first synchronize on + * the {@link AndroidDebugBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified <code>Client</code>. + * @param changeMask the mask indicating what changed in the <code>Client</code> + * @see #getLock() + */ + void clientChanged(Client client, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IClientChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sClientListeners.toArray( + new IClientChangeListener[sClientListeners.size()]); + + } + + // Notify the listeners + for (IClientChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.clientChanged(client, changeMask); + } catch (Exception e) { + Log.e(DDMS, e); + } + } + } + + /** + * Returns the {@link DeviceMonitor} object. + */ + DeviceMonitor getDeviceMonitor() { + return mDeviceMonitor; + } + + /** + * Starts the adb host side server. + * @return true if success + */ + synchronized boolean startAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot start adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "start-server"; //$NON-NLS-1$ + Log.d(DDMS, + String.format("Launching '%1$s %2$s' to ensure ADB is running.", //$NON-NLS-1$ + mAdbOsLocation, command[1])); + proc = Runtime.getRuntime().exec(command); + + ArrayList<String> errorOutput = new ArrayList<String>(); + ArrayList<String> stdOutput = new ArrayList<String>(); + status = grabProcessOutput(proc, errorOutput, stdOutput, + false /* waitForReaders */); + + } catch (IOException ioe) { + Log.d(DDMS, "Unable to run 'adb': " + ioe.getMessage()); //$NON-NLS-1$ + // we'll return false; + } catch (InterruptedException ie) { + Log.d(DDMS, "Unable to run 'adb': " + ie.getMessage()); //$NON-NLS-1$ + // we'll return false; + } + + if (status != 0) { + Log.w(DDMS, + "'adb start-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(DDMS, "'adb start-server' succeeded"); //$NON-NLS-1$ + + return true; + } + + /** + * Stops the adb host side server. + * @return true if success + */ + private synchronized boolean stopAdb() { + if (mAdbOsLocation == null) { + Log.e(ADB, + "Cannot stop adb when AndroidDebugBridge is created without the location of adb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mAdbOsLocation; + command[1] = "kill-server"; //$NON-NLS-1$ + proc = Runtime.getRuntime().exec(command); + status = proc.waitFor(); + } + catch (IOException ioe) { + // we'll return false; + } + catch (InterruptedException ie) { + // we'll return false; + } + + if (status != 0) { + Log.w(DDMS, + "'adb kill-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(DDMS, "'adb kill-server' succeeded"); //$NON-NLS-1$ + return true; + } + + /** + * Get the stderr/stdout outputs of a process and return when the process is done. + * Both <b>must</b> be read or the process will block on windows. + * @param process The process to get the ouput from + * @param errorOutput The array to store the stderr output. cannot be null. + * @param stdOutput The array to store the stdout output. cannot be null. + * @param displayStdOut If true this will display stdout as well + * @param waitforReaders if true, this will wait for the reader threads. + * @return the process return code. + * @throws InterruptedException + */ + private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput, + final ArrayList<String> stdOutput, boolean waitforReaders) + throws InterruptedException { + assert errorOutput != null; + assert stdOutput != null; + // read the lines as they come. if null is returned, it's + // because the process finished + Thread t1 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + Log.e(ADB, line); + errorOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + Thread t2 = new Thread("") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + Log.d(ADB, line); + stdOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + t1.start(); + t2.start(); + + // it looks like on windows process#waitFor() can return + // before the thread have filled the arrays, so we wait for both threads and the + // process itself. + if (waitforReaders) { + try { + t1.join(); + } catch (InterruptedException e) { + } + try { + t2.join(); + } catch (InterruptedException e) { + } + } + + // get the return code from the process + return process.waitFor(); + } + + /** + * Returns the singleton lock used by this class to protect any access to the listener. + * <p/> + * This includes adding/removing listeners, but also notifying listeners of new bridges, + * devices, and clients. + */ + static Object getLock() { + return sLock; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java new file mode 100644 index 0000000..129b312 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/BadPacketException.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java new file mode 100644 index 0000000..441b024 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ChunkHandler.java @@ -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; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java new file mode 100644 index 0000000..866d578 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Client.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java new file mode 100644 index 0000000..2b46b6f --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ClientData.java @@ -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; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java new file mode 100644 index 0000000..c96d40d --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DdmPreferences.java @@ -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. + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java new file mode 100644 index 0000000..9392127 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DebugPortManager.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java new file mode 100644 index 0000000..f30509a --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java @@ -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); + } + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java new file mode 100644 index 0000000..0e7f0bb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Device.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java new file mode 100644 index 0000000..f9d0fa0 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/DeviceMonitor.java @@ -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; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java new file mode 100644 index 0000000..f3986ed --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/EmulatorConsole.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java new file mode 100644 index 0000000..b50cf79 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/FileListingService.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java new file mode 100644 index 0000000..9293379 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/GetPropReceiver.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java new file mode 100644 index 0000000..99bd4d0 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleAppName.java @@ -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); + } + } + } + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java new file mode 100644 index 0000000..adeedbb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleExit.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java new file mode 100644 index 0000000..5752b86 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHeap.java @@ -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() + ")"); + } + } + } + } + +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java new file mode 100644 index 0000000..5ba5aeb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleHello.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java new file mode 100644 index 0000000..ca26590 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleNativeHeap.java @@ -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(); + } + } + + +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java new file mode 100644 index 0000000..b9f3a74 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleTest.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java new file mode 100644 index 0000000..572eed2 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleThread.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java new file mode 100644 index 0000000..d27e636 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HandleWait.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java new file mode 100644 index 0000000..6a62e60 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/HeapSegment.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java new file mode 100755 index 0000000..5dbce92 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IDevice.java @@ -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><build></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); + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java new file mode 100644 index 0000000..fb671bb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IShellOutputReceiver.java @@ -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(); +}; diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java new file mode 100644 index 0000000..3b9d730 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/IStackTraceInfo.java @@ -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(); + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java new file mode 100644 index 0000000..92bbb82 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/JdwpPacket.java @@ -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); + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java new file mode 100644 index 0000000..ce95b04 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/Log.java @@ -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); + } + +} + + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java new file mode 100644 index 0000000..79eb5bb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MonitorThread.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java new file mode 100644 index 0000000..24dbb05 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/MultiLineReceiver.java @@ -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); +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java new file mode 100644 index 0000000..956b004 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeAllocationInfo.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java new file mode 100644 index 0000000..5a26317 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeLibraryMapInfo.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java new file mode 100644 index 0000000..e54818d --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NativeStackCallInfo.java @@ -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> + * <sourcefile>:<linenumber> + */ + 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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java new file mode 100644 index 0000000..d2b5a1e --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/NullOutputReceiver.java @@ -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; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java new file mode 100644 index 0000000..610cb59 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/RawImage.java @@ -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; +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java new file mode 100644 index 0000000..44df000 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/SyncService.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java new file mode 100644 index 0000000..8f284f3 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/ThreadInfo.java @@ -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; + } +} + diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java new file mode 100644 index 0000000..ec9186c --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventContainer.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java new file mode 100644 index 0000000..85e99c1 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventLogParser.java @@ -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(); + } + } + } + + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java new file mode 100644 index 0000000..b68b4e8 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/EventValueDescription.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java new file mode 100644 index 0000000..7bae202 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/GcEventContainer.java @@ -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(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java new file mode 100644 index 0000000..016f8aa --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidTypeException.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java new file mode 100644 index 0000000..a3050c8 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/InvalidValueTypeException.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java new file mode 100644 index 0000000..b49f025 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/log/LogReceiver.java @@ -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; + } + +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java new file mode 100644 index 0000000..b61a698 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/ITestRunListener.java @@ -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); +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java new file mode 100755 index 0000000..bc1834f --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java @@ -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); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java new file mode 100644 index 0000000..4edbbbb --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunner.java @@ -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 ""; + } + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java new file mode 100644 index 0000000..4d3b108 --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/TestIdentifier.java @@ -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(); + } +} diff --git a/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java new file mode 100644 index 0000000..8167e5d --- /dev/null +++ b/ddms/libs/ddmlib/src/com/android/ddmlib/utils/ArrayHelper.java @@ -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; + } +} diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java new file mode 100644 index 0000000..77d10c1 --- /dev/null +++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/InstrumentationResultParserTest.java @@ -0,0 +1,245 @@ +/* + * 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 junit.framework.TestCase; + + +/** + * Tests InstrumentationResultParser. + */ +public class InstrumentationResultParserTest extends TestCase { + + private InstrumentationResultParser mParser; + private VerifyingTestResult mTestResult; + + // static dummy test names to use for validation + private static final String CLASS_NAME = "com.test.FooTest"; + private static final String TEST_NAME = "testFoo"; + private static final String STACK_TRACE = "java.lang.AssertionFailedException"; + + /** + * @param name - test name + */ + public InstrumentationResultParserTest(String name) { + super(name); + } + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + mTestResult = new VerifyingTestResult(); + mParser = new InstrumentationResultParser(mTestResult); + } + + /** + * Tests that the test run started and test start events is sent on first + * bundle received. + */ + public void testTestStarted() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + assertEquals(0, mTestResult.mNumTestsRun); + } + + /** + * Tests that a single successful test execution. + */ + public void testTestSuccess() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + addCommonStatus(output); + addSuccessCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + assertEquals(1, mTestResult.mNumTestsRun); + assertEquals(null, mTestResult.mTestStatus); + } + + /** + * Test basic parsing of failed test case. + */ + public void testTestFailed() { + StringBuilder output = buildCommonResult(); + addStartCode(output); + addCommonStatus(output); + addStackTrace(output); + addFailureCode(output); + + injectTestString(output.toString()); + assertCommonAttributes(); + + assertEquals(1, mTestResult.mNumTestsRun); + assertEquals(ITestRunListener.TestFailure.FAILURE, mTestResult.mTestStatus); + assertEquals(STACK_TRACE, mTestResult.mTrace); + } + + /** + * Test basic parsing and conversion of time from output. + */ + public void testTimeParsing() { + final String timeString = "Time: 4.9"; + injectTestString(timeString); + assertEquals(4900, mTestResult.mTestTime); + } + + /** + * builds a common test result using TEST_NAME and TEST_CLASS. + */ + private StringBuilder buildCommonResult() { + StringBuilder output = new StringBuilder(); + // add test start bundle + addCommonStatus(output); + addStatusCode(output, "1"); + // add end test bundle, without status + addCommonStatus(output); + return output; + } + + /** + * Adds common status results to the provided output. + */ + private void addCommonStatus(StringBuilder output) { + addStatusKey(output, "stream", "\r\n" + CLASS_NAME); + addStatusKey(output, "test", TEST_NAME); + addStatusKey(output, "class", CLASS_NAME); + addStatusKey(output, "current", "1"); + addStatusKey(output, "numtests", "1"); + addStatusKey(output, "id", "InstrumentationTestRunner"); + } + + /** + * Adds a stack trace status bundle to output. + */ + private void addStackTrace(StringBuilder output) { + addStatusKey(output, "stack", STACK_TRACE); + + } + + /** + * Helper method to add a status key-value bundle. + */ + private void addStatusKey(StringBuilder outputBuilder, String key, + String value) { + outputBuilder.append("INSTRUMENTATION_STATUS: "); + outputBuilder.append(key); + outputBuilder.append('='); + outputBuilder.append(value); + outputBuilder.append("\r\n"); + } + + private void addStartCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "1"); + } + + private void addSuccessCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "0"); + } + + private void addFailureCode(StringBuilder outputBuilder) { + addStatusCode(outputBuilder, "-2"); + } + + private void addStatusCode(StringBuilder outputBuilder, String value) { + outputBuilder.append("INSTRUMENTATION_STATUS_CODE: "); + outputBuilder.append(value); + outputBuilder.append("\r\n"); + } + + /** + * inject a test string into the result parser. + * + * @param result + */ + private void injectTestString(String result) { + byte[] data = result.getBytes(); + mParser.addOutput(data, 0, data.length); + mParser.flush(); + } + + private void assertCommonAttributes() { + assertEquals(CLASS_NAME, mTestResult.mSuiteName); + assertEquals(1, mTestResult.mTestCount); + assertEquals(TEST_NAME, mTestResult.mTestName); + } + + /** + * A specialized test listener that stores a single test events. + */ + private class VerifyingTestResult implements ITestRunListener { + + String mSuiteName; + int mTestCount; + int mNumTestsRun; + String mTestName; + long mTestTime; + TestFailure mTestStatus; + String mTrace; + boolean mStopped; + + VerifyingTestResult() { + mNumTestsRun = 0; + mTestStatus = null; + mStopped = false; + } + + public void testEnded(TestIdentifier test) { + mNumTestsRun++; + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); + + } + + public void testFailed(TestFailure status, TestIdentifier test, String trace) { + mTestStatus = status; + mTrace = trace; + assertEquals("Unexpected class name", mSuiteName, test.getClassName()); + assertEquals("Unexpected test ended", mTestName, test.getTestName()); + } + + public void testRunEnded(long elapsedTime) { + mTestTime = elapsedTime; + + } + + public void testRunStarted(int testCount) { + mTestCount = testCount; + } + + public void testRunStopped(long elapsedTime) { + mTestTime = elapsedTime; + mStopped = true; + } + + public void testStarted(TestIdentifier test) { + mSuiteName = test.getClassName(); + mTestName = test.getTestName(); + } + + public void testRunFailed(String errorMessage) { + // ignored + } + } + +} diff --git a/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java new file mode 100644 index 0000000..9acaaf9 --- /dev/null +++ b/ddms/libs/ddmlib/tests/src/com/android/ddmlib/testrunner/RemoteAndroidTestRunnerTest.java @@ -0,0 +1,248 @@ +/* + * 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.Client; +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.Device.DeviceState; +import com.android.ddmlib.log.LogReceiver; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.util.Map; + +/** + * Tests RemoteAndroidTestRunner. + */ +public class RemoteAndroidTestRunnerTest extends TestCase { + + private RemoteAndroidTestRunner mRunner; + private MockDevice mMockDevice; + + private static final String TEST_PACKAGE = "com.test"; + private static final String TEST_RUNNER = "com.test.InstrumentationTestRunner"; + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + mMockDevice = new MockDevice(); + mRunner = new RemoteAndroidTestRunner(TEST_PACKAGE, TEST_RUNNER, mMockDevice); + } + + /** + * Test the basic case building of the instrumentation runner command with no arguments. + */ + public void testRun() { + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r %s/%s", TEST_PACKAGE, TEST_RUNNER), + mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with log set. + */ + public void testRunWithLog() { + mRunner.setLogOnly(true); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e log true %s/%s", TEST_PACKAGE, + TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with method set. + */ + public void testRunWithMethod() { + final String className = "FooTest"; + final String testName = "fooTest"; + mRunner.setMethodName(className, testName); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r -e class %s#%s %s/%s", className, + testName, TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + /** + * Test the building of the instrumentation runner command with extra args set. + */ + public void testRunWithExtraArgs() { + final String extraArgs = "blah"; + mRunner.setExtraArgs(extraArgs); + mRunner.run(new EmptyListener()); + assertStringsEquals(String.format("am instrument -w -r %s %s/%s", extraArgs, + TEST_PACKAGE, TEST_RUNNER), mMockDevice.getLastShellCommand()); + } + + + /** + * Assert two strings are equal ignoring whitespace. + */ + private void assertStringsEquals(String str1, String str2) { + String strippedStr1 = str1.replaceAll(" ", ""); + String strippedStr2 = str2.replaceAll(" ", ""); + assertEquals(strippedStr1, strippedStr2); + } + + /** + * A dummy device that does nothing except store the provided executed shell command for + * later retrieval. + */ + private static class MockDevice implements IDevice { + + private String mLastShellCommand; + + /** + * Stores the provided command for later retrieval from getLastShellCommand. + */ + public void executeShellCommand(String command, + IShellOutputReceiver receiver) throws IOException { + mLastShellCommand = command; + } + + /** + * Get the last command provided to executeShellCommand. + */ + public String getLastShellCommand() { + return mLastShellCommand; + } + + public boolean createForward(int localPort, int remotePort) { + throw new UnsupportedOperationException(); + } + + public Client getClient(String applicationName) { + throw new UnsupportedOperationException(); + } + + public String getClientName(int pid) { + throw new UnsupportedOperationException(); + } + + public Client[] getClients() { + throw new UnsupportedOperationException(); + } + + public FileListingService getFileListingService() { + throw new UnsupportedOperationException(); + } + + public Map<String, String> getProperties() { + throw new UnsupportedOperationException(); + } + + public String getProperty(String name) { + throw new UnsupportedOperationException(); + } + + public int getPropertyCount() { + throw new UnsupportedOperationException(); + } + + public RawImage getScreenshot() throws IOException { + throw new UnsupportedOperationException(); + } + + public String getSerialNumber() { + throw new UnsupportedOperationException(); + } + + public DeviceState getState() { + throw new UnsupportedOperationException(); + } + + public SyncService getSyncService() { + throw new UnsupportedOperationException(); + } + + public boolean hasClients() { + throw new UnsupportedOperationException(); + } + + public boolean isBootLoader() { + throw new UnsupportedOperationException(); + } + + public boolean isEmulator() { + throw new UnsupportedOperationException(); + } + + public boolean isOffline() { + throw new UnsupportedOperationException(); + } + + public boolean isOnline() { + throw new UnsupportedOperationException(); + } + + public boolean removeForward(int localPort, int remotePort) { + throw new UnsupportedOperationException(); + } + + public void runEventLogService(LogReceiver receiver) throws IOException { + throw new UnsupportedOperationException(); + } + + public void runLogService(String logname, LogReceiver receiver) throws IOException { + throw new UnsupportedOperationException(); + } + + public String getAvdName() { + return ""; + } + + } + + /** + * An empty implementation of ITestRunListener. + */ + private static class EmptyListener implements ITestRunListener { + + public void testEnded(TestIdentifier test) { + // ignore + } + + public void testFailed(TestFailure status, TestIdentifier test, String trace) { + // ignore + } + + public void testRunEnded(long elapsedTime) { + // ignore + } + + public void testRunFailed(String errorMessage) { + // ignore + } + + public void testRunStarted(int testCount) { + // ignore + } + + public void testRunStopped(long elapsedTime) { + // ignore + } + + public void testStarted(TestIdentifier test) { + // ignore + } + + } +} |