blob: 5ccf7efcb2155e7ff0fb468524713d195f3f0ea1 [file] [log] [blame]
/*
* 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.AdbHelper.AdbResponse;
import com.android.ddmlib.ClientData.DebuggerStatus;
import com.android.ddmlib.DebugPortManager.IDebugPortProvider;
import com.android.ddmlib.IDevice.DeviceState;
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.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A Device monitor. This connects to the Android Debug Bridge and get device and
* debuggable process information from it.
*/
final class DeviceMonitor {
private byte[] mLengthBuffer = new byte[4];
private byte[] mLengthBuffer2 = new byte[4];
private boolean mQuit = false;
private AndroidDebugBridge mServer;
private SocketChannel mMainAdbConnection = null;
private boolean mMonitoring = false;
private int mConnectionAttempt = 0;
private int mRestartAttemptCount = 0;
private boolean mInitialDeviceListDone = false;
private Selector mSelector;
private final ArrayList<Device> mDevices = new ArrayList<Device>();
private final ArrayList<Integer> mDebuggerPorts = new ArrayList<Integer>();
private final HashMap<Client, Integer> mClientsToReopen = new HashMap<Client, Integer>();
/**
* Creates a new {@link DeviceMonitor} object and links it to the running
* {@link AndroidDebugBridge} object.
* @param server the running {@link AndroidDebugBridge}.
*/
DeviceMonitor(AndroidDebugBridge server) {
mServer = server;
mDebuggerPorts.add(DdmPreferences.getDebugPortBase());
}
/**
* Starts the monitoring.
*/
void start() {
new Thread("Device List Monitor") { //$NON-NLS-1$
@Override
public void run() {
deviceMonitorLoop();
}
}.start();
}
/**
* Stops the monitoring.
*/
void stop() {
mQuit = true;
// wakeup the main loop thread by closing the main connection to adb.
try {
if (mMainAdbConnection != null) {
mMainAdbConnection.close();
}
} catch (IOException e1) {
}
// wake up the secondary loop by closing the selector.
if (mSelector != null) {
mSelector.wakeup();
}
}
/**
* Returns if the monitor is currently connected to the debug bridge server.
* @return
*/
boolean isMonitoring() {
return mMonitoring;
}
int getConnectionAttemptCount() {
return mConnectionAttempt;
}
int getRestartAttemptCount() {
return mRestartAttemptCount;
}
/**
* Returns the devices.
*/
Device[] getDevices() {
synchronized (mDevices) {
return mDevices.toArray(new Device[mDevices.size()]);
}
}
boolean hasInitialDeviceList() {
return mInitialDeviceListDone;
}
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();
}
/**
* Monitors the devices. This connects to the Debug Bridge
*/
private void deviceMonitorLoop() {
do {
try {
if (mMainAdbConnection == null) {
Log.d("DeviceMonitor", "Opening adb connection");
mMainAdbConnection = openAdbConnection();
if (mMainAdbConnection == null) {
mConnectionAttempt++;
Log.e("DeviceMonitor", "Connection attempts: " + mConnectionAttempt);
if (mConnectionAttempt > 10) {
if (!mServer.startAdb()) {
mRestartAttemptCount++;
Log.e("DeviceMonitor",
"adb restart attempts: " + mRestartAttemptCount);
} else {
mRestartAttemptCount = 0;
}
}
waitABit();
} else {
Log.d("DeviceMonitor", "Connected to adb for device monitoring");
mConnectionAttempt = 0;
}
}
if (mMainAdbConnection != null && !mMonitoring) {
mMonitoring = sendDeviceListMonitoringRequest();
}
if (mMonitoring) {
// read the length of the incoming message
int length = readLength(mMainAdbConnection, 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) {
handleExpectionInMonitorLoop(ioe);
} catch (IOException ioe) {
handleExpectionInMonitorLoop(ioe);
}
} while (!mQuit);
}
private void handleExpectionInMonitorLoop(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 (mMainAdbConnection != null) {
try {
mMainAdbConnection.close();
} catch (IOException ioe) {
// we can safely ignore that one.
}
mMainAdbConnection = null;
// remove all devices from list
// because we are going to call mServer.deviceDisconnected which will acquire this
// lock we lock it first, so that the AndroidDebugBridge lock is always locked
// first.
synchronized (AndroidDebugBridge.getLock()) {
synchronized (mDevices) {
for (int n = mDevices.size() - 1; n >= 0; n--) {
Device device = mDevices.get(0);
removeDevice(device);
mServer.deviceDisconnected(device);
}
}
}
}
}
}
/**
* Sleeps for a little bit.
*/
private void waitABit() {
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
}
}
/**
* Attempts to connect to the debug bridge server.
* @return a connect socket if success, null otherwise
*/
private SocketChannel openAdbConnection() {
Log.d("DeviceMonitor", "Connecting to adb for Device List Monitoring...");
SocketChannel adbChannel = null;
try {
adbChannel = SocketChannel.open(AndroidDebugBridge.getSocketAddress());
adbChannel.socket().setTcpNoDelay(true);
} catch (IOException e) {
}
return adbChannel;
}
/**
*
* @return
* @throws IOException
*/
private boolean sendDeviceListMonitoringRequest() throws TimeoutException, IOException {
byte[] request = AdbHelper.formAdbRequest("host:track-devices"); //$NON-NLS-1$
try {
AdbHelper.write(mMainAdbConnection, request);
AdbResponse resp = AdbHelper.readAdbResponse(mMainAdbConnection,
false /* readDiagString */);
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!");
mMainAdbConnection.close();
throw e;
}
}
/** Processes an incoming device message from the socket */
private void processIncomingDeviceData(int length) throws IOException {
ArrayList<Device> list = new ArrayList<Device>();
if (length > 0) {
byte[] buffer = new byte[length];
String result = read(mMainAdbConnection, buffer);
String[] devices = 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
Device device = new Device(this, param[0] /*serialnumber*/,
DeviceState.getState(param[1]));
//add the device to the list
list.add(device);
}
}
}
// now merge the new devices with the old ones.
updateDevices(list);
}
/**
* Updates the device list with the new items received from the monitoring service.
*/
private void updateDevices(ArrayList<Device> newList) {
// because we are going to call mServer.deviceDisconnected which will acquire this lock
// we lock it first, so that the AndroidDebugBridge lock is always locked first.
synchronized (AndroidDebugBridge.getLock()) {
// array to store the devices that must be queried for information.
// it's important to not do it inside the synchronized loop as this could block
// the whole workspace (this lock is acquired during build too).
ArrayList<Device> devicesToQuery = new ArrayList<Device>();
synchronized (mDevices) {
// For each device in the current list, we look for a matching the new list.
// * if we find it, we update the current object with whatever new information
// there is
// (mostly state change, if the device becomes ready, we query for build info).
// We also remove the device from the new list to mark it as "processed"
// * if we do not find it, we remove it from the current list.
// Once this is done, the new list contains device we aren't monitoring yet, so we
// add them to the list, and start monitoring them.
for (int d = 0 ; d < mDevices.size() ;) {
Device device = mDevices.get(d);
// look for a similar device in the new list.
int count = newList.size();
boolean foundMatch = false;
for (int dd = 0 ; dd < count ; dd++) {
Device newDevice = newList.get(dd);
// see if it matches in id and serial number.
if (newDevice.getSerialNumber().equals(device.getSerialNumber())) {
foundMatch = true;
// update the state if needed.
if (device.getState() != newDevice.getState()) {
device.setState(newDevice.getState());
device.update(Device.CHANGE_STATE);
// if the device just got ready/online, we need to start
// monitoring it.
if (device.isOnline()) {
if (AndroidDebugBridge.getClientSupport()) {
if (!startMonitoringDevice(device)) {
Log.e("DeviceMonitor",
"Failed to start monitoring "
+ device.getSerialNumber());
}
}
if (device.getPropertyCount() == 0) {
devicesToQuery.add(device);
}
}
}
// remove the new device from the list since it's been used
newList.remove(dd);
break;
}
}
if (!foundMatch) {
// the device is gone, we need to remove it, and keep current index
// to process the next one.
removeDevice(device);
mServer.deviceDisconnected(device);
} else {
// process the next one
d++;
}
}
// at this point we should still have some new devices in newList, so we
// process them.
for (Device newDevice : newList) {
// add them to the list
mDevices.add(newDevice);
mServer.deviceConnected(newDevice);
// start monitoring them.
if (AndroidDebugBridge.getClientSupport()) {
if (newDevice.isOnline()) {
startMonitoringDevice(newDevice);
}
}
// look for their build info.
if (newDevice.isOnline()) {
devicesToQuery.add(newDevice);
}
}
}
// query the new devices for info.
for (Device d : devicesToQuery) {
queryNewDeviceForInfo(d);
}
}
newList.clear();
}
private void removeDevice(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.
}
}
}
/**
* Queries a device for its build info.
* @param device the device to query.
*/
private void queryNewDeviceForInfo(Device device) {
// TODO: do this in a separate thread.
try {
// first get the list of properties.
device.executeShellCommand(GetPropReceiver.GETPROP_COMMAND,
new GetPropReceiver(device));
queryNewDeviceForMountingPoint(device, IDevice.MNT_EXTERNAL_STORAGE);
queryNewDeviceForMountingPoint(device, IDevice.MNT_DATA);
queryNewDeviceForMountingPoint(device, IDevice.MNT_ROOT);
// now get the emulator Virtual Device name (if applicable).
if (device.isEmulator()) {
EmulatorConsole console = EmulatorConsole.getConsole(device);
if (console != null) {
device.setAvdName(console.getAvdName());
console.close();
}
}
} catch (TimeoutException e) {
Log.w("DeviceMonitor", String.format("Connection timeout getting info for device %s",
device.getSerialNumber()));
} catch (AdbCommandRejectedException e) {
// This should never happen as we only do this once the device is online.
Log.w("DeviceMonitor", String.format(
"Adb rejected command to get device %1$s info: %2$s",
device.getSerialNumber(), e.getMessage()));
} catch (ShellCommandUnresponsiveException e) {
Log.w("DeviceMonitor", String.format(
"Adb shell command took too long returning info for device %s",
device.getSerialNumber()));
} catch (IOException e) {
Log.w("DeviceMonitor", String.format(
"IO Error getting info for device %s",
device.getSerialNumber()));
}
}
private void queryNewDeviceForMountingPoint(final Device device, final String name)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
IOException {
device.executeShellCommand("echo $" + name, new MultiLineReceiver() { //$NON-NLS-1$
@Override
public boolean isCancelled() {
return false;
}
@Override
public void processNewLines(String[] lines) {
for (String line : lines) {
if (!line.isEmpty()) {
// this should be the only one.
device.setMountingPoint(name, line);
}
}
}
});
}
/**
* Starts a monitoring service for a device.
* @param device the device to monitor.
* @return true if success.
*/
private boolean startMonitoringDevice(Device device) {
SocketChannel socketChannel = openAdbConnection();
if (socketChannel != null) {
try {
boolean result = sendDeviceMonitoringRequest(socketChannel, device);
if (result) {
if (mSelector == null) {
startDeviceMonitorThread();
}
device.setClientMonitoringSocket(socketChannel);
synchronized (mDevices) {
// always wakeup before doing the register. The synchronized block
// ensure that the selector won't select() before the end of this block.
// @see deviceClientMonitorLoop
mSelector.wakeup();
socketChannel.configureBlocking(false);
socketChannel.register(mSelector, SelectionKey.OP_READ, device);
}
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 {
// This synchronized block stops us from doing the select() if a new
// Device is being added.
// @see startMonitoringDevice()
synchronized (mDevices) {
}
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!
waitABit();
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();
}
}
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
synchronized (mDevices) {
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 boolean sendDeviceMonitoringRequest(SocketChannel socket, Device device)
throws TimeoutException, AdbCommandRejectedException, IOException {
try {
AdbHelper.setDevice(socket, device);
byte[] request = AdbHelper.formAdbRequest("track-jdwp"); //$NON-NLS-1$
AdbHelper.write(socket, request);
AdbResponse resp = AdbHelper.readAdbResponse(socket, false /* readDiagString */);
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(Device device, 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.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.
* @return
*/
private void openClient(Device device, int pid, int port, 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
* @param device
* @param pid
* @param socket
* @param debuggerPort the debugger port.
* @param monitorThread the {@link MonitorThread} object.
*/
private void createClient(Device device, int pid, SocketChannel socket, int debuggerPort,
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);
} else {
client = null;
}
}
private int getNextDebuggerPort() {
// get the first port and remove it
synchronized (mDebuggerPorts) {
if (!mDebuggerPorts.isEmpty()) {
int port = mDebuggerPorts.get(0);
// remove it.
mDebuggerPorts.remove(0);
// if there's nothing left, add the next port to the list
if (mDebuggerPorts.isEmpty()) {
mDebuggerPorts.add(port+1);
}
return port;
}
}
return -1;
}
void addPortToAvailableList(int port) {
if (port > 0) {
synchronized (mDebuggerPorts) {
// because there could be case where clients are closed twice, we have to make
// sure the port number is not already in the list.
if (mDebuggerPorts.indexOf(port) == -1) {
// add the port to the list while keeping it sorted. It's not like there's
// going to be tons of objects so we do it linearly.
int count = mDebuggerPorts.size();
for (int i = 0 ; i < count ; i++) {
if (port < mDebuggerPorts.get(i)) {
mDebuggerPorts.add(i, port);
break;
}
}
// TODO: check if we can compact the end of the list.
}
}
}
}
/**
* 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 int readLength(SocketChannel socket, 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 from a socket.
* @param socket
* @param buffer
* @return the content of the buffer as a string, or null if it failed to convert the buffer.
* @throws IOException
*/
private String read(SocketChannel socket, 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) {
// we'll return null below.
}
return null;
}
}