| /* |
| * 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.annotations.NonNull; |
| 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; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * 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 status change */ |
| public static final int CHANGE_DEBUGGER_STATUS = 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: allocation information updated */ |
| public static final int CHANGE_METHOD_PROFILING_STATUS = 0x0800; |
| /** Client change bit mask: hprof data updated */ |
| public static final int CHANGE_HPROF = 0x1000; |
| |
| /** Client change bit mask: combination of {@link Client#CHANGE_NAME}, |
| * {@link Client#CHANGE_DEBUGGER_STATUS}, and {@link Client#CHANGE_PORT}. |
| */ |
| public static final int CHANGE_INFO = CHANGE_NAME | CHANGE_DEBUGGER_STATUS | 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 mHeapInfoUpdateEnabled; |
| private boolean mHeapSegmentUpdateEnabled; |
| |
| /* |
| * 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 = 800*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(); |
| mHeapInfoUpdateEnabled = DdmPreferences.getInitialHeapUpdate(); |
| mHeapSegmentUpdateEnabled = DdmPreferences.getInitialHeapUpdate(); |
| } |
| |
| /** |
| * Returns a string representation of the {@link Client} object. |
| */ |
| @Override |
| public String toString() { |
| return "[Client pid: " + mClientData.getPid() + "]"; |
| } |
| |
| /** |
| * Returns the {@link IDevice} on which this Client is running. |
| */ |
| public IDevice getDevice() { |
| return mDevice; |
| } |
| |
| /** Returns the {@link Device} on which this Client is running. |
| */ |
| Device getDeviceImpl() { |
| 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. |
| */ |
| @NonNull |
| 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 |
| } |
| } |
| |
| /** |
| * Makes the VM dump an HPROF file |
| */ |
| public void dumpHprof() { |
| boolean canStream = mClientData.hasFeature(ClientData.FEATURE_HPROF_STREAMING); |
| try { |
| if (canStream) { |
| HandleHeap.sendHPDS(this); |
| } else { |
| String file = "/sdcard/" + mClientData.getClientDescription().replaceAll( |
| "\\:.*", "") + ".hprof"; |
| HandleHeap.sendHPDU(this, file); |
| } |
| } catch (IOException e) { |
| Log.w("ddms", "Send of HPDU message failed"); |
| // ignore |
| } |
| } |
| |
| /** |
| * Toggles method profiling state. |
| * @deprecated Use {@link #startMethodTracer()}, {@link #stopMethodTracer()}, |
| * {@link #startSamplingProfiler(int, java.util.concurrent.TimeUnit)} or |
| * {@link #stopSamplingProfiler()} instead. |
| */ |
| public void toggleMethodProfiling() { |
| try { |
| switch (mClientData.getMethodProfilingStatus()) { |
| case TRACER_ON: |
| stopMethodTracer(); |
| break; |
| case SAMPLER_ON: |
| stopSamplingProfiler(); |
| break; |
| case OFF: |
| startMethodTracer(); |
| break; |
| } |
| } catch (IOException e) { |
| Log.w("ddms", "Toggle method profiling failed"); |
| // ignore |
| } |
| } |
| |
| private int getProfileBufferSize() { |
| return DdmPreferences.getProfilerBufferSizeMb() * 1024 * 1024; |
| } |
| |
| public void startMethodTracer() throws IOException { |
| boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING); |
| int bufferSize = getProfileBufferSize(); |
| if (canStream) { |
| HandleProfiling.sendMPSS(this, bufferSize, 0 /*flags*/); |
| } else { |
| String file = "/sdcard/" + |
| mClientData.getClientDescription().replaceAll("\\:.*", "") + |
| DdmConstants.DOT_TRACE; |
| HandleProfiling.sendMPRS(this, file, bufferSize, 0 /*flags*/); |
| } |
| } |
| |
| public void stopMethodTracer() throws IOException { |
| boolean canStream = mClientData.hasFeature(ClientData.FEATURE_PROFILING_STREAMING); |
| |
| if (canStream) { |
| HandleProfiling.sendMPSE(this); |
| } else { |
| HandleProfiling.sendMPRE(this); |
| } |
| } |
| |
| public void startSamplingProfiler(int samplingInterval, TimeUnit timeUnit) throws IOException { |
| int bufferSize = getProfileBufferSize(); |
| HandleProfiling.sendSPSS(this, bufferSize, samplingInterval, timeUnit); |
| } |
| |
| public void stopSamplingProfiler() throws IOException { |
| HandleProfiling.sendSPSE(this); |
| } |
| |
| public boolean startOpenGlTracing() { |
| boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING); |
| if (!canTraceOpenGl) { |
| return false; |
| } |
| |
| try { |
| HandleViewDebug.sendStartGlTracing(this); |
| return true; |
| } catch (IOException e) { |
| Log.w("ddms", "Start OpenGL Tracing failed"); |
| return false; |
| } |
| } |
| |
| public boolean stopOpenGlTracing() { |
| boolean canTraceOpenGl = mClientData.hasFeature(ClientData.FEATURE_OPENGL_TRACING); |
| if (!canTraceOpenGl) { |
| return false; |
| } |
| |
| try { |
| HandleViewDebug.sendStopGlTracing(this); |
| return true; |
| } catch (IOException e) { |
| Log.w("ddms", "Stop OpenGL Tracing failed"); |
| return false; |
| } |
| } |
| |
| /** |
| * Sends a request to the VM to send the enable status of the method profiling. |
| * 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 requestMethodProfilingStatus() { |
| try { |
| HandleHeap.sendREAQ(this); |
| } catch (IOException e) { |
| Log.e("ddmlib", e); |
| } |
| } |
| |
| |
| /** |
| * 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) { |
| 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) { |
| setHeapInfoUpdateEnabled(enabled); |
| setHeapSegmentUpdateEnabled(enabled); |
| } |
| |
| public void setHeapInfoUpdateEnabled(boolean enabled) { |
| mHeapInfoUpdateEnabled = enabled; |
| |
| try { |
| HandleHeap.sendHPIF(this, |
| enabled ? HandleHeap.HPIF_WHEN_EVERY_GC : HandleHeap.HPIF_WHEN_NEVER); |
| |
| } catch (IOException ioe) { |
| // ignore it here; client will clean up shortly |
| } |
| |
| update(CHANGE_HEAP_MODE); |
| } |
| |
| public void setHeapSegmentUpdateEnabled(boolean enabled) { |
| mHeapSegmentUpdateEnabled = enabled; |
| |
| try { |
| 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); |
| } |
| |
| void initializeHeapUpdateStatus() throws IOException { |
| setHeapInfoUpdateEnabled(mHeapInfoUpdateEnabled); |
| } |
| |
| /** |
| * Fires a single heap update. |
| */ |
| public void updateHeapInfo() { |
| try { |
| HandleHeap.sendHPIF(this, HandleHeap.HPIF_WHEN_NOW); |
| } catch (IOException ioe) { |
| // ignore it here; client will clean up shortly |
| } |
| } |
| |
| /** |
| * Returns whether any heap update is enabled. |
| * @see #setHeapUpdateEnabled(boolean) |
| */ |
| public boolean isHeapUpdateEnabled() { |
| return mHeapInfoUpdateEnabled || mHeapSegmentUpdateEnabled; |
| } |
| |
| /** |
| * 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 information. 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 { |
| |
| // Fix to avoid a race condition on mChan. This should be better synchronized |
| // but just capturing the channel here, avoids a NPE. |
| SocketChannel chan = mChan; |
| if (chan == 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); |
| } |
| |
| // Synchronizing on this variable is still useful as we do not want to threads |
| // reading at the same time from the same channel, and the only change that |
| // can happen to this channel is to be closed and mChan become null. |
| //noinspection SynchronizationOnLocalVariableOrMethodParameter |
| synchronized (chan) { |
| try { |
| packet.writeAndConsume(chan); |
| } |
| 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.d("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.d("ddms", |
| "Good handshake from client, sending HELO to " + mClientData.getPid()); |
| JdwpPacket.consumeHandshake(mReadBuffer); |
| mConnState = ST_NEED_DDM_PKT; |
| HandleHello.sendHelloCommands(this, SERVER_PROTOCOL_VERSION); |
| // see if we have another packet in the buffer |
| return getJdwpPacket(); |
| case JdwpPacket.HANDSHAKE_BAD: |
| Log.d("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.d("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.d("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.d("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); |
| } |
| } |
| |