blob: 9356c13e4891f46326d2a223e7c7e85b9cc01f04 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ddmlib;
import com.android.ddmlib.ClientData.DebuggerStatus;
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.d("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(DebuggerStatus.DEFAULT);
mClient.update(Client.CHANGE_DEBUGGER_STATUS);
}
} 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.d("ddms", "Good handshake from debugger");
JdwpPacket.consumeHandshake(mReadBuffer);
sendHandshake();
mConnState = ST_READY;
ClientData cd = mClient.getClientData();
cd.setDebuggerConnectionStatus(DebuggerStatus.ATTACHED);
mClient.update(Client.CHANGE_DEBUGGER_STATUS);
// 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.d("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);
}
}
}