blob: a4ff115dcd3153b426a1349287ec527852e43d6f [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.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 sInstance;
/**
* 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 sInstance = new MonitorThread();
}
/**
* Get singleton instance of the client monitor thread.
*/
static MonitorThread getInstance() {
return sInstance;
}
/**
* Sets or changes the port number for "debug selected".
*/
synchronized void setDebugSelectedPort(int port) throws IllegalStateException {
if (sInstance == null) {
return;
}
if (!AndroidDebugBridge.getClientSupport()) {
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 (sInstance == 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[mClientList.size()]);
}
}
/**
* Register "handler" as the handler for type "type".
*/
synchronized void registerChunkHandler(int type, ChunkHandler handler) {
if (sInstance == 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() || !key.isValid()) {
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 (sInstance == null) {
return;
}
synchronized (mClientList) {
if (!mClientList.remove(client)) {
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();
}
/**
* Drops the provided list of clients from the monitor. This will lock the {@link Client}
* list of the {@link Device} running each of the clients.
*/
synchronized void dropClients(Collection<? extends Client> clients, boolean notify) {
for (Client c : clients) {
dropClient(c, notify);
}
}
/*
* 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.w("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.d("ddms", "Closing connection to debugger " + dbg);
dbg.closeData();
Client client = dbg.getClient();
if (client.isDdmAware()) {
// TODO: soft-disconnect DDM-aware clients
Log.d("ddms", " (recycling client connection as well)");
// we should drop the client, but also attempt to reopen it.
// This is done by the DeviceMonitor.
client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client,
IDebugPortProvider.NO_STATIC_PORT);
} else {
Log.d("ddms", " (recycling client connection as well)");
// we should drop the client, but also attempt to reopen it.
// This is done by the DeviceMonitor.
client.getDeviceImpl().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();
}
sInstance = 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 (sInstance == 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.d("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);
}
}