Home | History | Annotate | Download | only in remote
      1 /*
      2  * Copyright (C) 2011 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 package com.android.tradefed.command.remote;
     17 
     18 import com.android.ddmlib.Log.LogLevel;
     19 import com.android.tradefed.command.ICommandScheduler;
     20 import com.android.tradefed.command.remote.CommandResult.Status;
     21 import com.android.tradefed.config.ConfigurationException;
     22 import com.android.tradefed.config.Option;
     23 import com.android.tradefed.config.OptionClass;
     24 import com.android.tradefed.device.FreeDeviceState;
     25 import com.android.tradefed.device.IDeviceManager;
     26 import com.android.tradefed.device.ITestDevice;
     27 import com.android.tradefed.log.LogUtil.CLog;
     28 import com.android.tradefed.util.ArrayUtil;
     29 import com.android.tradefed.util.StreamUtil;
     30 
     31 import com.google.common.annotations.VisibleForTesting;
     32 
     33 import org.json.JSONException;
     34 import org.json.JSONObject;
     35 
     36 import java.io.BufferedReader;
     37 import java.io.IOException;
     38 import java.io.InputStreamReader;
     39 import java.io.PrintWriter;
     40 import java.net.ServerSocket;
     41 import java.net.Socket;
     42 import java.net.SocketException;
     43 import java.net.SocketTimeoutException;
     44 
     45 /**
     46  * Class that receives {@link com.android.tradefed.command.remote.RemoteOperation}s via a socket.
     47  * <p/>
     48  * Currently accepts only one remote connection at one time, and processes incoming commands
     49  * serially.
     50  * <p/>
     51  * Usage:
     52  * <pre>
     53  * RemoteManager r = new RemoteManager(deviceMgr, scheduler);
     54  * r.connect();
     55  * r.start();
     56  * int port = r.getPort();
     57  * ... inform client of port to use. Shuts down when instructed by client or on #cancel()
     58  * </pre>
     59  */
     60 @OptionClass(alias = "remote-manager")
     61 public class RemoteManager extends Thread {
     62 
     63     private ServerSocket mServerSocket = null;
     64     private boolean mCancel = false;
     65     private final IDeviceManager mDeviceManager;
     66     private final ICommandScheduler mScheduler;
     67 
     68     @Option(name = "start-remote-mgr",
     69             description = "Whether or not to start a remote manager on boot.")
     70     private static boolean mStartRemoteManagerOnBoot = false;
     71 
     72     @Option(name = "auto-handover",
     73             description = "Whether or not to start handover if there is another instance of " +
     74                           "Tradefederation running on the machine")
     75     private static boolean mAutoHandover = false;
     76 
     77     @Option(name = "remote-mgr-port",
     78             description = "The remote manager port to use.")
     79     private static int mRemoteManagerPort = RemoteClient.DEFAULT_PORT;
     80 
     81     @Option(name = "remote-mgr-socket-timeout-ms",
     82             description = "Timeout for when accepting connections with the remote manager socket.")
     83     private static int mSocketTimeout = 2000;
     84 
     85     public boolean getStartRemoteMgrOnBoot() {
     86         return mStartRemoteManagerOnBoot;
     87     }
     88 
     89     public int getRemoteManagerPort() {
     90         return mRemoteManagerPort;
     91     }
     92 
     93     public void setRemoteManagerPort(int port) {
     94         mRemoteManagerPort = port;
     95     }
     96 
     97     public void setRemoteManagerTimeout(int timeout) {
     98         mSocketTimeout = timeout;
     99     }
    100 
    101     public boolean getAutoHandover() {
    102         return mAutoHandover;
    103     }
    104 
    105     public RemoteManager() {
    106         super("RemoteManager");
    107         mDeviceManager = null;
    108         mScheduler = null;
    109     }
    110 
    111     /**
    112      * Creates a {@link RemoteManager}.
    113      *
    114      * @param manager the {@link IDeviceManager} to use to allocate and free devices.
    115      * @param scheduler the {@link ICommandScheduler} to use to schedule commands.
    116      */
    117     public RemoteManager(IDeviceManager manager, ICommandScheduler scheduler) {
    118         super("RemoteManager");
    119         mDeviceManager = manager;
    120         mScheduler = scheduler;
    121     }
    122 
    123     /**
    124      * Attempts to init server and connect it to a port.
    125      * @return true if we successfully connect the server to the default port.
    126      */
    127     public boolean connect() {
    128         return connect(mRemoteManagerPort);
    129     }
    130 
    131     /**
    132      * Attemps to connect to any free port.
    133      * @return true if we successfully connected to the port, false otherwise.
    134      */
    135     public boolean connectAnyPort() {
    136         return connect(0);
    137     }
    138 
    139     /**
    140      * Attempts to connect server to a given port.
    141      * @return true if we successfully connect to the port, false otherwise.
    142      */
    143     protected boolean connect(int port) {
    144         mServerSocket = openSocket(port);
    145         return mServerSocket != null;
    146     }
    147 
    148     /**
    149      * Attempts to open server socket at given port.
    150      * @param port to open the socket at.
    151      * @return the ServerSocket or null if attempt failed.
    152      */
    153     private ServerSocket openSocket(int port) {
    154         try {
    155             return new ServerSocket(port);
    156         } catch (IOException e) {
    157             // avoid printing a scary stack that is due to handover.
    158             CLog.w(
    159                     "Failed to open server socket: %s. Probably due to another instance of TF "
    160                             + "running.",
    161                     e.getMessage());
    162             return null;
    163         }
    164     }
    165 
    166 
    167     /**
    168      * The main thread body of the remote manager.
    169      * <p/>
    170      * Creates a server socket, and waits for client connections.
    171      */
    172     @Override
    173     public void run() {
    174         if (mServerSocket == null) {
    175             CLog.e("Started remote manager thread without connecting");
    176             return;
    177         }
    178         try {
    179             // Set a timeout as we don't want to be blocked listening for connections,
    180             // we could receive a request for cancel().
    181             mServerSocket.setSoTimeout(mSocketTimeout);
    182             processClientConnections(mServerSocket);
    183         } catch (SocketException e) {
    184             CLog.e("Error when setting socket timeout");
    185             CLog.e(e);
    186         } finally {
    187             freeAllDevices();
    188             closeSocket(mServerSocket);
    189         }
    190     }
    191 
    192     /**
    193      * Gets the socket port the remote manager is listening on, blocking for a short time if
    194      * necessary.
    195      * <p/>
    196      * {@link #start()} should be called before this method.
    197      * @return the port the remote manager is listening on, or -1 if no port is setup.
    198      */
    199     public synchronized int getPort() {
    200         if (mServerSocket == null) {
    201             try {
    202                 wait(10*1000);
    203             } catch (InterruptedException e) {
    204                 // ignore
    205             }
    206         }
    207         if (mServerSocket == null) {
    208             return -1;
    209         }
    210         return mServerSocket.getLocalPort();
    211     }
    212 
    213     private void processClientConnections(ServerSocket serverSocket) {
    214         while (!mCancel) {
    215             Socket clientSocket = null;
    216             BufferedReader in = null;
    217             PrintWriter out = null;
    218             try {
    219                 clientSocket = serverSocket.accept();
    220                 in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    221                 out = new PrintWriter(clientSocket.getOutputStream(), true);
    222                 processClientOperations(in, out);
    223             } catch (SocketTimeoutException e) {
    224                 // ignore.
    225             } catch (IOException e) {
    226                 CLog.e("Failed to accept connection");
    227                 CLog.e(e);
    228             } finally {
    229                 closeReader(in);
    230                 closeWriter(out);
    231                 closeSocket(clientSocket);
    232             }
    233         }
    234     }
    235 
    236     /**
    237      * Process {@link com.android.tradefed.command.remote.RemoteClient} operations.
    238      *
    239      * @param in the {@link BufferedReader} coming from the client socket.
    240      * @param out the {@link PrintWriter} to write to the client socket.
    241      * @throws IOException
    242      */
    243     @VisibleForTesting
    244     void processClientOperations(BufferedReader in, PrintWriter out) throws IOException {
    245         String line = null;
    246         while ((line = in.readLine()) != null && !mCancel) {
    247             JSONObject result = new JSONObject();
    248             RemoteOperation<?> rc;
    249             Thread postOp = null;
    250             try {
    251                 rc = RemoteOperation.createRemoteOpFromString(line);
    252                 switch (rc.getType()) {
    253                     case ADD_COMMAND:
    254                         processAdd((AddCommandOp)rc, result);
    255                         break;
    256                     case ADD_COMMAND_FILE:
    257                         processAddCommandFile((AddCommandFileOp)rc, result);
    258                         break;
    259                     case CLOSE:
    260                         processClose((CloseOp)rc, result);
    261                         break;
    262                     case ALLOCATE_DEVICE:
    263                         processAllocate((AllocateDeviceOp)rc, result);
    264                         break;
    265                     case FREE_DEVICE:
    266                         processFree((FreeDeviceOp)rc, result);
    267                         break;
    268                     case START_HANDOVER:
    269                         postOp = processStartHandover((StartHandoverOp)rc, result);
    270                         break;
    271                     case HANDOVER_INIT_COMPLETE:
    272                         processHandoverInitComplete((HandoverInitCompleteOp)rc, result);
    273                         break;
    274                     case HANDOVER_COMPLETE:
    275                         postOp = processHandoverComplete((HandoverCompleteOp)rc, result);
    276                         break;
    277                     case LIST_DEVICES:
    278                         processListDevices((ListDevicesOp)rc, result);
    279                         break;
    280                     case EXEC_COMMAND:
    281                         processExecCommand((ExecCommandOp)rc, result);
    282                         break;
    283                     case GET_LAST_COMMAND_RESULT:
    284                         processGetLastCommandResult((GetLastCommandResultOp)rc, result);
    285                         break;
    286                     default:
    287                         result.put(RemoteOperation.ERROR, "Unrecognized operation");
    288                         break;
    289                 }
    290             } catch (RemoteException e) {
    291                 addErrorToResult(result, e);
    292             } catch (JSONException e) {
    293                 addErrorToResult(result, e);
    294             } catch (RuntimeException e) {
    295                 addErrorToResult(result, e);
    296             }
    297             sendAck(result, out);
    298             if (postOp != null) {
    299                 postOp.start();
    300             }
    301         }
    302     }
    303 
    304     private void addErrorToResult(JSONObject result, Exception e) {
    305         try {
    306             CLog.e("Failed to handle remote command");
    307             CLog.e(e);
    308             result.put(RemoteOperation.ERROR, "Failed to handle remote command: " +
    309                     e.toString());
    310         } catch (JSONException e1) {
    311             CLog.e("Failed to build json remote response");
    312             CLog.e(e1);
    313         }
    314     }
    315 
    316     private void processListDevices(ListDevicesOp rc, JSONObject result) {
    317         try {
    318             rc.packResponseIntoJson(mDeviceManager.listAllDevices(), result);
    319         } catch (JSONException e) {
    320             addErrorToResult(result, e);
    321         }
    322     }
    323 
    324     @VisibleForTesting
    325     DeviceTracker getDeviceTracker() {
    326         return DeviceTracker.getInstance();
    327     }
    328 
    329     private Thread processStartHandover(StartHandoverOp c, JSONObject result) {
    330         final int port = c.getPort();
    331         CLog.logAndDisplay(LogLevel.INFO, "Performing handover to remote TF at port %d", port);
    332         // handle the handover as an async operation
    333         Thread t = new Thread("handover thread") {
    334             @Override
    335             public void run() {
    336                 if (!mScheduler.handoverShutdown(port)) {
    337                     // TODO: send handover failed
    338                 }
    339             }
    340         };
    341         return t;
    342     }
    343 
    344     private void processHandoverInitComplete(HandoverInitCompleteOp c, JSONObject result) {
    345         CLog.logAndDisplay(LogLevel.INFO, "Received handover complete.");
    346         mScheduler.handoverInitiationComplete();
    347     }
    348 
    349     private Thread processHandoverComplete(HandoverCompleteOp c, JSONObject result) {
    350         // handle the handover as an async operation
    351         Thread t = new Thread("handover thread") {
    352             @Override
    353             public void run() {
    354                 mScheduler.completeHandover();
    355             }
    356         };
    357         return t;
    358     }
    359 
    360     private void processAllocate(AllocateDeviceOp c, JSONObject result) throws JSONException {
    361         ITestDevice allocatedDevice = mDeviceManager.forceAllocateDevice(c.getDeviceSerial());
    362         if (allocatedDevice != null) {
    363             CLog.logAndDisplay(LogLevel.INFO, "Remotely allocating device %s", c.getDeviceSerial());
    364             getDeviceTracker().allocateDevice(allocatedDevice);
    365         } else {
    366             String msg = "Failed to allocate device " + c.getDeviceSerial();
    367             CLog.e(msg);
    368             result.put(RemoteOperation.ERROR, msg);
    369         }
    370     }
    371 
    372     private void processFree(FreeDeviceOp c, JSONObject result) throws JSONException {
    373         if (FreeDeviceOp.ALL_DEVICES.equals(c.getDeviceSerial())) {
    374             freeAllDevices();
    375         } else {
    376             ITestDevice d = getDeviceTracker().freeDevice(c.getDeviceSerial());
    377             if (d != null) {
    378                 CLog.logAndDisplay(LogLevel.INFO,
    379                         "Remotely freeing device %s",
    380                                 c.getDeviceSerial());
    381                 mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE);
    382             } else {
    383                 String msg = "Could not find device to free " + c.getDeviceSerial();
    384                 CLog.w(msg);
    385                 result.put(RemoteOperation.ERROR, msg);
    386             }
    387         }
    388     }
    389 
    390     private void processAdd(AddCommandOp c, JSONObject result) throws JSONException {
    391         CLog.logAndDisplay(LogLevel.INFO, "Adding command '%s'", ArrayUtil.join(" ",
    392                 (Object[])c.getCommandArgs()));
    393         try {
    394             if (!mScheduler.addCommand(c.getCommandArgs(), c.getTotalTime())) {
    395                 result.put(RemoteOperation.ERROR, "Failed to add command");
    396             }
    397         } catch (ConfigurationException e) {
    398             CLog.e("Failed to add command");
    399             CLog.e(e);
    400             result.put(RemoteOperation.ERROR, "Config error: " + e.toString());
    401         }
    402     }
    403 
    404     private void processAddCommandFile(AddCommandFileOp c, JSONObject result) throws JSONException {
    405         CLog.logAndDisplay(LogLevel.INFO, "Adding command file '%s %s'", c.getCommandFile(),
    406                 ArrayUtil.join(" ", c.getExtraArgs()));
    407         try {
    408             mScheduler.addCommandFile(c.getCommandFile(), c.getExtraArgs());
    409         } catch (ConfigurationException e) {
    410             CLog.e("Failed to add command");
    411             CLog.e(e);
    412             result.put(RemoteOperation.ERROR, "Config error: " + e.toString());
    413         }
    414     }
    415 
    416     private void processExecCommand(ExecCommandOp c, JSONObject result) throws JSONException {
    417         ITestDevice device = getDeviceTracker().getDeviceForSerial(c.getDeviceSerial());
    418         if (device == null) {
    419             String msg = String.format("Could not find remotely allocated device with serial %s",
    420                     c.getDeviceSerial());
    421             CLog.e(msg);
    422             result.put(RemoteOperation.ERROR, msg);
    423             return;
    424         }
    425         ExecCommandTracker commandResult =
    426                 getDeviceTracker().getLastCommandResult(c.getDeviceSerial());
    427         if (commandResult != null &&
    428             commandResult.getCommandResult().getStatus() == Status.EXECUTING) {
    429             String msg = String.format("Another command is already executing on %s",
    430                     c.getDeviceSerial());
    431             CLog.e(msg);
    432             result.put(RemoteOperation.ERROR, msg);
    433             return;
    434         }
    435         CLog.logAndDisplay(LogLevel.INFO, "Executing command '%s'", ArrayUtil.join(" ",
    436                 (Object[])c.getCommandArgs()));
    437         try {
    438             ExecCommandTracker tracker = new ExecCommandTracker();
    439             mScheduler.execCommand(tracker, device, c.getCommandArgs());
    440             getDeviceTracker().setCommandTracker(c.getDeviceSerial(), tracker);
    441         } catch (ConfigurationException e) {
    442             CLog.e("Failed to exec command");
    443             CLog.e(e);
    444             result.put(RemoteOperation.ERROR, "Config error: " + e.toString());
    445         }
    446     }
    447 
    448     private void processGetLastCommandResult(GetLastCommandResultOp c, JSONObject json)
    449             throws JSONException {
    450         ITestDevice device = getDeviceTracker().getDeviceForSerial(c.getDeviceSerial());
    451         ExecCommandTracker tracker = getDeviceTracker().getLastCommandResult(c.getDeviceSerial());
    452         if (device == null) {
    453             c.packResponseIntoJson(new CommandResult(CommandResult.Status.NOT_ALLOCATED), json);
    454         } else if (tracker == null) {
    455             c.packResponseIntoJson(new CommandResult(CommandResult.Status.NO_ACTIVE_COMMAND),
    456                     json);
    457         } else {
    458             c.packResponseIntoJson(tracker.getCommandResult(), json);
    459         }
    460     }
    461 
    462     private void processClose(CloseOp rc, JSONObject result) {
    463         cancel();
    464     }
    465 
    466     private void freeAllDevices() {
    467         for (ITestDevice d : getDeviceTracker().freeAll()) {
    468             CLog.logAndDisplay(LogLevel.INFO,
    469                     "Freeing device %s no longer in use by remote tradefed",
    470                             d.getSerialNumber());
    471             mDeviceManager.freeDevice(d, FreeDeviceState.AVAILABLE);
    472         }
    473     }
    474 
    475     private void sendAck(JSONObject result, PrintWriter out) {
    476         out.println(result.toString());
    477     }
    478 
    479     /**
    480      * Request to cancel the remote manager.
    481      */
    482     public synchronized void cancel() {
    483         if (!mCancel) {
    484             mCancel  = true;
    485             CLog.logAndDisplay(LogLevel.INFO, "Closing remote manager at port %d", getPort());
    486         }
    487     }
    488 
    489     /**
    490      * Convenience method to request a remote manager shutdown and wait for it to complete.
    491      */
    492     public void cancelAndWait() {
    493         cancel();
    494         try {
    495             join();
    496         } catch (InterruptedException e) {
    497             CLog.e(e);
    498         }
    499     }
    500 
    501     private void closeSocket(ServerSocket serverSocket) {
    502         StreamUtil.close(serverSocket);
    503     }
    504 
    505     private void closeSocket(Socket clientSocket) {
    506         StreamUtil.close(clientSocket);
    507     }
    508 
    509     private void closeReader(BufferedReader in) {
    510         StreamUtil.close(in);
    511     }
    512 
    513     private void closeWriter(PrintWriter out) {
    514         StreamUtil.close(out);
    515     }
    516 
    517     /**
    518      * @return <code>true</code> if a cancel has been requested
    519      */
    520     public boolean isCanceled() {
    521         return mCancel;
    522     }
    523 }
    524