1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ddmlib; 18 19 20 import com.android.ddmlib.DebugPortManager.IDebugPortProvider; 21 import com.android.ddmlib.Log.LogLevel; 22 23 import java.io.IOException; 24 import java.net.InetAddress; 25 import java.net.InetSocketAddress; 26 import java.nio.BufferOverflowException; 27 import java.nio.ByteBuffer; 28 import java.nio.channels.CancelledKeyException; 29 import java.nio.channels.NotYetBoundException; 30 import java.nio.channels.SelectionKey; 31 import java.nio.channels.Selector; 32 import java.nio.channels.ServerSocketChannel; 33 import java.nio.channels.SocketChannel; 34 import java.util.ArrayList; 35 import java.util.Collection; 36 import java.util.HashMap; 37 import java.util.HashSet; 38 import java.util.Iterator; 39 import java.util.Set; 40 41 /** 42 * Monitor open connections. 43 */ 44 final class MonitorThread extends Thread { 45 46 // For broadcasts to message handlers 47 //private static final int CLIENT_CONNECTED = 1; 48 49 private static final int CLIENT_READY = 2; 50 51 private static final int CLIENT_DISCONNECTED = 3; 52 53 private volatile boolean mQuit = false; 54 55 // List of clients we're paying attention to 56 private ArrayList<Client> mClientList; 57 58 // The almighty mux 59 private Selector mSelector; 60 61 // Map chunk types to handlers 62 private HashMap<Integer, ChunkHandler> mHandlerMap; 63 64 // port for "debug selected" 65 private ServerSocketChannel mDebugSelectedChan; 66 67 private int mNewDebugSelectedPort; 68 69 private int mDebugSelectedPort = -1; 70 71 /** 72 * "Selected" client setup to answer debugging connection to the mNewDebugSelectedPort port. 73 */ 74 private Client mSelectedClient = null; 75 76 // singleton 77 private static MonitorThread mInstance; 78 79 /** 80 * Generic constructor. 81 */ MonitorThread()82 private MonitorThread() { 83 super("Monitor"); 84 mClientList = new ArrayList<Client>(); 85 mHandlerMap = new HashMap<Integer, ChunkHandler>(); 86 87 mNewDebugSelectedPort = DdmPreferences.getSelectedDebugPort(); 88 } 89 90 /** 91 * Creates and return the singleton instance of the client monitor thread. 92 */ createInstance()93 static MonitorThread createInstance() { 94 return mInstance = new MonitorThread(); 95 } 96 97 /** 98 * Get singleton instance of the client monitor thread. 99 */ getInstance()100 static MonitorThread getInstance() { 101 return mInstance; 102 } 103 104 105 /** 106 * Sets or changes the port number for "debug selected". 107 */ setDebugSelectedPort(int port)108 synchronized void setDebugSelectedPort(int port) throws IllegalStateException { 109 if (mInstance == null) { 110 return; 111 } 112 113 if (AndroidDebugBridge.getClientSupport() == false) { 114 return; 115 } 116 117 if (mDebugSelectedChan != null) { 118 Log.d("ddms", "Changing debug-selected port to " + port); 119 mNewDebugSelectedPort = port; 120 wakeup(); 121 } else { 122 // we set mNewDebugSelectedPort instead of mDebugSelectedPort so that it's automatically 123 // opened on the first run loop. 124 mNewDebugSelectedPort = port; 125 } 126 } 127 128 /** 129 * Sets the client to accept debugger connection on the custom "Selected debug port". 130 * @param selectedClient the client. Can be null. 131 */ setSelectedClient(Client selectedClient)132 synchronized void setSelectedClient(Client selectedClient) { 133 if (mInstance == null) { 134 return; 135 } 136 137 if (mSelectedClient != selectedClient) { 138 Client oldClient = mSelectedClient; 139 mSelectedClient = selectedClient; 140 141 if (oldClient != null) { 142 oldClient.update(Client.CHANGE_PORT); 143 } 144 145 if (mSelectedClient != null) { 146 mSelectedClient.update(Client.CHANGE_PORT); 147 } 148 } 149 } 150 151 /** 152 * Returns the client accepting debugger connection on the custom "Selected debug port". 153 */ getSelectedClient()154 Client getSelectedClient() { 155 return mSelectedClient; 156 } 157 158 159 /** 160 * Returns "true" if we want to retry connections to clients if we get a bad 161 * JDWP handshake back, "false" if we want to just mark them as bad and 162 * leave them alone. 163 */ getRetryOnBadHandshake()164 boolean getRetryOnBadHandshake() { 165 return true; // TODO? make configurable 166 } 167 168 /** 169 * Get an array of known clients. 170 */ getClients()171 Client[] getClients() { 172 synchronized (mClientList) { 173 return mClientList.toArray(new Client[0]); 174 } 175 } 176 177 /** 178 * Register "handler" as the handler for type "type". 179 */ registerChunkHandler(int type, ChunkHandler handler)180 synchronized void registerChunkHandler(int type, ChunkHandler handler) { 181 if (mInstance == null) { 182 return; 183 } 184 185 synchronized (mHandlerMap) { 186 if (mHandlerMap.get(type) == null) { 187 mHandlerMap.put(type, handler); 188 } 189 } 190 } 191 192 /** 193 * Watch for activity from clients and debuggers. 194 */ 195 @Override run()196 public void run() { 197 Log.d("ddms", "Monitor is up"); 198 199 // create a selector 200 try { 201 mSelector = Selector.open(); 202 } catch (IOException ioe) { 203 Log.logAndDisplay(LogLevel.ERROR, "ddms", 204 "Failed to initialize Monitor Thread: " + ioe.getMessage()); 205 return; 206 } 207 208 while (!mQuit) { 209 210 try { 211 /* 212 * sync with new registrations: we wait until addClient is done before going through 213 * and doing mSelector.select() again. 214 * @see {@link #addClient(Client)} 215 */ 216 synchronized (mClientList) { 217 } 218 219 // (re-)open the "debug selected" port, if it's not opened yet or 220 // if the port changed. 221 try { 222 if (AndroidDebugBridge.getClientSupport()) { 223 if ((mDebugSelectedChan == null || 224 mNewDebugSelectedPort != mDebugSelectedPort) && 225 mNewDebugSelectedPort != -1) { 226 if (reopenDebugSelectedPort()) { 227 mDebugSelectedPort = mNewDebugSelectedPort; 228 } 229 } 230 } 231 } catch (IOException ioe) { 232 Log.e("ddms", 233 "Failed to reopen debug port for Selected Client to: " + mNewDebugSelectedPort); 234 Log.e("ddms", ioe); 235 mNewDebugSelectedPort = mDebugSelectedPort; // no retry 236 } 237 238 int count; 239 try { 240 count = mSelector.select(); 241 } catch (IOException ioe) { 242 ioe.printStackTrace(); 243 continue; 244 } catch (CancelledKeyException cke) { 245 continue; 246 } 247 248 if (count == 0) { 249 // somebody called wakeup() ? 250 // Log.i("ddms", "selector looping"); 251 continue; 252 } 253 254 Set<SelectionKey> keys = mSelector.selectedKeys(); 255 Iterator<SelectionKey> iter = keys.iterator(); 256 257 while (iter.hasNext()) { 258 SelectionKey key = iter.next(); 259 iter.remove(); 260 261 try { 262 if (key.attachment() instanceof Client) { 263 processClientActivity(key); 264 } 265 else if (key.attachment() instanceof Debugger) { 266 processDebuggerActivity(key); 267 } 268 else if (key.attachment() instanceof MonitorThread) { 269 processDebugSelectedActivity(key); 270 } 271 else { 272 Log.e("ddms", "unknown activity key"); 273 } 274 } catch (Exception e) { 275 // we don't want to have our thread be killed because of any uncaught 276 // exception, so we intercept all here. 277 Log.e("ddms", "Exception during activity from Selector."); 278 Log.e("ddms", e); 279 } 280 } 281 } catch (Exception e) { 282 // we don't want to have our thread be killed because of any uncaught 283 // exception, so we intercept all here. 284 Log.e("ddms", "Exception MonitorThread.run()"); 285 Log.e("ddms", e); 286 } 287 } 288 } 289 290 291 /** 292 * Returns the port on which the selected client listen for debugger 293 */ getDebugSelectedPort()294 int getDebugSelectedPort() { 295 return mDebugSelectedPort; 296 } 297 298 /* 299 * Something happened. Figure out what. 300 */ processClientActivity(SelectionKey key)301 private void processClientActivity(SelectionKey key) { 302 Client client = (Client)key.attachment(); 303 304 try { 305 if (key.isReadable() == false || key.isValid() == false) { 306 Log.d("ddms", "Invalid key from " + client + ". Dropping client."); 307 dropClient(client, true /* notify */); 308 return; 309 } 310 311 client.read(); 312 313 /* 314 * See if we have a full packet in the buffer. It's possible we have 315 * more than one packet, so we have to loop. 316 */ 317 JdwpPacket packet = client.getJdwpPacket(); 318 while (packet != null) { 319 if (packet.isDdmPacket()) { 320 // unsolicited DDM request - hand it off 321 assert !packet.isReply(); 322 callHandler(client, packet, null); 323 packet.consume(); 324 } else if (packet.isReply() 325 && client.isResponseToUs(packet.getId()) != null) { 326 // reply to earlier DDM request 327 ChunkHandler handler = client 328 .isResponseToUs(packet.getId()); 329 if (packet.isError()) 330 client.packetFailed(packet); 331 else if (packet.isEmpty()) 332 Log.d("ddms", "Got empty reply for 0x" 333 + Integer.toHexString(packet.getId()) 334 + " from " + client); 335 else 336 callHandler(client, packet, handler); 337 packet.consume(); 338 client.removeRequestId(packet.getId()); 339 } else { 340 Log.v("ddms", "Forwarding client " 341 + (packet.isReply() ? "reply" : "event") + " 0x" 342 + Integer.toHexString(packet.getId()) + " to " 343 + client.getDebugger()); 344 client.forwardPacketToDebugger(packet); 345 } 346 347 // find next 348 packet = client.getJdwpPacket(); 349 } 350 } catch (CancelledKeyException e) { 351 // key was canceled probably due to a disconnected client before we could 352 // read stuff coming from the client, so we drop it. 353 dropClient(client, true /* notify */); 354 } catch (IOException ex) { 355 // something closed down, no need to print anything. The client is simply dropped. 356 dropClient(client, true /* notify */); 357 } catch (Exception ex) { 358 Log.e("ddms", ex); 359 360 /* close the client; automatically un-registers from selector */ 361 dropClient(client, true /* notify */); 362 363 if (ex instanceof BufferOverflowException) { 364 Log.w("ddms", 365 "Client data packet exceeded maximum buffer size " 366 + client); 367 } else { 368 // don't know what this is, display it 369 Log.e("ddms", ex); 370 } 371 } 372 } 373 374 /* 375 * Process an incoming DDM packet. If this is a reply to an earlier request, 376 * "handler" will be set to the handler responsible for the original 377 * request. The spec allows a JDWP message to include multiple DDM chunks. 378 */ callHandler(Client client, JdwpPacket packet, ChunkHandler handler)379 private void callHandler(Client client, JdwpPacket packet, 380 ChunkHandler handler) { 381 382 // on first DDM packet received, broadcast a "ready" message 383 if (!client.ddmSeen()) 384 broadcast(CLIENT_READY, client); 385 386 ByteBuffer buf = packet.getPayload(); 387 int type, length; 388 boolean reply = true; 389 390 type = buf.getInt(); 391 length = buf.getInt(); 392 393 if (handler == null) { 394 // not a reply, figure out who wants it 395 synchronized (mHandlerMap) { 396 handler = mHandlerMap.get(type); 397 reply = false; 398 } 399 } 400 401 if (handler == null) { 402 Log.w("ddms", "Received unsupported chunk type " 403 + ChunkHandler.name(type) + " (len=" + length + ")"); 404 } else { 405 Log.d("ddms", "Calling handler for " + ChunkHandler.name(type) 406 + " [" + handler + "] (len=" + length + ")"); 407 ByteBuffer ibuf = buf.slice(); 408 ByteBuffer roBuf = ibuf.asReadOnlyBuffer(); // enforce R/O 409 roBuf.order(ChunkHandler.CHUNK_ORDER); 410 // do the handling of the chunk synchronized on the client list 411 // to be sure there's no concurrency issue when we look for HOME 412 // in hasApp() 413 synchronized (mClientList) { 414 handler.handleChunk(client, type, roBuf, reply, packet.getId()); 415 } 416 } 417 } 418 419 /** 420 * Drops a client from the monitor. 421 * <p/>This will lock the {@link Client} list of the {@link Device} running <var>client</var>. 422 * @param client 423 * @param notify 424 */ dropClient(Client client, boolean notify)425 synchronized void dropClient(Client client, boolean notify) { 426 if (mInstance == null) { 427 return; 428 } 429 430 synchronized (mClientList) { 431 if (mClientList.remove(client) == false) { 432 return; 433 } 434 } 435 client.close(notify); 436 broadcast(CLIENT_DISCONNECTED, client); 437 438 /* 439 * http://forum.java.sun.com/thread.jspa?threadID=726715&start=0 440 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5073504 441 */ 442 wakeup(); 443 } 444 445 /* 446 * Process activity from one of the debugger sockets. This could be a new 447 * connection or a data packet. 448 */ processDebuggerActivity(SelectionKey key)449 private void processDebuggerActivity(SelectionKey key) { 450 Debugger dbg = (Debugger)key.attachment(); 451 452 try { 453 if (key.isAcceptable()) { 454 try { 455 acceptNewDebugger(dbg, null); 456 } catch (IOException ioe) { 457 Log.w("ddms", "debugger accept() failed"); 458 ioe.printStackTrace(); 459 } 460 } else if (key.isReadable()) { 461 processDebuggerData(key); 462 } else { 463 Log.d("ddm-debugger", "key in unknown state"); 464 } 465 } catch (CancelledKeyException cke) { 466 // key has been cancelled we can ignore that. 467 } 468 } 469 470 /* 471 * Accept a new connection from a debugger. If successful, register it with 472 * the Selector. 473 */ acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan)474 private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan) 475 throws IOException { 476 477 synchronized (mClientList) { 478 SocketChannel chan; 479 480 if (acceptChan == null) 481 chan = dbg.accept(); 482 else 483 chan = dbg.accept(acceptChan); 484 485 if (chan != null) { 486 chan.socket().setTcpNoDelay(true); 487 488 wakeup(); 489 490 try { 491 chan.register(mSelector, SelectionKey.OP_READ, dbg); 492 } catch (IOException ioe) { 493 // failed, drop the connection 494 dbg.closeData(); 495 throw ioe; 496 } catch (RuntimeException re) { 497 // failed, drop the connection 498 dbg.closeData(); 499 throw re; 500 } 501 } else { 502 Log.w("ddms", "ignoring duplicate debugger"); 503 // new connection already closed 504 } 505 } 506 } 507 508 /* 509 * We have incoming data from the debugger. Forward it to the client. 510 */ processDebuggerData(SelectionKey key)511 private void processDebuggerData(SelectionKey key) { 512 Debugger dbg = (Debugger)key.attachment(); 513 514 try { 515 /* 516 * Read pending data. 517 */ 518 dbg.read(); 519 520 /* 521 * See if we have a full packet in the buffer. It's possible we have 522 * more than one packet, so we have to loop. 523 */ 524 JdwpPacket packet = dbg.getJdwpPacket(); 525 while (packet != null) { 526 Log.v("ddms", "Forwarding dbg req 0x" 527 + Integer.toHexString(packet.getId()) + " to " 528 + dbg.getClient()); 529 530 dbg.forwardPacketToClient(packet); 531 532 packet = dbg.getJdwpPacket(); 533 } 534 } catch (IOException ioe) { 535 /* 536 * Close data connection; automatically un-registers dbg from 537 * selector. The failure could be caused by the debugger going away, 538 * or by the client going away and failing to accept our data. 539 * Either way, the debugger connection does not need to exist any 540 * longer. We also need to recycle the connection to the client, so 541 * that the VM sees the debugger disconnect. For a DDM-aware client 542 * this won't be necessary, and we can just send a "debugger 543 * disconnected" message. 544 */ 545 Log.d("ddms", "Closing connection to debugger " + dbg); 546 dbg.closeData(); 547 Client client = dbg.getClient(); 548 if (client.isDdmAware()) { 549 // TODO: soft-disconnect DDM-aware clients 550 Log.d("ddms", " (recycling client connection as well)"); 551 552 // we should drop the client, but also attempt to reopen it. 553 // This is done by the DeviceMonitor. 554 client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, 555 IDebugPortProvider.NO_STATIC_PORT); 556 } else { 557 Log.d("ddms", " (recycling client connection as well)"); 558 // we should drop the client, but also attempt to reopen it. 559 // This is done by the DeviceMonitor. 560 client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, 561 IDebugPortProvider.NO_STATIC_PORT); 562 } 563 } 564 } 565 566 /* 567 * Tell the thread that something has changed. 568 */ wakeup()569 private void wakeup() { 570 mSelector.wakeup(); 571 } 572 573 /** 574 * Tell the thread to stop. Called from UI thread. 575 */ quit()576 synchronized void quit() { 577 mQuit = true; 578 wakeup(); 579 Log.d("ddms", "Waiting for Monitor thread"); 580 try { 581 this.join(); 582 // since we're quitting, lets drop all the client and disconnect 583 // the DebugSelectedPort 584 synchronized (mClientList) { 585 for (Client c : mClientList) { 586 c.close(false /* notify */); 587 broadcast(CLIENT_DISCONNECTED, c); 588 } 589 mClientList.clear(); 590 } 591 592 if (mDebugSelectedChan != null) { 593 mDebugSelectedChan.close(); 594 mDebugSelectedChan.socket().close(); 595 mDebugSelectedChan = null; 596 } 597 mSelector.close(); 598 } catch (InterruptedException ie) { 599 ie.printStackTrace(); 600 } catch (IOException e) { 601 // TODO Auto-generated catch block 602 e.printStackTrace(); 603 } 604 605 mInstance = null; 606 } 607 608 /** 609 * Add a new Client to the list of things we monitor. Also adds the client's 610 * channel and the client's debugger listener to the selection list. This 611 * should only be called from one thread (the VMWatcherThread) to avoid a 612 * race between "alreadyOpen" and Client creation. 613 */ addClient(Client client)614 synchronized void addClient(Client client) { 615 if (mInstance == null) { 616 return; 617 } 618 619 Log.d("ddms", "Adding new client " + client); 620 621 synchronized (mClientList) { 622 mClientList.add(client); 623 624 /* 625 * Register the Client's socket channel with the selector. We attach 626 * the Client to the SelectionKey. If you try to register a new 627 * channel with the Selector while it is waiting for I/O, you will 628 * block. The solution is to call wakeup() and then hold a lock to 629 * ensure that the registration happens before the Selector goes 630 * back to sleep. 631 */ 632 try { 633 wakeup(); 634 635 client.register(mSelector); 636 637 Debugger dbg = client.getDebugger(); 638 if (dbg != null) { 639 dbg.registerListener(mSelector); 640 } 641 } catch (IOException ioe) { 642 // not really expecting this to happen 643 ioe.printStackTrace(); 644 } 645 } 646 } 647 648 /* 649 * Broadcast an event to all message handlers. 650 */ broadcast(int event, Client client)651 private void broadcast(int event, Client client) { 652 Log.d("ddms", "broadcast " + event + ": " + client); 653 654 /* 655 * The handler objects appear once in mHandlerMap for each message they 656 * handle. We want to notify them once each, so we convert the HashMap 657 * to a HashSet before we iterate. 658 */ 659 HashSet<ChunkHandler> set; 660 synchronized (mHandlerMap) { 661 Collection<ChunkHandler> values = mHandlerMap.values(); 662 set = new HashSet<ChunkHandler>(values); 663 } 664 665 Iterator<ChunkHandler> iter = set.iterator(); 666 while (iter.hasNext()) { 667 ChunkHandler handler = iter.next(); 668 switch (event) { 669 case CLIENT_READY: 670 try { 671 handler.clientReady(client); 672 } catch (IOException ioe) { 673 // Something failed with the client. It should 674 // fall out of the list the next time we try to 675 // do something with it, so we discard the 676 // exception here and assume cleanup will happen 677 // later. May need to propagate farther. The 678 // trouble is that not all values for "event" may 679 // actually throw an exception. 680 Log.w("ddms", 681 "Got exception while broadcasting 'ready'"); 682 return; 683 } 684 break; 685 case CLIENT_DISCONNECTED: 686 handler.clientDisconnected(client); 687 break; 688 default: 689 throw new UnsupportedOperationException(); 690 } 691 } 692 693 } 694 695 /** 696 * Opens (or reopens) the "debug selected" port and listen for connections. 697 * @return true if the port was opened successfully. 698 * @throws IOException 699 */ reopenDebugSelectedPort()700 private boolean reopenDebugSelectedPort() throws IOException { 701 702 Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort); 703 if (mDebugSelectedChan != null) { 704 mDebugSelectedChan.close(); 705 } 706 707 mDebugSelectedChan = ServerSocketChannel.open(); 708 mDebugSelectedChan.configureBlocking(false); // required for Selector 709 710 InetSocketAddress addr = new InetSocketAddress( 711 InetAddress.getByName("localhost"), //$NON-NLS-1$ 712 mNewDebugSelectedPort); 713 mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR 714 715 try { 716 mDebugSelectedChan.socket().bind(addr); 717 if (mSelectedClient != null) { 718 mSelectedClient.update(Client.CHANGE_PORT); 719 } 720 721 mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this); 722 723 return true; 724 } catch (java.net.BindException e) { 725 displayDebugSelectedBindError(mNewDebugSelectedPort); 726 727 // do not attempt to reopen it. 728 mDebugSelectedChan = null; 729 mNewDebugSelectedPort = -1; 730 731 return false; 732 } 733 } 734 735 /* 736 * We have some activity on the "debug selected" port. Handle it. 737 */ processDebugSelectedActivity(SelectionKey key)738 private void processDebugSelectedActivity(SelectionKey key) { 739 assert key.isAcceptable(); 740 741 ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel(); 742 743 /* 744 * Find the debugger associated with the currently-selected client. 745 */ 746 if (mSelectedClient != null) { 747 Debugger dbg = mSelectedClient.getDebugger(); 748 749 if (dbg != null) { 750 Log.d("ddms", "Accepting connection on 'debug selected' port"); 751 try { 752 acceptNewDebugger(dbg, acceptChan); 753 } catch (IOException ioe) { 754 // client should be gone, keep going 755 } 756 757 return; 758 } 759 } 760 761 Log.w("ddms", 762 "Connection on 'debug selected' port, but none selected"); 763 try { 764 SocketChannel chan = acceptChan.accept(); 765 chan.close(); 766 } catch (IOException ioe) { 767 // not expected; client should be gone, keep going 768 } catch (NotYetBoundException e) { 769 displayDebugSelectedBindError(mDebugSelectedPort); 770 } 771 } 772 displayDebugSelectedBindError(int port)773 private void displayDebugSelectedBindError(int port) { 774 String message = String.format( 775 "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.", 776 port); 777 778 Log.logAndDisplay(LogLevel.ERROR, "ddms", message); 779 } 780 } 781