/* * Copyright (C) 2011 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.tradefed.command; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.tradefed.device.IDeviceManager; import com.android.tradefed.device.IDeviceManager.FreeDeviceState; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.util.ArrayUtil; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Arrays; import java.util.Hashtable; import java.util.Map; /** * Class that receives remote commands to add and remove devices from use via a socket. * <p/> * Currently accepts only one remote connection at one time, and processes incoming commands * serially. * <p/> * Usage: * <pre> * RemoteManager r = new RemoteManager(deviceMgr, scheduler); * r.start(); * int port = r.getPort(); * ... inform client of port to use. Shuts down when instructed by client or on #cancel() * </pre> */ public class RemoteManager extends Thread { // constants that define wire protocol between RemoteClient and RemoteManager static final String DELIM = ";"; static final String FILTER = "filter"; static final String UNFILTER = "unfilter"; static final String ALL_DEVICES = "*"; static final String CLOSE = "close"; static final String ADD_COMMAND = "add_command"; private ServerSocket mServerSocket = null; private boolean mCancel = false; private final IDeviceManager mDeviceManager; private final ICommandScheduler mScheduler; private Map<String, ITestDevice> mFilteredDeviceMap = new Hashtable<String, ITestDevice>(); /** * Creates a {@link RemoteManager}. * * @param manager the {@link IDeviceManager} to use to allocate and free devices. * @param scheduler the {@link ICommandScheduler} to use to schedule commands. */ public RemoteManager(IDeviceManager manager, ICommandScheduler scheduler) { mDeviceManager = manager; mScheduler = scheduler; } /** * The main thread body of the remote manager. * <p/> * Creates a server socket, and waits for client connections. */ @Override public void run() { synchronized (this) { try { mServerSocket = new ServerSocket(0); } catch (IOException e) { CLog.e("Failed to open server socket: %s", e); return; } finally { // notify any listeners that the socket has been created notifyAll(); } } try { processClientConnections(mServerSocket); } finally { freeAllDevices(); closeSocket(mServerSocket); } } /** * Gets the socket port the remote manager is listening on, blocking for a short time if * necessary. * <p/> * {@link #start()} should be called before this method. * @return */ public synchronized int getPort() { if (mServerSocket == null) { try { wait(10*1000); } catch (InterruptedException e) { // ignore } } if (mServerSocket == null) { return -1; } return mServerSocket.getLocalPort(); } private void processClientConnections(ServerSocket serverSocket) { while (!mCancel) { Socket clientSocket = null; BufferedReader in = null; PrintWriter out = null; try { clientSocket = serverSocket.accept(); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); out = new PrintWriter(clientSocket.getOutputStream(), true); processClientCommands(in, out); } catch (IOException e) { CLog.e("Failed to accept connection: %s", e); } finally { closeReader(in); closeWriter(out); closeSocket(clientSocket); } } } private void processClientCommands(BufferedReader in, PrintWriter out) throws IOException { String line = null; while ((line = in.readLine()) != null && !mCancel) { boolean result = false; String[] commandSegments = line.split(DELIM); String cmdType = commandSegments[0]; if (FILTER.equals(cmdType)) { result = processFilterCommand(commandSegments); } else if (UNFILTER.equals(cmdType)) { result = processUnfilterCommand(commandSegments); } else if (CLOSE.equals(cmdType)) { cancel(); result = true; } else if (ADD_COMMAND.equals(cmdType)) { result = processAddCommand(commandSegments); } sendAck(result, out); } } private boolean processFilterCommand(final String[] commandSegments) { if (commandSegments.length < 2) { CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments)); return false; } final String serial = commandSegments[1]; ITestDevice allocatedDevice = mDeviceManager.forceAllocateDevice(serial); if (allocatedDevice != null) { Log.logAndDisplay(LogLevel.INFO, "RemoteManager", String.format("Allocating device %s that is still in use by remote tradefed", serial)); mFilteredDeviceMap.put(serial, allocatedDevice); return true; } else { CLog.e("Failed to allocate remote device %s", serial); return false; } } private boolean processUnfilterCommand(final String[] commandSegments) { if (commandSegments.length < 2) { CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments)); return false; } // TODO: consider making this synchronous, and sending ack back to client once allocated final String serial = commandSegments[1]; if (ALL_DEVICES.equals(serial)) { freeAllDevices(); return true; } else { ITestDevice d = mFilteredDeviceMap.remove(serial); if (d != null) { Log.logAndDisplay(LogLevel.INFO, "RemoteManager", String.format("Freeing device %s no longer in use by remote tradefed", serial)); mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE); return true; } else { CLog.w("Could not find device to free %s", serial); } } return false; } private boolean processAddCommand(final String[] commandSegments) { if (commandSegments.length < 3) { CLog.e("Invalid command received: %s", ArrayUtil.join(" ", (Object[])commandSegments)); return false; } long totalTime = Long.parseLong(commandSegments[1]); String[] cmdArgs = Arrays.copyOfRange(commandSegments, 2, commandSegments.length); Log.logAndDisplay(LogLevel.INFO, "RemoteManager", String.format("Adding command '%s'", ArrayUtil.join(" ", (Object[])cmdArgs))); return mScheduler.addCommand(cmdArgs, totalTime); } private void freeAllDevices() { for (ITestDevice d : mFilteredDeviceMap.values()) { Log.logAndDisplay(LogLevel.INFO, "RemoteManager", String.format("Freeing device %s no longer in use by remote tradefed", d.getSerialNumber())); mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE); } mFilteredDeviceMap.clear(); } private void sendAck(boolean result, PrintWriter out) { out.println(result); } /** * Cancel the remote manager. */ public synchronized void cancel() { if (!mCancel) { mCancel = true; Log.logAndDisplay(LogLevel.INFO, "RemoteManager", "Closing remote manager"); } } private void closeSocket(ServerSocket serverSocket) { if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { // ignore } } } private void closeSocket(Socket clientSocket) { if (clientSocket != null) { try { clientSocket.close(); } catch (IOException e) { // ignore e.printStackTrace(); } } } private void closeReader(BufferedReader in) { if (in != null) { try { in.close(); } catch (IOException e) { // ignore } } } private void closeWriter(PrintWriter out) { if (out != null) { out.close(); } } /** * @return <code>true</code> if a cancel has been requested */ public boolean isCanceled() { return mCancel; } }