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 */ 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 */ 93 static MonitorThread createInstance() { 94 return mInstance = new MonitorThread(); 95 } 96 97 /** 98 * Get singleton instance of the client monitor thread. 99 */ 100 static MonitorThread getInstance() { 101 return mInstance; 102 } 103 104 105 /** 106 * Sets or changes the port number for "debug selected". 107 */ 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 */ 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 */ 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 */ 164 boolean getRetryOnBadHandshake() { 165 return true; // TODO? make configurable 166 } 167 168 /** 169 * Get an array of known clients. 170 */ 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 */ 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 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 */ 294 int getDebugSelectedPort() { 295 return mDebugSelectedPort; 296 } 297 298 /* 299 * Something happened. Figure out what. 300 */ 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 */ 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 */ 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 * Drops the provided list of clients from the monitor. This will lock the {@link Client} 447 * list of the {@link Device} running each of the clients. 448 */ 449 synchronized void dropClients(Collection<? extends Client> clients, boolean notify) { 450 for (Client c : clients) { 451 dropClient(c, notify); 452 } 453 } 454 455 /* 456 * Process activity from one of the debugger sockets. This could be a new 457 * connection or a data packet. 458 */ 459 private void processDebuggerActivity(SelectionKey key) { 460 Debugger dbg = (Debugger)key.attachment(); 461 462 try { 463 if (key.isAcceptable()) { 464 try { 465 acceptNewDebugger(dbg, null); 466 } catch (IOException ioe) { 467 Log.w("ddms", "debugger accept() failed"); 468 ioe.printStackTrace(); 469 } 470 } else if (key.isReadable()) { 471 processDebuggerData(key); 472 } else { 473 Log.d("ddm-debugger", "key in unknown state"); 474 } 475 } catch (CancelledKeyException cke) { 476 // key has been cancelled we can ignore that. 477 } 478 } 479 480 /* 481 * Accept a new connection from a debugger. If successful, register it with 482 * the Selector. 483 */ 484 private void acceptNewDebugger(Debugger dbg, ServerSocketChannel acceptChan) 485 throws IOException { 486 487 synchronized (mClientList) { 488 SocketChannel chan; 489 490 if (acceptChan == null) 491 chan = dbg.accept(); 492 else 493 chan = dbg.accept(acceptChan); 494 495 if (chan != null) { 496 chan.socket().setTcpNoDelay(true); 497 498 wakeup(); 499 500 try { 501 chan.register(mSelector, SelectionKey.OP_READ, dbg); 502 } catch (IOException ioe) { 503 // failed, drop the connection 504 dbg.closeData(); 505 throw ioe; 506 } catch (RuntimeException re) { 507 // failed, drop the connection 508 dbg.closeData(); 509 throw re; 510 } 511 } else { 512 Log.w("ddms", "ignoring duplicate debugger"); 513 // new connection already closed 514 } 515 } 516 } 517 518 /* 519 * We have incoming data from the debugger. Forward it to the client. 520 */ 521 private void processDebuggerData(SelectionKey key) { 522 Debugger dbg = (Debugger)key.attachment(); 523 524 try { 525 /* 526 * Read pending data. 527 */ 528 dbg.read(); 529 530 /* 531 * See if we have a full packet in the buffer. It's possible we have 532 * more than one packet, so we have to loop. 533 */ 534 JdwpPacket packet = dbg.getJdwpPacket(); 535 while (packet != null) { 536 Log.v("ddms", "Forwarding dbg req 0x" 537 + Integer.toHexString(packet.getId()) + " to " 538 + dbg.getClient()); 539 540 dbg.forwardPacketToClient(packet); 541 542 packet = dbg.getJdwpPacket(); 543 } 544 } catch (IOException ioe) { 545 /* 546 * Close data connection; automatically un-registers dbg from 547 * selector. The failure could be caused by the debugger going away, 548 * or by the client going away and failing to accept our data. 549 * Either way, the debugger connection does not need to exist any 550 * longer. We also need to recycle the connection to the client, so 551 * that the VM sees the debugger disconnect. For a DDM-aware client 552 * this won't be necessary, and we can just send a "debugger 553 * disconnected" message. 554 */ 555 Log.d("ddms", "Closing connection to debugger " + dbg); 556 dbg.closeData(); 557 Client client = dbg.getClient(); 558 if (client.isDdmAware()) { 559 // TODO: soft-disconnect DDM-aware clients 560 Log.d("ddms", " (recycling client connection as well)"); 561 562 // we should drop the client, but also attempt to reopen it. 563 // This is done by the DeviceMonitor. 564 client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, 565 IDebugPortProvider.NO_STATIC_PORT); 566 } else { 567 Log.d("ddms", " (recycling client connection as well)"); 568 // we should drop the client, but also attempt to reopen it. 569 // This is done by the DeviceMonitor. 570 client.getDeviceImpl().getMonitor().addClientToDropAndReopen(client, 571 IDebugPortProvider.NO_STATIC_PORT); 572 } 573 } 574 } 575 576 /* 577 * Tell the thread that something has changed. 578 */ 579 private void wakeup() { 580 mSelector.wakeup(); 581 } 582 583 /** 584 * Tell the thread to stop. Called from UI thread. 585 */ 586 synchronized void quit() { 587 mQuit = true; 588 wakeup(); 589 Log.d("ddms", "Waiting for Monitor thread"); 590 try { 591 this.join(); 592 // since we're quitting, lets drop all the client and disconnect 593 // the DebugSelectedPort 594 synchronized (mClientList) { 595 for (Client c : mClientList) { 596 c.close(false /* notify */); 597 broadcast(CLIENT_DISCONNECTED, c); 598 } 599 mClientList.clear(); 600 } 601 602 if (mDebugSelectedChan != null) { 603 mDebugSelectedChan.close(); 604 mDebugSelectedChan.socket().close(); 605 mDebugSelectedChan = null; 606 } 607 mSelector.close(); 608 } catch (InterruptedException ie) { 609 ie.printStackTrace(); 610 } catch (IOException e) { 611 // TODO Auto-generated catch block 612 e.printStackTrace(); 613 } 614 615 mInstance = null; 616 } 617 618 /** 619 * Add a new Client to the list of things we monitor. Also adds the client's 620 * channel and the client's debugger listener to the selection list. This 621 * should only be called from one thread (the VMWatcherThread) to avoid a 622 * race between "alreadyOpen" and Client creation. 623 */ 624 synchronized void addClient(Client client) { 625 if (mInstance == null) { 626 return; 627 } 628 629 Log.d("ddms", "Adding new client " + client); 630 631 synchronized (mClientList) { 632 mClientList.add(client); 633 634 /* 635 * Register the Client's socket channel with the selector. We attach 636 * the Client to the SelectionKey. If you try to register a new 637 * channel with the Selector while it is waiting for I/O, you will 638 * block. The solution is to call wakeup() and then hold a lock to 639 * ensure that the registration happens before the Selector goes 640 * back to sleep. 641 */ 642 try { 643 wakeup(); 644 645 client.register(mSelector); 646 647 Debugger dbg = client.getDebugger(); 648 if (dbg != null) { 649 dbg.registerListener(mSelector); 650 } 651 } catch (IOException ioe) { 652 // not really expecting this to happen 653 ioe.printStackTrace(); 654 } 655 } 656 } 657 658 /* 659 * Broadcast an event to all message handlers. 660 */ 661 private void broadcast(int event, Client client) { 662 Log.d("ddms", "broadcast " + event + ": " + client); 663 664 /* 665 * The handler objects appear once in mHandlerMap for each message they 666 * handle. We want to notify them once each, so we convert the HashMap 667 * to a HashSet before we iterate. 668 */ 669 HashSet<ChunkHandler> set; 670 synchronized (mHandlerMap) { 671 Collection<ChunkHandler> values = mHandlerMap.values(); 672 set = new HashSet<ChunkHandler>(values); 673 } 674 675 Iterator<ChunkHandler> iter = set.iterator(); 676 while (iter.hasNext()) { 677 ChunkHandler handler = iter.next(); 678 switch (event) { 679 case CLIENT_READY: 680 try { 681 handler.clientReady(client); 682 } catch (IOException ioe) { 683 // Something failed with the client. It should 684 // fall out of the list the next time we try to 685 // do something with it, so we discard the 686 // exception here and assume cleanup will happen 687 // later. May need to propagate farther. The 688 // trouble is that not all values for "event" may 689 // actually throw an exception. 690 Log.w("ddms", 691 "Got exception while broadcasting 'ready'"); 692 return; 693 } 694 break; 695 case CLIENT_DISCONNECTED: 696 handler.clientDisconnected(client); 697 break; 698 default: 699 throw new UnsupportedOperationException(); 700 } 701 } 702 703 } 704 705 /** 706 * Opens (or reopens) the "debug selected" port and listen for connections. 707 * @return true if the port was opened successfully. 708 * @throws IOException 709 */ 710 private boolean reopenDebugSelectedPort() throws IOException { 711 712 Log.d("ddms", "reopen debug-selected port: " + mNewDebugSelectedPort); 713 if (mDebugSelectedChan != null) { 714 mDebugSelectedChan.close(); 715 } 716 717 mDebugSelectedChan = ServerSocketChannel.open(); 718 mDebugSelectedChan.configureBlocking(false); // required for Selector 719 720 InetSocketAddress addr = new InetSocketAddress( 721 InetAddress.getByName("localhost"), //$NON-NLS-1$ 722 mNewDebugSelectedPort); 723 mDebugSelectedChan.socket().setReuseAddress(true); // enable SO_REUSEADDR 724 725 try { 726 mDebugSelectedChan.socket().bind(addr); 727 if (mSelectedClient != null) { 728 mSelectedClient.update(Client.CHANGE_PORT); 729 } 730 731 mDebugSelectedChan.register(mSelector, SelectionKey.OP_ACCEPT, this); 732 733 return true; 734 } catch (java.net.BindException e) { 735 displayDebugSelectedBindError(mNewDebugSelectedPort); 736 737 // do not attempt to reopen it. 738 mDebugSelectedChan = null; 739 mNewDebugSelectedPort = -1; 740 741 return false; 742 } 743 } 744 745 /* 746 * We have some activity on the "debug selected" port. Handle it. 747 */ 748 private void processDebugSelectedActivity(SelectionKey key) { 749 assert key.isAcceptable(); 750 751 ServerSocketChannel acceptChan = (ServerSocketChannel)key.channel(); 752 753 /* 754 * Find the debugger associated with the currently-selected client. 755 */ 756 if (mSelectedClient != null) { 757 Debugger dbg = mSelectedClient.getDebugger(); 758 759 if (dbg != null) { 760 Log.d("ddms", "Accepting connection on 'debug selected' port"); 761 try { 762 acceptNewDebugger(dbg, acceptChan); 763 } catch (IOException ioe) { 764 // client should be gone, keep going 765 } 766 767 return; 768 } 769 } 770 771 Log.w("ddms", 772 "Connection on 'debug selected' port, but none selected"); 773 try { 774 SocketChannel chan = acceptChan.accept(); 775 chan.close(); 776 } catch (IOException ioe) { 777 // not expected; client should be gone, keep going 778 } catch (NotYetBoundException e) { 779 displayDebugSelectedBindError(mDebugSelectedPort); 780 } 781 } 782 783 private void displayDebugSelectedBindError(int port) { 784 String message = String.format( 785 "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.", 786 port); 787 788 Log.logAndDisplay(LogLevel.ERROR, "ddms", message); 789 } 790 } 791