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