
com.android.ddmlib.MonitorThread Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ddmlib Show documentation
Show all versions of ddmlib Show documentation
Library providing APIs to talk to Android devices
/*
* 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 mClientList;
// The almighty mux
private Selector mSelector;
// Map chunk types to handlers
private HashMap 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();
mHandlerMap = new HashMap();
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 keys = mSelector.selectedKeys();
Iterator 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.
* This will lock the {@link Client} list of the {@link Device} running client.
* @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 set;
synchronized (mHandlerMap) {
Collection values = mHandlerMap.values();
set = new HashSet(values);
}
Iterator 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);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy