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 
getStartRemoteMgrOnBoot()85     public boolean getStartRemoteMgrOnBoot() {
86         return mStartRemoteManagerOnBoot;
87     }
88 
getRemoteManagerPort()89     public int getRemoteManagerPort() {
90         return mRemoteManagerPort;
91     }
92 
setRemoteManagerPort(int port)93     public void setRemoteManagerPort(int port) {
94         mRemoteManagerPort = port;
95     }
96 
setRemoteManagerTimeout(int timeout)97     public void setRemoteManagerTimeout(int timeout) {
98         mSocketTimeout = timeout;
99     }
100 
getAutoHandover()101     public boolean getAutoHandover() {
102         return mAutoHandover;
103     }
104 
RemoteManager()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      */
RemoteManager(IDeviceManager manager, ICommandScheduler scheduler)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      */
connect()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      */
connectAnyPort()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      */
connect(int port)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      */
openSocket(int port)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
run()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      */
getPort()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 
processClientConnections(ServerSocket serverSocket)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
processClientOperations(BufferedReader in, PrintWriter out)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 
addErrorToResult(JSONObject result, Exception e)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 
processListDevices(ListDevicesOp rc, JSONObject result)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
getDeviceTracker()325     DeviceTracker getDeviceTracker() {
326         return DeviceTracker.getInstance();
327     }
328 
processStartHandover(StartHandoverOp c, JSONObject result)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 
processHandoverInitComplete(HandoverInitCompleteOp c, JSONObject result)344     private void processHandoverInitComplete(HandoverInitCompleteOp c, JSONObject result) {
345         CLog.logAndDisplay(LogLevel.INFO, "Received handover complete.");
346         mScheduler.handoverInitiationComplete();
347     }
348 
processHandoverComplete(HandoverCompleteOp c, JSONObject result)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 
processAllocate(AllocateDeviceOp c, JSONObject result)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 
processFree(FreeDeviceOp c, JSONObject result)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 
processAdd(AddCommandOp c, JSONObject result)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())) {
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 
processAddCommandFile(AddCommandFileOp c, JSONObject result)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 
processExecCommand(ExecCommandOp c, JSONObject result)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 
processGetLastCommandResult(GetLastCommandResultOp c, JSONObject json)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 
processClose(CloseOp rc, JSONObject result)462     private void processClose(CloseOp rc, JSONObject result) {
463         cancel();
464     }
465 
freeAllDevices()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 
sendAck(JSONObject result, PrintWriter out)475     private void sendAck(JSONObject result, PrintWriter out) {
476         out.println(result.toString());
477     }
478 
479     /**
480      * Request to cancel the remote manager.
481      */
cancel()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      */
cancelAndWait()492     public void cancelAndWait() {
493         cancel();
494         try {
495             join();
496         } catch (InterruptedException e) {
497             CLog.e(e);
498         }
499     }
500 
closeSocket(ServerSocket serverSocket)501     private void closeSocket(ServerSocket serverSocket) {
502         StreamUtil.close(serverSocket);
503     }
504 
closeSocket(Socket clientSocket)505     private void closeSocket(Socket clientSocket) {
506         StreamUtil.close(clientSocket);
507     }
508 
closeReader(BufferedReader in)509     private void closeReader(BufferedReader in) {
510         StreamUtil.close(in);
511     }
512 
closeWriter(PrintWriter out)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      */
isCanceled()520     public boolean isCanceled() {
521         return mCancel;
522     }
523 }
524