/* * 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.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.ddmlib.AdbHelper.AdbResponse; import com.android.ddmlib.ClientData.DebuggerStatus; import com.android.ddmlib.DebugPortManager.IDebugPortProvider; import com.android.ddmlib.IDevice.DeviceState; import com.android.ddmlib.utils.DebuggerPorts; import com.android.utils.Pair; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import com.google.common.util.concurrent.Uninterruptibles; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousCloseException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; /** * The {@link DeviceMonitor} monitors devices attached to adb. * * On one thread, it runs the {@link com.android.ddmlib.DeviceMonitor.DeviceListMonitorTask}. * This establishes a socket connection to the adb host, and issues a * {@link #ADB_TRACK_DEVICES_COMMAND}. It then monitors that socket for all changes about device * connection and device state. * * For each device that is detected to be online, it then opens a new socket connection to adb, * and issues a "track-jdwp" command to that device. On this connection, it monitors active * clients on the device. Note: a single thread monitors jdwp connections from all devices. * The different socket connections to adb (one per device) are multiplexed over a single selector. */ final class DeviceMonitor { private static final String ADB_TRACK_DEVICES_COMMAND = "host:track-devices"; private static final String ADB_TRACK_JDWP_COMMAND = "track-jdwp"; private final byte[] mLengthBuffer2 = new byte[4]; private volatile boolean mQuit = false; private final AndroidDebugBridge mServer; private DeviceListMonitorTask mDeviceListMonitorTask; private Selector mSelector; private final List<Device> mDevices = Lists.newCopyOnWriteArrayList(); private final DebuggerPorts mDebuggerPorts = new DebuggerPorts(DdmPreferences.getDebugPortBase()); private final Map<Client, Integer> mClientsToReopen = new HashMap<Client, Integer>(); private final BlockingQueue<Pair<SocketChannel,Device>> mChannelsToRegister = Queues.newLinkedBlockingQueue(); /** * Creates a new {@link DeviceMonitor} object and links it to the running * {@link AndroidDebugBridge} object. * @param server the running {@link AndroidDebugBridge}. */ DeviceMonitor(@NonNull AndroidDebugBridge server) { mServer = server; } /** * Starts the monitoring. */ void start() { mDeviceListMonitorTask = new DeviceListMonitorTask(mServer, new DeviceListUpdateListener()); new Thread(mDeviceListMonitorTask, "Device List Monitor").start(); //$NON-NLS-1$ } /** * Stops the monitoring. */ void stop() { mQuit = true; if (mDeviceListMonitorTask != null) { mDeviceListMonitorTask.stop(); } // wake up the secondary loop by closing the selector. if (mSelector != null) { mSelector.wakeup(); } } /** * Returns whether the monitor is currently connected to the debug bridge server. */ boolean isMonitoring() { return mDeviceListMonitorTask != null && mDeviceListMonitorTask.isMonitoring(); } int getConnectionAttemptCount() { return mDeviceListMonitorTask == null ? 0 : mDeviceListMonitorTask.getConnectionAttemptCount(); } int getRestartAttemptCount() { return mDeviceListMonitorTask == null ? 0 : mDeviceListMonitorTask.getRestartAttemptCount(); } boolean hasInitialDeviceList() { return mDeviceListMonitorTask != null && mDeviceListMonitorTask.hasInitialDeviceList(); } /** * Returns the devices. */ @NonNull Device[] getDevices() { // Since this is a copy of write array list, we don't want to do a compound operation // (toArray with an appropriate size) without locking, so we just let the container provide // an appropriately sized array //noinspection ToArrayCallWithZeroLengthArrayArgument return mDevices.toArray(new Device[0]); } @NonNull AndroidDebugBridge getServer() { return mServer; } void addClientToDropAndReopen(Client client, int port) { synchronized (mClientsToReopen) { Log.d("DeviceMonitor", "Adding " + client + " to list of client to reopen (" + port + ")."); if (mClientsToReopen.get(client) == null) { mClientsToReopen.put(client, port); } } mSelector.wakeup(); } /** * Attempts to connect to the debug bridge server. * @return a connect socket if success, null otherwise */ @Nullable private static SocketChannel openAdbConnection() { try { SocketChannel adbChannel = SocketChannel.open(AndroidDebugBridge.getSocketAddress()); adbChannel.socket().setTcpNoDelay(true); return adbChannel; } catch (IOException e) { return null; } } /** * Updates the device list with the new items received from the monitoring service. */ private void updateDevices(@NonNull List<Device> newList) { DeviceListComparisonResult result = DeviceListComparisonResult.compare(mDevices, newList); for (IDevice device : result.removed) { removeDevice((Device) device); mServer.deviceDisconnected(device); } List<Device> newlyOnline = Lists.newArrayListWithExpectedSize(mDevices.size()); for (Map.Entry<IDevice, DeviceState> entry : result.updated.entrySet()) { Device device = (Device) entry.getKey(); device.setState(entry.getValue()); device.update(Device.CHANGE_STATE); if (device.isOnline()) { newlyOnline.add(device); } } for (IDevice device : result.added) { mDevices.add((Device) device); mServer.deviceConnected(device); if (device.isOnline()) { newlyOnline.add((Device) device); } } if (AndroidDebugBridge.getClientSupport()) { for (Device device : newlyOnline) { if (!startMonitoringDevice(device)) { Log.e("DeviceMonitor", "Failed to start monitoring " + device.getSerialNumber()); } } } for (Device device : newlyOnline) { queryAvdName(device); } } private void removeDevice(@NonNull Device device) { device.clearClientList(); mDevices.remove(device); SocketChannel channel = device.getClientMonitoringSocket(); if (channel != null) { try { channel.close(); } catch (IOException e) { // doesn't really matter if the close fails. } } } private static void queryAvdName(@NonNull Device device) { if (!device.isEmulator()) { return; } EmulatorConsole console = EmulatorConsole.getConsole(device); if (console != null) { device.setAvdName(console.getAvdName()); console.close(); } } /** * Starts a monitoring service for a device. * @param device the device to monitor. * @return true if success. */ private boolean startMonitoringDevice(@NonNull Device device) { SocketChannel socketChannel = openAdbConnection(); if (socketChannel != null) { try { boolean result = sendDeviceMonitoringRequest(socketChannel, device); if (result) { if (mSelector == null) { startDeviceMonitorThread(); } device.setClientMonitoringSocket(socketChannel); socketChannel.configureBlocking(false); try { mChannelsToRegister.put(Pair.of(socketChannel, device)); } catch (InterruptedException e) { // the queue is unbounded, and isn't going to block } mSelector.wakeup(); return true; } } catch (TimeoutException e) { try { // attempt to close the socket if needed. socketChannel.close(); } catch (IOException e1) { // we can ignore that one. It may already have been closed. } Log.d("DeviceMonitor", "Connection Failure when starting to monitor device '" + device + "' : timeout"); } catch (AdbCommandRejectedException e) { try { // attempt to close the socket if needed. socketChannel.close(); } catch (IOException e1) { // we can ignore that one. It may already have been closed. } Log.d("DeviceMonitor", "Adb refused to start monitoring device '" + device + "' : " + e.getMessage()); } catch (IOException e) { try { // attempt to close the socket if needed. socketChannel.close(); } catch (IOException e1) { // we can ignore that one. It may already have been closed. } Log.d("DeviceMonitor", "Connection Failure when starting to monitor device '" + device + "' : " + e.getMessage()); } } return false; } private void startDeviceMonitorThread() throws IOException { mSelector = Selector.open(); new Thread("Device Client Monitor") { //$NON-NLS-1$ @Override public void run() { deviceClientMonitorLoop(); } }.start(); } private void deviceClientMonitorLoop() { do { try { int count = mSelector.select(); if (mQuit) { return; } synchronized (mClientsToReopen) { if (!mClientsToReopen.isEmpty()) { Set<Client> clients = mClientsToReopen.keySet(); MonitorThread monitorThread = MonitorThread.getInstance(); for (Client client : clients) { Device device = client.getDeviceImpl(); int pid = client.getClientData().getPid(); monitorThread.dropClient(client, false /* notify */); // This is kinda bad, but if we don't wait a bit, the client // will never answer the second handshake! Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); int port = mClientsToReopen.get(client); if (port == IDebugPortProvider.NO_STATIC_PORT) { port = getNextDebuggerPort(); } Log.d("DeviceMonitor", "Reopening " + client); openClient(device, pid, port, monitorThread); device.update(Device.CHANGE_CLIENT_LIST); } mClientsToReopen.clear(); } } // register any new channels while (!mChannelsToRegister.isEmpty()) { try { Pair<SocketChannel, Device> data = mChannelsToRegister.take(); data.getFirst().register(mSelector, SelectionKey.OP_READ, data.getSecond()); } catch (InterruptedException e) { // doesn't block: this thread is the only reader and it reads only when // there is data } } if (count == 0) { continue; } Set<SelectionKey> keys = mSelector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); iter.remove(); if (key.isValid() && key.isReadable()) { Object attachment = key.attachment(); if (attachment instanceof Device) { Device device = (Device)attachment; SocketChannel socket = device.getClientMonitoringSocket(); if (socket != null) { try { int length = readLength(socket, mLengthBuffer2); processIncomingJdwpData(device, socket, length); } catch (IOException ioe) { Log.d("DeviceMonitor", "Error reading jdwp list: " + ioe.getMessage()); socket.close(); // restart the monitoring of that device if (mDevices.contains(device)) { Log.d("DeviceMonitor", "Restarting monitoring service for " + device); startMonitoringDevice(device); } } } } } } } catch (IOException e) { Log.e("DeviceMonitor", "Connection error while monitoring clients."); } } while (!mQuit); } private static boolean sendDeviceMonitoringRequest(@NonNull SocketChannel socket, @NonNull Device device) throws TimeoutException, AdbCommandRejectedException, IOException { try { AdbHelper.setDevice(socket, device); AdbHelper.write(socket, AdbHelper.formAdbRequest(ADB_TRACK_JDWP_COMMAND)); AdbResponse resp = AdbHelper.readAdbResponse(socket, false); if (!resp.okay) { // request was refused by adb! Log.e("DeviceMonitor", "adb refused request: " + resp.message); } return resp.okay; } catch (TimeoutException e) { Log.e("DeviceMonitor", "Sending jdwp tracking request timed out!"); throw e; } catch (IOException e) { Log.e("DeviceMonitor", "Sending jdwp tracking request failed!"); throw e; } } private void processIncomingJdwpData(@NonNull Device device, @NonNull SocketChannel monitorSocket, int length) throws IOException { // This methods reads @length bytes from the @monitorSocket channel. // These bytes correspond to the pids of the current set of processes on the device. // It takes this set of pids and compares them with the existing set of clients // for the device. Clients that correspond to pids that are not alive anymore are // dropped, and new clients are created for pids that don't have a corresponding Client. if (length >= 0) { // array for the current pids. Set<Integer> newPids = new HashSet<Integer>(); // get the string data if there are any if (length > 0) { byte[] buffer = new byte[length]; String result = read(monitorSocket, buffer); // split each line in its own list and create an array of integer pid String[] pids = result == null ? new String[0] : result.split("\n"); //$NON-NLS-1$ for (String pid : pids) { try { newPids.add(Integer.valueOf(pid)); } catch (NumberFormatException nfe) { // looks like this pid is not really a number. Lets ignore it. continue; } } } MonitorThread monitorThread = MonitorThread.getInstance(); List<Client> clients = device.getClientList(); Map<Integer, Client> existingClients = new HashMap<Integer, Client>(); synchronized (clients) { for (Client c : clients) { existingClients.put(c.getClientData().getPid(), c); } } Set<Client> clientsToRemove = new HashSet<Client>(); for (Integer pid : existingClients.keySet()) { if (!newPids.contains(pid)) { clientsToRemove.add(existingClients.get(pid)); } } Set<Integer> pidsToAdd = new HashSet<Integer>(newPids); pidsToAdd.removeAll(existingClients.keySet()); monitorThread.dropClients(clientsToRemove, false); // at this point whatever pid is left in the list needs to be converted into Clients. for (int newPid : pidsToAdd) { openClient(device, newPid, getNextDebuggerPort(), monitorThread); } if (!pidsToAdd.isEmpty() || !clientsToRemove.isEmpty()) { mServer.deviceChanged(device, Device.CHANGE_CLIENT_LIST); } } } /** Opens and creates a new client. */ private static void openClient(@NonNull Device device, int pid, int port, @NonNull MonitorThread monitorThread) { SocketChannel clientSocket; try { clientSocket = AdbHelper.createPassThroughConnection( AndroidDebugBridge.getSocketAddress(), device, pid); // required for Selector clientSocket.configureBlocking(false); } catch (UnknownHostException uhe) { Log.d("DeviceMonitor", "Unknown Jdwp pid: " + pid); return; } catch (TimeoutException e) { Log.w("DeviceMonitor", "Failed to connect to client '" + pid + "': timeout"); return; } catch (AdbCommandRejectedException e) { Log.w("DeviceMonitor", "Adb rejected connection to client '" + pid + "': " + e.getMessage()); return; } catch (IOException ioe) { Log.w("DeviceMonitor", "Failed to connect to client '" + pid + "': " + ioe.getMessage()); return ; } createClient(device, pid, clientSocket, port, monitorThread); } /** Creates a client and register it to the monitor thread */ private static void createClient(@NonNull Device device, int pid, @NonNull SocketChannel socket, int debuggerPort, @NonNull MonitorThread monitorThread) { /* * Successfully connected to something. Create a Client object, add * it to the list, and initiate the JDWP handshake. */ Client client = new Client(device, socket, pid); if (client.sendHandshake()) { try { if (AndroidDebugBridge.getClientSupport()) { client.listenForDebugger(debuggerPort); } } catch (IOException ioe) { client.getClientData().setDebuggerConnectionStatus(DebuggerStatus.ERROR); Log.e("ddms", "Can't bind to local " + debuggerPort + " for debugger"); // oh well } client.requestAllocationStatus(); } else { Log.e("ddms", "Handshake with " + client + " failed!"); /* * The handshake send failed. We could remove it now, but if the * failure is "permanent" we'll just keep banging on it and * getting the same result. Keep it in the list with its "error" * state so we don't try to reopen it. */ } if (client.isValid()) { device.addClient(client); monitorThread.addClient(client); } } private int getNextDebuggerPort() { return mDebuggerPorts.next(); } void addPortToAvailableList(int port) { mDebuggerPorts.free(port); } /** * Reads the length of the next message from a socket. * @param socket The {@link SocketChannel} to read from. * @return the length, or 0 (zero) if no data is available from the socket. * @throws IOException if the connection failed. */ private static int readLength(@NonNull SocketChannel socket, @NonNull byte[] buffer) throws IOException { String msg = read(socket, buffer); if (msg != null) { try { return Integer.parseInt(msg, 16); } catch (NumberFormatException nfe) { // we'll throw an exception below. } } // we receive something we can't read. It's better to reset the connection at this point. throw new IOException("Unable to read length"); } /** * Fills a buffer by reading data from a socket. * @return the content of the buffer as a string, or null if it failed to convert the buffer. * @throws IOException if there was not enough data to fill the buffer */ @Nullable private static String read(@NonNull SocketChannel socket, @NonNull byte[] buffer) throws IOException { ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length); while (buf.position() != buf.limit()) { int count; count = socket.read(buf); if (count < 0) { throw new IOException("EOF"); } } try { return new String(buffer, 0, buf.position(), AdbHelper.DEFAULT_ENCODING); } catch (UnsupportedEncodingException e) { return null; } } private class DeviceListUpdateListener implements DeviceListMonitorTask.UpdateListener { @Override public void connectionError(@NonNull Exception e) { for (Device device : mDevices) { removeDevice(device); mServer.deviceDisconnected(device); } } @Override public void deviceListUpdate(@NonNull Map<String, DeviceState> devices) { List<Device> l = Lists.newArrayListWithExpectedSize(devices.size()); for (Map.Entry<String, DeviceState> entry : devices.entrySet()) { l.add(new Device(DeviceMonitor.this, entry.getKey(), entry.getValue())); } // now merge the new devices with the old ones. updateDevices(l); } } @VisibleForTesting static class DeviceListComparisonResult { @NonNull public final Map<IDevice,DeviceState> updated; @NonNull public final List<IDevice> added; @NonNull public final List<IDevice> removed; private DeviceListComparisonResult(@NonNull Map<IDevice,DeviceState> updated, @NonNull List<IDevice> added, @NonNull List<IDevice> removed) { this.updated = updated; this.added = added; this.removed = removed; } @NonNull public static DeviceListComparisonResult compare(@NonNull List<? extends IDevice> previous, @NonNull List<? extends IDevice> current) { current = Lists.newArrayList(current); final Map<IDevice,DeviceState> updated = Maps.newHashMapWithExpectedSize(current.size()); final List<IDevice> added = Lists.newArrayListWithExpectedSize(1); final List<IDevice> removed = Lists.newArrayListWithExpectedSize(1); for (IDevice device : previous) { IDevice currentDevice = find(current, device); if (currentDevice != null) { if (currentDevice.getState() != device.getState()) { updated.put(device, currentDevice.getState()); } current.remove(currentDevice); } else { removed.add(device); } } added.addAll(current); return new DeviceListComparisonResult(updated, added, removed); } @Nullable private static IDevice find(@NonNull List<? extends IDevice> devices, @NonNull IDevice device) { for (IDevice d : devices) { if (d.getSerialNumber().equals(device.getSerialNumber())) { return d; } } return null; } } @VisibleForTesting static class DeviceListMonitorTask implements Runnable { private final byte[] mLengthBuffer = new byte[4]; private final AndroidDebugBridge mBridge; private final UpdateListener mListener; private SocketChannel mAdbConnection = null; private boolean mMonitoring = false; private int mConnectionAttempt = 0; private int mRestartAttemptCount = 0; private boolean mInitialDeviceListDone = false; private volatile boolean mQuit; private interface UpdateListener { void connectionError(@NonNull Exception e); void deviceListUpdate(@NonNull Map<String,DeviceState> devices); } public DeviceListMonitorTask(@NonNull AndroidDebugBridge bridge, @NonNull UpdateListener listener) { mBridge = bridge; mListener = listener; } @Override public void run() { do { if (mAdbConnection == null) { Log.d("DeviceMonitor", "Opening adb connection"); mAdbConnection = openAdbConnection(); if (mAdbConnection == null) { mConnectionAttempt++; Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt); if (mConnectionAttempt > 10) { if (!mBridge.startAdb()) { mRestartAttemptCount++; Log.e("DeviceMonitor", "adb restart attempts: " + mRestartAttemptCount); } else { Log.i("DeviceMonitor", "adb restarted"); mRestartAttemptCount = 0; } } Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); } else { Log.d("DeviceMonitor", "Connected to adb for device monitoring"); mConnectionAttempt = 0; } } try { if (mAdbConnection != null && !mMonitoring) { mMonitoring = sendDeviceListMonitoringRequest(); } if (mMonitoring) { int length = readLength(mAdbConnection, mLengthBuffer); if (length >= 0) { // read the incoming message processIncomingDeviceData(length); // flag the fact that we have build the list at least once. mInitialDeviceListDone = true; } } } catch (AsynchronousCloseException ace) { // this happens because of a call to Quit. We do nothing, and the loop will break. } catch (TimeoutException ioe) { handleExceptionInMonitorLoop(ioe); } catch (IOException ioe) { handleExceptionInMonitorLoop(ioe); } } while (!mQuit); } private boolean sendDeviceListMonitoringRequest() throws TimeoutException, IOException { byte[] request = AdbHelper.formAdbRequest(ADB_TRACK_DEVICES_COMMAND); try { AdbHelper.write(mAdbConnection, request); AdbResponse resp = AdbHelper.readAdbResponse(mAdbConnection, false); if (!resp.okay) { // request was refused by adb! Log.e("DeviceMonitor", "adb refused request: " + resp.message); } return resp.okay; } catch (IOException e) { Log.e("DeviceMonitor", "Sending Tracking request failed!"); mAdbConnection.close(); throw e; } } private void handleExceptionInMonitorLoop(@NonNull Exception e) { if (!mQuit) { if (e instanceof TimeoutException) { Log.e("DeviceMonitor", "Adb connection Error: timeout"); } else { Log.e("DeviceMonitor", "Adb connection Error:" + e.getMessage()); } mMonitoring = false; if (mAdbConnection != null) { try { mAdbConnection.close(); } catch (IOException ioe) { // we can safely ignore that one. } mAdbConnection = null; mListener.connectionError(e); } } } /** Processes an incoming device message from the socket */ private void processIncomingDeviceData(int length) throws IOException { Map<String, DeviceState> result; if (length <= 0) { result = Collections.emptyMap(); } else { String response = read(mAdbConnection, new byte[length]); result = parseDeviceListResponse(response); } mListener.deviceListUpdate(result); } @VisibleForTesting static Map<String, DeviceState> parseDeviceListResponse(@Nullable String result) { Map<String, DeviceState> deviceStateMap = Maps.newHashMap(); String[] devices = result == null ? new String[0] : result.split("\n"); //$NON-NLS-1$ for (String d : devices) { String[] param = d.split("\t"); //$NON-NLS-1$ if (param.length == 2) { // new adb uses only serial numbers to identify devices deviceStateMap.put(param[0], DeviceState.getState(param[1])); } } return deviceStateMap; } boolean isMonitoring() { return mMonitoring; } boolean hasInitialDeviceList() { return mInitialDeviceListDone; } int getConnectionAttemptCount() { return mConnectionAttempt; } int getRestartAttemptCount() { return mRestartAttemptCount; } public void stop() { mQuit = true; // wakeup the main loop thread by closing the main connection to adb. if (mAdbConnection != null) { try { mAdbConnection.close(); } catch (IOException ignored) { } } } } }