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