/* * 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); } } }