1 /* 2 * Copyright (C) 2010 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 17 package com.android.tradefed.command; 18 19 import com.android.ddmlib.DdmPreferences; 20 import com.android.ddmlib.Log; 21 import com.android.ddmlib.Log.LogLevel; 22 import com.android.tradefed.build.BuildRetrievalError; 23 import com.android.tradefed.clearcut.ClearcutClient; 24 import com.android.tradefed.command.CommandFileParser.CommandLine; 25 import com.android.tradefed.command.CommandFileWatcher.ICommandFileListener; 26 import com.android.tradefed.command.CommandRunner.ExitCode; 27 import com.android.tradefed.command.remote.DeviceDescriptor; 28 import com.android.tradefed.command.remote.IRemoteClient; 29 import com.android.tradefed.command.remote.RemoteClient; 30 import com.android.tradefed.command.remote.RemoteException; 31 import com.android.tradefed.command.remote.RemoteManager; 32 import com.android.tradefed.config.ArgsOptionParser; 33 import com.android.tradefed.config.Configuration; 34 import com.android.tradefed.config.ConfigurationDescriptor; 35 import com.android.tradefed.config.ConfigurationException; 36 import com.android.tradefed.config.ConfigurationFactory; 37 import com.android.tradefed.config.DynamicRemoteFileResolver; 38 import com.android.tradefed.config.GlobalConfiguration; 39 import com.android.tradefed.config.IConfiguration; 40 import com.android.tradefed.config.IConfigurationFactory; 41 import com.android.tradefed.config.IDeviceConfiguration; 42 import com.android.tradefed.config.IGlobalConfiguration; 43 import com.android.tradefed.config.Option; 44 import com.android.tradefed.config.RetryConfigurationFactory; 45 import com.android.tradefed.config.SandboxConfigurationFactory; 46 import com.android.tradefed.config.proxy.ProxyConfiguration; 47 import com.android.tradefed.config.proxy.TradefedDelegator; 48 import com.android.tradefed.device.DeviceAllocationState; 49 import com.android.tradefed.device.DeviceManager; 50 import com.android.tradefed.device.DeviceNotAvailableException; 51 import com.android.tradefed.device.DeviceUnresponsiveException; 52 import com.android.tradefed.device.FreeDeviceState; 53 import com.android.tradefed.device.IDeviceManager; 54 import com.android.tradefed.device.IDeviceMonitor; 55 import com.android.tradefed.device.IManagedTestDevice; 56 import com.android.tradefed.device.ITestDevice; 57 import com.android.tradefed.device.ITestDevice.RecoveryMode; 58 import com.android.tradefed.device.NoDeviceException; 59 import com.android.tradefed.device.StubDevice; 60 import com.android.tradefed.device.TestDeviceState; 61 import com.android.tradefed.invoker.IInvocationContext; 62 import com.android.tradefed.invoker.IRescheduler; 63 import com.android.tradefed.invoker.ITestInvocation; 64 import com.android.tradefed.invoker.InvocationContext; 65 import com.android.tradefed.invoker.TestInvocation; 66 import com.android.tradefed.invoker.shard.ParentShardReplicate; 67 import com.android.tradefed.log.ILogRegistry.EventType; 68 import com.android.tradefed.log.LogRegistry; 69 import com.android.tradefed.log.LogUtil.CLog; 70 import com.android.tradefed.result.ITestInvocationListener; 71 import com.android.tradefed.result.ResultForwarder; 72 import com.android.tradefed.result.suite.SuiteResultReporter; 73 import com.android.tradefed.sandbox.ISandbox; 74 import com.android.tradefed.testtype.IRemoteTest; 75 import com.android.tradefed.testtype.suite.retry.RetryRescheduler; 76 import com.android.tradefed.util.ArrayUtil; 77 import com.android.tradefed.util.FileUtil; 78 import com.android.tradefed.util.QuotationAwareTokenizer; 79 import com.android.tradefed.util.RunUtil; 80 import com.android.tradefed.util.StreamUtil; 81 import com.android.tradefed.util.TableFormatter; 82 import com.android.tradefed.util.TimeUtil; 83 import com.android.tradefed.util.hostmetric.IHostMonitor; 84 import com.android.tradefed.util.hostmetric.IHostMonitor.HostDataPoint; 85 import com.android.tradefed.util.hostmetric.IHostMonitor.HostMetricType; 86 import com.android.tradefed.util.keystore.IKeyStoreClient; 87 import com.android.tradefed.util.keystore.IKeyStoreFactory; 88 import com.android.tradefed.util.keystore.KeyStoreException; 89 90 import com.google.common.annotations.VisibleForTesting; 91 import com.google.common.collect.ImmutableSet; 92 93 import java.io.File; 94 import java.io.IOException; 95 import java.io.PrintWriter; 96 import java.util.ArrayList; 97 import java.util.Arrays; 98 import java.util.Collections; 99 import java.util.Comparator; 100 import java.util.HashMap; 101 import java.util.HashSet; 102 import java.util.Iterator; 103 import java.util.LinkedHashMap; 104 import java.util.LinkedHashSet; 105 import java.util.LinkedList; 106 import java.util.List; 107 import java.util.Map; 108 import java.util.Set; 109 import java.util.Timer; 110 import java.util.TimerTask; 111 import java.util.concurrent.CountDownLatch; 112 import java.util.concurrent.ScheduledThreadPoolExecutor; 113 import java.util.concurrent.TimeUnit; 114 import java.util.regex.Pattern; 115 116 /** 117 * A scheduler for running TradeFederation commands across all available devices. 118 * <p/> 119 * Will attempt to prioritize commands to run based on a total running count of their execution 120 * time. e.g. infrequent or fast running commands will get prioritized over long running commands. 121 * <p/> 122 * Runs forever in background until shutdown. 123 */ 124 public class CommandScheduler extends Thread implements ICommandScheduler, ICommandFileListener { 125 126 /** the queue of commands ready to be executed. */ 127 private List<ExecutableCommand> mReadyCommands; 128 private Set<ExecutableCommand> mUnscheduledWarning; 129 130 /** the queue of commands sleeping. */ 131 private Set<ExecutableCommand> mSleepingCommands; 132 133 /** the queue of commands current executing. */ 134 private Set<ExecutableCommand> mExecutingCommands; 135 136 /** map of device to active invocation threads */ 137 private Map<IInvocationContext, InvocationThread> mInvocationThreadMap; 138 139 /** timer for scheduling commands to be re-queued for execution */ 140 private ScheduledThreadPoolExecutor mCommandTimer; 141 142 private IRemoteClient mRemoteClient = null; 143 private RemoteManager mRemoteManager = null; 144 145 private CommandFileWatcher mCommandFileWatcher = null; 146 147 /** latch used to notify other threads that this thread is running */ 148 private final CountDownLatch mRunLatch; 149 150 /** maximum time to wait for handover initiation to complete */ 151 private static final long MAX_HANDOVER_INIT_TIME = 2 * 60 * 1000; 152 153 /** Maximum time to wait for adb to initialize and get the physical devices discovered */ 154 private static final long ADB_INIT_TIME_MS = 500; 155 156 /** used to assign unique ids to each CommandTracker created */ 157 private int mCurrentCommandId = 0; 158 159 /** flag for instructing scheduler to exit when no commands are present */ 160 private boolean mShutdownOnEmpty = false; 161 162 private boolean mStarted = false; 163 164 // flag to indicate this scheduler is currently handing over control to another remote TF 165 private boolean mPerformingHandover = false; 166 167 private WaitObj mHandoverHandshake = new WaitObj(); 168 169 private WaitObj mCommandProcessWait = new WaitObj(); 170 171 /** The last {@link InvocationThread} that ran error code and error stack*/ 172 private ExitCode mLastInvocationExitCode = ExitCode.NO_ERROR; 173 private Throwable mLastInvocationThrowable = null; 174 175 /** Client to report metric data of the harness. */ 176 private ClearcutClient mClient = null; 177 178 @Option(name = "reload-cmdfiles", description = 179 "Whether to enable the command file autoreload mechanism") 180 // FIXME: enable this to be enabled or disabled on a per-cmdfile basis 181 private boolean mReloadCmdfiles = false; 182 183 @Option( 184 name = "max-poll-time", 185 description = "ms between forced command scheduler execution time" 186 ) 187 private long mPollTime = 30 * 1000; // 30 seconds 188 189 @Option(name = "shutdown-on-cmdfile-error", description = 190 "terminate TF session if a configuration exception on command file occurs") 191 private boolean mShutdownOnCmdfileError = false; 192 193 @Option(name = "shutdown-delay", description = 194 "maximum time to wait before allowing final interruption of scheduler to terminate" 195 + " TF session. If value is zero, interruption will only be triggered" 196 + " when Invocation become interruptible. (Default behavior).", isTimeVal = true) 197 private long mShutdownTimeout = 0; 198 199 private HostState mHostState = HostState.UNKNOWN; 200 201 private enum CommandState { 202 WAITING_FOR_DEVICE("Wait_for_device"), 203 EXECUTING("Executing"), 204 SLEEPING("Sleeping"); 205 206 private final String mDisplayName; 207 CommandState(String displayName)208 CommandState(String displayName) { 209 mDisplayName = displayName; 210 } 211 getDisplayName()212 public String getDisplayName() { 213 return mDisplayName; 214 } 215 } 216 217 /** Enums of different status of host */ 218 public enum HostState { 219 UNKNOWN, 220 RUNNING, 221 QUITTING, 222 KILLING; 223 } 224 setHostState(HostState state)225 private void setHostState(HostState state) { 226 mHostState = state; 227 } 228 getHostState()229 public HostState getHostState() { 230 return mHostState; 231 } 232 233 /** 234 * Represents one active command added to the scheduler. Will track total execution time of all 235 * instances of this command 236 */ 237 static class CommandTracker { 238 private final int mId; 239 private final String[] mArgs; 240 private final String mCommandFilePath; 241 242 /** the total amount of time this command was executing. Used to prioritize */ 243 private long mTotalExecTime = 0; 244 CommandTracker(int id, String[] args, String commandFilePath)245 CommandTracker(int id, String[] args, String commandFilePath) { 246 mId = id; 247 mArgs = args; 248 mCommandFilePath = commandFilePath; 249 } 250 incrementExecTime(long execTime)251 synchronized void incrementExecTime(long execTime) { 252 mTotalExecTime += execTime; 253 } 254 255 /** 256 * @return the total amount of execution time for this command. 257 */ getTotalExecTime()258 synchronized long getTotalExecTime() { 259 return mTotalExecTime; 260 } 261 262 /** 263 * Get the full list of config arguments associated with this command. 264 */ getArgs()265 String[] getArgs() { 266 return mArgs; 267 } 268 getId()269 int getId() { 270 return mId; 271 } 272 273 /** 274 * Return the path of the file this command is associated with. null if not applicable 275 */ getCommandFilePath()276 String getCommandFilePath() { 277 return mCommandFilePath; 278 } 279 } 280 281 /** 282 * Represents one instance of a command to be executed. 283 */ 284 private class ExecutableCommand { 285 private final CommandTracker mCmdTracker; 286 private final IConfiguration mConfig; 287 private final boolean mRescheduled; 288 private final long mCreationTime; 289 private Long mSleepTime; 290 ExecutableCommand(CommandTracker tracker, IConfiguration config, boolean rescheduled)291 private ExecutableCommand(CommandTracker tracker, IConfiguration config, 292 boolean rescheduled) { 293 mConfig = config; 294 mCmdTracker = tracker; 295 mRescheduled = rescheduled; 296 mCreationTime = System.currentTimeMillis(); 297 } 298 299 /** 300 * Gets the {@link IConfiguration} for this command instance 301 */ getConfiguration()302 public IConfiguration getConfiguration() { 303 return mConfig; 304 } 305 306 /** 307 * Gets the associated {@link CommandTracker}. 308 */ getCommandTracker()309 CommandTracker getCommandTracker() { 310 return mCmdTracker; 311 } 312 313 /** 314 * Callback to inform listener that command has started execution. 315 */ commandStarted()316 void commandStarted() { 317 mSleepTime = null; 318 } 319 commandFinished(long elapsedTime)320 public void commandFinished(long elapsedTime) { 321 getCommandTracker().incrementExecTime(elapsedTime); 322 CLog.d("removing exec command for id %d", getCommandTracker().getId()); 323 synchronized (CommandScheduler.this) { 324 mExecutingCommands.remove(this); 325 } 326 if (isShuttingDown()) { 327 mCommandProcessWait.signalEventReceived(); 328 } 329 } 330 isRescheduled()331 public boolean isRescheduled() { 332 return mRescheduled; 333 } 334 getCreationTime()335 public long getCreationTime() { 336 return mCreationTime; 337 } 338 isLoopMode()339 public boolean isLoopMode() { 340 return mConfig.getCommandOptions().isLoopMode(); 341 } 342 getSleepTime()343 public Long getSleepTime() { 344 return mSleepTime; 345 } 346 getCommandFilePath()347 public String getCommandFilePath() { 348 return mCmdTracker.getCommandFilePath(); 349 } 350 } 351 352 /** 353 * struct for a command and its state 354 */ 355 private static class ExecutableCommandState { 356 final ExecutableCommand cmd; 357 final CommandState state; 358 ExecutableCommandState(ExecutableCommand cmd, CommandState state)359 ExecutableCommandState(ExecutableCommand cmd, CommandState state) { 360 this.cmd = cmd; 361 this.state = state; 362 } 363 } 364 365 /** 366 * A {@link IRescheduler} that will add a command back to the queue. 367 */ 368 private class Rescheduler implements IRescheduler { 369 370 private CommandTracker mCmdTracker; 371 Rescheduler(CommandTracker cmdTracker)372 Rescheduler(CommandTracker cmdTracker) { 373 mCmdTracker = cmdTracker; 374 } 375 376 /** 377 * {@inheritDoc} 378 */ 379 @Override scheduleConfig(IConfiguration config)380 public boolean scheduleConfig(IConfiguration config) { 381 // force loop mode to off, otherwise each rescheduled config will be treated as 382 // a new command and added back to queue 383 config.getCommandOptions().setLoopMode(false); 384 ExecutableCommand rescheduledCmd = createExecutableCommand(mCmdTracker, config, true); 385 return addExecCommandToQueue(rescheduledCmd, 0); 386 } 387 388 /** 389 * {@inheritDoc} 390 */ 391 @Override rescheduleCommand()392 public boolean rescheduleCommand() { 393 try { 394 CLog.d("rescheduling for command %d", mCmdTracker.getId()); 395 IConfiguration config = getConfigFactory().createConfigurationFromArgs( 396 mCmdTracker.getArgs()); 397 ExecutableCommand execCmd = createExecutableCommand(mCmdTracker, config, true); 398 return addExecCommandToQueue(execCmd, config.getCommandOptions().getLoopTime()); 399 } catch (ConfigurationException e) { 400 // FIXME: do this with jline somehow for ANSI support 401 // note: make sure not to log (aka record) this line, as (args) may contain 402 // passwords. 403 System.out.println(String.format("Error while processing args: %s", 404 Arrays.toString(mCmdTracker.getArgs()))); 405 System.out.println(e.getMessage()); 406 System.out.println(); 407 return false; 408 } 409 } 410 } 411 412 /** 413 * Comparator for {@link ExecutableCommand}. 414 * <p/> 415 * Delegates to {@link CommandTrackerTimeComparator}. 416 */ 417 private static class ExecutableCommandComparator implements Comparator<ExecutableCommand> { 418 CommandTrackerTimeComparator mTrackerComparator = new CommandTrackerTimeComparator(); 419 420 /** 421 * {@inheritDoc} 422 */ 423 @Override compare(ExecutableCommand c1, ExecutableCommand c2)424 public int compare(ExecutableCommand c1, ExecutableCommand c2) { 425 return mTrackerComparator.compare(c1.getCommandTracker(), c2.getCommandTracker()); 426 } 427 } 428 429 /** 430 * Comparator for {@link CommandTracker}. 431 * <p/> 432 * Compares by mTotalExecTime, prioritizing configs with lower execution time 433 */ 434 private static class CommandTrackerTimeComparator implements Comparator<CommandTracker> { 435 436 @Override compare(CommandTracker c1, CommandTracker c2)437 public int compare(CommandTracker c1, CommandTracker c2) { 438 if (c1.getTotalExecTime() == c2.getTotalExecTime()) { 439 return 0; 440 } else if (c1.getTotalExecTime() < c2.getTotalExecTime()) { 441 return -1; 442 } else { 443 return 1; 444 } 445 } 446 } 447 448 /** 449 * Comparator for {@link CommandTracker}. 450 * <p/> 451 * Compares by id. 452 */ 453 static class CommandTrackerIdComparator implements Comparator<CommandTracker> { 454 455 @Override compare(CommandTracker c1, CommandTracker c2)456 public int compare(CommandTracker c1, CommandTracker c2) { 457 if (c1.getId() == c2.getId()) { 458 return 0; 459 } else if (c1.getId() < c2.getId()) { 460 return -1; 461 } else { 462 return 1; 463 } 464 } 465 } 466 467 /** 468 * An {@link com.android.tradefed.command.ICommandScheduler.IScheduledInvocationListener} 469 * for locally scheduled commands added via addCommand. 470 * <p/> 471 * Returns device to device manager and remote handover server if applicable. 472 */ 473 private class FreeDeviceHandler extends ResultForwarder implements 474 IScheduledInvocationListener { 475 476 private final IDeviceManager mDeviceManager; 477 private boolean mDeviceReleased = false; 478 FreeDeviceHandler(IDeviceManager deviceManager, IScheduledInvocationListener... listeners)479 FreeDeviceHandler(IDeviceManager deviceManager, 480 IScheduledInvocationListener... listeners) { 481 super(listeners); 482 mDeviceManager = deviceManager; 483 } 484 485 @Override releaseDevices( IInvocationContext context, Map<ITestDevice, FreeDeviceState> devicesStates)486 public void releaseDevices( 487 IInvocationContext context, Map<ITestDevice, FreeDeviceState> devicesStates) { 488 if (mDeviceReleased) { 489 return; 490 } 491 for (ITestDevice device : context.getDevices()) { 492 mDeviceManager.freeDevice(device, devicesStates.get(device)); 493 remoteFreeDevice(device); 494 if (device instanceof IManagedTestDevice) { 495 // This quite an important setting so we do make sure it's reset. 496 ((IManagedTestDevice)device).setFastbootPath(mDeviceManager.getFastbootPath()); 497 } 498 } 499 mDeviceReleased = true; 500 } 501 502 @Override invocationComplete( IInvocationContext context, Map<ITestDevice, FreeDeviceState> devicesStates)503 public void invocationComplete( 504 IInvocationContext context, Map<ITestDevice, FreeDeviceState> devicesStates) { 505 for (ITestInvocationListener listener : getListeners()) { 506 try { 507 ((IScheduledInvocationListener) listener) 508 .invocationComplete(context, devicesStates); 509 } catch (Exception e) { 510 CLog.e("Exception during invocationComplete:"); 511 CLog.e(e); 512 } 513 } 514 releaseDevices(context, devicesStates); 515 } 516 } 517 518 /** 519 * Monitor Class for {@link InvocationThread} to make sure it does not run for too long. 520 */ 521 private class InvocationThreadMonitor extends TimerTask { 522 523 private InvocationThread mInvocationThread = null; 524 private boolean mTriggered = false; 525 InvocationThreadMonitor(InvocationThread toMonitor)526 public InvocationThreadMonitor(InvocationThread toMonitor) { 527 mInvocationThread = toMonitor; 528 } 529 530 @Override run()531 public void run() { 532 if (mInvocationThread != null) { 533 mTriggered = true; 534 mInvocationThread.stopInvocation("Invocation Timeout Reached."); 535 } 536 } 537 isTriggered()538 public boolean isTriggered() { 539 return mTriggered; 540 } 541 } 542 543 private class InvocationThread extends Thread { 544 545 private static final int EXPECTED_THREAD_COUNT = 1; 546 private static final String INVOC_END_EVENT_ID_KEY = "id"; 547 private static final String INVOC_END_EVENT_ELAPSED_KEY = "elapsed-time"; 548 private static final String INVOC_END_EVENT_TAG_KEY = "test-tag"; 549 550 private final IScheduledInvocationListener[] mListeners; 551 private final IInvocationContext mInvocationContext; 552 private final ExecutableCommand mCmd; 553 private final ITestInvocation mInvocation; 554 private final InvocationThreadMonitor mInvocationThreadMonitor; 555 private final Timer mExecutionTimer; 556 private long mStartTime = -1; 557 InvocationThread(String name, IInvocationContext invocationContext, ExecutableCommand command, IScheduledInvocationListener... listeners)558 public InvocationThread(String name, IInvocationContext invocationContext, 559 ExecutableCommand command, IScheduledInvocationListener... listeners) { 560 // create a thread group so LoggerRegistry can identify this as an invocationThread 561 super(new ThreadGroup(name), name); 562 mListeners = listeners; 563 mInvocationContext = invocationContext; 564 mCmd = command; 565 mInvocation = createRunInstance(); 566 567 // Daemon timer 568 mExecutionTimer = new Timer(true); 569 mInvocationThreadMonitor = new InvocationThreadMonitor(this); 570 } 571 getStartTime()572 public long getStartTime() { 573 return mStartTime; 574 } 575 576 @Override run()577 public void run() { 578 if (mClient != null) { 579 mClient.notifyTradefedInvocationStartEvent(); 580 } 581 mStartTime = System.currentTimeMillis(); 582 ITestInvocation instance = getInvocation(); 583 IConfiguration config = mCmd.getConfiguration(); 584 585 for (final IScheduledInvocationListener listener : mListeners) { 586 try { 587 listener.invocationInitiated(mInvocationContext); 588 } catch (Throwable anyException) { 589 CLog.e("Exception caught while calling invocationInitiated:"); 590 CLog.e(anyException); 591 } 592 } 593 594 Exception trackDeviceException = null; 595 try { 596 // Copy the command options invocation attributes to the invocation if it has not 597 // been already done. 598 if (!config.getConfigurationDescription().shouldUseSandbox() 599 && !config.getCommandOptions().getInvocationData().isEmpty()) { 600 mInvocationContext.addInvocationAttributes( 601 config.getCommandOptions().getInvocationData()); 602 } 603 mCmd.commandStarted(); 604 long invocTimeout = config.getCommandOptions().getInvocationTimeout(); 605 if (invocTimeout > 0) { 606 CLog.i("Setting a timer for the invocation in %sms", invocTimeout); 607 mExecutionTimer.schedule(mInvocationThreadMonitor, invocTimeout); 608 } 609 instance.invoke(mInvocationContext, config, 610 new Rescheduler(mCmd.getCommandTracker()), mListeners); 611 } catch (DeviceUnresponsiveException e) { 612 CLog.w("Device %s is unresponsive. Reason: %s", e.getSerial(), e.getMessage()); 613 trackDeviceException = e; 614 setLastInvocationExitCode(ExitCode.DEVICE_UNRESPONSIVE, e); 615 } catch (DeviceNotAvailableException e) { 616 CLog.w("Device %s is not available. Reason: %s", e.getSerial(), e.getMessage()); 617 trackDeviceException = e; 618 setLastInvocationExitCode(ExitCode.DEVICE_UNAVAILABLE, e); 619 } catch (FatalHostError e) { 620 CLog.wtf(String.format("Fatal error occurred: %s, shutting down", e.getMessage()), 621 e); 622 setLastInvocationExitCode(ExitCode.FATAL_HOST_ERROR, e); 623 shutdown(); 624 } catch (Throwable e) { 625 setLastInvocationExitCode(ExitCode.THROWABLE_EXCEPTION, e); 626 CLog.e(e); 627 } finally { 628 mExecutionTimer.cancel(); 629 if (mInvocationThreadMonitor.isTriggered()) { 630 CLog.e("Invocation reached its timeout. Cleaning up."); 631 } 632 long elapsedTime = System.currentTimeMillis() - mStartTime; 633 CLog.i("Updating command %d with elapsed time %d ms", 634 mCmd.getCommandTracker().getId(), elapsedTime); 635 // remove invocation thread first so another invocation can be started on device 636 // when freed 637 removeInvocationThread(this); 638 639 checkStrayThreads(); 640 641 Map<ITestDevice, FreeDeviceState> deviceStates = 642 createReleaseMap(mInvocationContext, trackDeviceException); 643 for (final IScheduledInvocationListener listener : mListeners) { 644 try { 645 listener.invocationComplete(mInvocationContext, deviceStates); 646 } catch (Throwable anyException) { 647 CLog.e("Exception caught while calling invocationComplete:"); 648 CLog.e(anyException); 649 } 650 } 651 mCmd.commandFinished(elapsedTime); 652 logInvocationEndedEvent( 653 mCmd.getCommandTracker().getId(), elapsedTime, mInvocationContext); 654 } 655 } 656 657 /** Check the number of thread in the ThreadGroup, only one should exists (itself). */ checkStrayThreads()658 private void checkStrayThreads() { 659 int numThread = this.getThreadGroup().activeCount(); 660 if (numThread == EXPECTED_THREAD_COUNT) { 661 // No stray thread detected at the end of invocation 662 return; 663 } 664 List<String> cmd = Arrays.asList(mCmd.getCommandTracker().getArgs()); 665 CLog.e( 666 "Stray thread detected for command %d, %s. %d threads instead of %d", 667 mCmd.getCommandTracker().getId(), cmd, numThread, EXPECTED_THREAD_COUNT); 668 // This is the best we have for debug, it prints to std out. 669 Thread[] listThreads = new Thread[numThread]; 670 this.getThreadGroup().enumerate(listThreads); 671 CLog.e("List of remaining threads: %s", Arrays.asList(listThreads)); 672 List<IHostMonitor> hostMonitors = GlobalConfiguration.getHostMonitorInstances(); 673 if (hostMonitors != null) { 674 for (IHostMonitor hm : hostMonitors) { 675 HostDataPoint data = new HostDataPoint("numThread", numThread, cmd.toString()); 676 hm.addHostEvent(HostMetricType.INVOCATION_STRAY_THREAD, data); 677 } 678 } 679 // printing to stderr will help to catch them. 680 System.err.println( 681 String.format( 682 "We have %s threads instead of 1: %s. Check the logs for list of " 683 + "threads.", 684 numThread, Arrays.asList(listThreads))); 685 } 686 687 /** Helper to log an invocation ended event. */ logInvocationEndedEvent( int invocId, long elapsedTime, final IInvocationContext context)688 private void logInvocationEndedEvent( 689 int invocId, long elapsedTime, final IInvocationContext context) { 690 Map<String, String> args = new HashMap<>(); 691 args.put(INVOC_END_EVENT_ID_KEY, Integer.toString(invocId)); 692 args.put(INVOC_END_EVENT_ELAPSED_KEY, TimeUtil.formatElapsedTime(elapsedTime)); 693 args.put(INVOC_END_EVENT_TAG_KEY, context.getTestTag()); 694 // Add all the invocation attributes to the final event logging. 695 // UniqueMultiMap cannot be easily converted to Map so we just iterate. 696 for (String key : context.getAttributes().keySet()) { 697 args.put(key, context.getAttributes().get(key).get(0)); 698 } 699 logEvent(EventType.INVOCATION_END, args); 700 } 701 getInvocation()702 ITestInvocation getInvocation() { 703 return mInvocation; 704 } 705 getInvocationContext()706 IInvocationContext getInvocationContext() { 707 return mInvocationContext; 708 } 709 710 /** 711 * Stops a running invocation. {@link CommandScheduler#shutdownHard()} will stop all running 712 * invocations. 713 */ stopInvocation(String message)714 public void stopInvocation(String message) { 715 getInvocation().notifyInvocationStopped(message); 716 for (ITestDevice device : mInvocationContext.getDevices()) { 717 if (TestDeviceState.ONLINE.equals(device.getDeviceState())) { 718 // Kill all running processes on device. 719 if (!(device.getIDevice() instanceof StubDevice)) { 720 try { 721 device.executeShellCommand("am kill-all"); 722 } catch (DeviceNotAvailableException e) { 723 CLog.e("failed to kill process on device %s", device.getSerialNumber()); 724 CLog.e(e); 725 } 726 } 727 } 728 // Finish with device tear down: We try to ensure that regardless of the invocation 729 // state during the interruption we at least do minimal tear down of devices with 730 // their built-in clean up. 731 CLog.d("Attempting postInvocationTearDown in stopInvocation"); 732 device.postInvocationTearDown(null); 733 } 734 // If invocation is not currently in an interruptible state we provide a timer 735 // after which it will become interruptible. 736 // If timeout is 0, we do not enforce future interruption. 737 if (getShutdownTimeout() != 0) { 738 RunUtil.getDefault().setInterruptibleInFuture(this, getShutdownTimeout()); 739 } 740 RunUtil.getDefault().interrupt(this, message); 741 } 742 743 /** 744 * Disable the reporting from reporters that implements a non-default 745 * {@link ITestInvocationListener#invocationInterrupted()}. 746 * Should be called on shutdown. 747 */ disableReporters()748 public void disableReporters() { 749 for (ITestInvocationListener listener : 750 mCmd.getConfiguration().getTestInvocationListeners()) { 751 listener.invocationInterrupted(); 752 } 753 } 754 755 /** 756 * Checks whether the device battery level is above the required value to keep running the 757 * invocation. 758 */ checkDeviceBatteryLevel()759 public void checkDeviceBatteryLevel() { 760 for (String deviceName : mInvocationContext.getDeviceConfigNames()) { 761 if (mCmd.getConfiguration().getDeviceConfigByName(deviceName) 762 .getDeviceOptions() == null) { 763 CLog.d("No deviceOptions in the configuration, cannot do Battery level check"); 764 return; 765 } 766 final Integer cutoffBattery = mCmd.getConfiguration() 767 .getDeviceConfigByName(deviceName).getDeviceOptions().getCutoffBattery(); 768 769 if (mInvocationContext.getDevice(deviceName) != null && cutoffBattery != null) { 770 final ITestDevice device = mInvocationContext.getDevice(deviceName); 771 Integer batteryLevel = device.getBattery(); 772 if (batteryLevel == null) { 773 return; 774 } 775 CLog.d("device %s: battery level=%d%%", device.getSerialNumber(), batteryLevel); 776 // This logic is based on the assumption that batterLevel will be 0 or -1 if TF 777 // fails to fetch a valid battery level or the device is not using a battery. 778 // So batteryLevel=0 will not trigger a stop. 779 if (0 < batteryLevel && batteryLevel < cutoffBattery) { 780 if (RunUtil.getDefault().isInterruptAllowed()) { 781 CLog.i("Stopping %s: battery too low (%d%% < %d%%)", 782 getName(), batteryLevel, cutoffBattery); 783 stopInvocation( 784 String.format( 785 "battery too low (%d%% < %d%%)", 786 batteryLevel, cutoffBattery)); 787 } else { 788 // In this case, the battery is check periodically by CommandScheduler 789 // so there will be more opportunity to terminate the invocation when 790 // it's interruptible. 791 CLog.w("device: %s has a low battery but is in uninterruptible state.", 792 device.getSerialNumber()); 793 } 794 } 795 } 796 } 797 } 798 } 799 800 /** Create a map of the devices state so they can be released appropriately. */ createReleaseMap( IInvocationContext context, Throwable e)801 public static Map<ITestDevice, FreeDeviceState> createReleaseMap( 802 IInvocationContext context, Throwable e) { 803 Map<ITestDevice, FreeDeviceState> deviceStates = new LinkedHashMap<>(); 804 for (ITestDevice device : context.getDevices()) { 805 deviceStates.put(device, FreeDeviceState.AVAILABLE); 806 } 807 808 for (ITestDevice device : context.getDevices()) { 809 if (device.getIDevice() instanceof StubDevice) { 810 // Never release stub and Tcp devices, otherwise they will disappear 811 // during deallocation since they are only placeholder. 812 deviceStates.put(device, FreeDeviceState.AVAILABLE); 813 } else { 814 TestDeviceState deviceState = device.getDeviceState(); 815 CLog.d( 816 "TestDeviceState for releasing '%s' is '%s'", 817 device.getSerialNumber(), deviceState); 818 if (!TestDeviceState.ONLINE.equals(deviceState)) { 819 // If the device is offline at the end of the test 820 deviceStates.put(device, FreeDeviceState.UNAVAILABLE); 821 } else if (!device.waitForDeviceShell(30000)) { 822 // If device cannot pass basic shell responsiveness test. 823 deviceStates.put(device, FreeDeviceState.UNAVAILABLE); 824 } 825 } 826 // Reset the recovery mode at the end of the invocation. 827 device.setRecoveryMode(RecoveryMode.AVAILABLE); 828 } 829 830 if (e instanceof DeviceUnresponsiveException) { 831 ITestDevice badDevice = 832 context.getDeviceBySerial(((DeviceUnresponsiveException) e).getSerial()); 833 if (badDevice != null && !(badDevice.getIDevice() instanceof StubDevice)) { 834 deviceStates.put(badDevice, FreeDeviceState.UNRESPONSIVE); 835 } 836 } else if (e instanceof DeviceNotAvailableException) { 837 ITestDevice badDevice = 838 context.getDeviceBySerial(((DeviceNotAvailableException) e).getSerial()); 839 if (badDevice != null && !(badDevice.getIDevice() instanceof StubDevice)) { 840 deviceStates.put(badDevice, FreeDeviceState.UNAVAILABLE); 841 } 842 } 843 return deviceStates; 844 } 845 846 /** 847 * A {@link IDeviceMonitor} that signals scheduler to process commands when an available device 848 * is added. 849 */ 850 private class AvailDeviceMonitor implements IDeviceMonitor { 851 852 @Override run()853 public void run() { 854 // ignore 855 } 856 857 @Override stop()858 public void stop() { 859 // ignore 860 } 861 862 @Override setDeviceLister(DeviceLister lister)863 public void setDeviceLister(DeviceLister lister) { 864 // ignore 865 } 866 867 @Override notifyDeviceStateChange(String serial, DeviceAllocationState oldState, DeviceAllocationState newState)868 public void notifyDeviceStateChange(String serial, DeviceAllocationState oldState, 869 DeviceAllocationState newState) { 870 if (newState.equals(DeviceAllocationState.Available)) { 871 // new avail device was added, wake up scheduler 872 mCommandProcessWait.signalEventReceived(); 873 } 874 } 875 } 876 877 /** 878 * Creates a {@link CommandScheduler}. 879 * <p /> 880 * Note: start must be called before use. 881 */ CommandScheduler()882 public CommandScheduler() { 883 super("CommandScheduler"); // set the thread name 884 mReadyCommands = new LinkedList<>(); 885 mUnscheduledWarning = new HashSet<>(); 886 mSleepingCommands = new HashSet<>(); 887 mExecutingCommands = new HashSet<>(); 888 mInvocationThreadMap = new HashMap<IInvocationContext, InvocationThread>(); 889 // use a ScheduledThreadPoolExecutorTimer as a single-threaded timer. This class 890 // is used instead of a java.util.Timer because it offers advanced shutdown options 891 mCommandTimer = new ScheduledThreadPoolExecutor(1); 892 mRunLatch = new CountDownLatch(1); 893 } 894 895 /** 896 * Starts the scheduler including setting up of logging, init of {@link DeviceManager} etc 897 */ 898 @Override start()899 public void start() { 900 synchronized (this) { 901 if (mStarted) { 902 throw new IllegalStateException("scheduler has already been started"); 903 } 904 initLogging(); 905 906 initDeviceManager(); 907 908 mStarted = true; 909 } 910 super.start(); 911 setHostState(HostState.RUNNING); 912 } 913 914 /** 915 * {@inheritDoc} 916 */ 917 @Override getCommandFileWatcher()918 public synchronized CommandFileWatcher getCommandFileWatcher() { 919 assertStarted(); 920 if (mCommandFileWatcher == null) { 921 mCommandFileWatcher = new CommandFileWatcher(this); 922 mCommandFileWatcher.start(); 923 } 924 return mCommandFileWatcher; 925 } 926 927 /** 928 * Initialize the device manager, optionally using a global device filter if specified. 929 */ initDeviceManager()930 void initDeviceManager() { 931 getDeviceManager().init(); 932 if (getDeviceManager().waitForFirstDeviceAdded(ADB_INIT_TIME_MS)) { 933 // If a first device is added we wait a short extra time to allow more devices to be 934 // discovered. 935 RunUtil.getDefault().sleep(ADB_INIT_TIME_MS); 936 } 937 } 938 939 /** 940 * Factory method for creating a {@link TestInvocation}. 941 * 942 * @return the {@link ITestInvocation} to use 943 */ createRunInstance()944 ITestInvocation createRunInstance() { 945 return new TestInvocation(); 946 } 947 948 /** 949 * Factory method for getting a reference to the {@link IDeviceManager} 950 * 951 * @return the {@link IDeviceManager} to use 952 */ getDeviceManager()953 protected IDeviceManager getDeviceManager() { 954 return GlobalConfiguration.getDeviceManagerInstance(); 955 } 956 957 /** 958 * Factory method for getting a reference to the {@link IHostMonitor} 959 * 960 * @return the {@link IHostMonitor} to use 961 */ getHostMonitor()962 List<IHostMonitor> getHostMonitor() { 963 return GlobalConfiguration.getHostMonitorInstances(); 964 } 965 966 /** 967 * Factory method for getting a reference to the {@link IConfigurationFactory} 968 * 969 * @return the {@link IConfigurationFactory} to use 970 */ getConfigFactory()971 protected IConfigurationFactory getConfigFactory() { 972 return ConfigurationFactory.getInstance(); 973 } 974 975 /** 976 * Fetches a {@link IKeyStoreClient} using the {@link IKeyStoreFactory} 977 * declared in {@link IGlobalConfiguration} or null if none is defined. 978 * @return IKeyStoreClient 979 */ getKeyStoreClient()980 protected IKeyStoreClient getKeyStoreClient() { 981 try { 982 IKeyStoreFactory f = GlobalConfiguration.getInstance().getKeyStoreFactory(); 983 if (f != null) { 984 try { 985 return f.createKeyStoreClient(); 986 } catch (KeyStoreException e) { 987 CLog.e("Failed to create key store client"); 988 CLog.e(e); 989 } 990 } 991 } catch (IllegalStateException e) { 992 CLog.w("Global configuration has not been created, failed to get keystore"); 993 CLog.e(e); 994 } 995 return null; 996 } 997 998 /** 999 * The main execution block of this thread. 1000 */ 1001 @Override run()1002 public void run() { 1003 assertStarted(); 1004 try { 1005 IDeviceManager manager = getDeviceManager(); 1006 1007 startRemoteManager(); 1008 1009 // Notify other threads that we're running. 1010 mRunLatch.countDown(); 1011 1012 // add a listener that will wake up scheduler when a new avail device is added 1013 manager.addDeviceMonitor(new AvailDeviceMonitor()); 1014 1015 while (!isShutdown()) { 1016 // wait until processing is required again 1017 mCommandProcessWait.waitAndReset(mPollTime); 1018 checkInvocations(); 1019 try { 1020 processReadyCommands(manager); 1021 } catch (RuntimeException e) { 1022 CLog.e(e); 1023 Map<String, String> information = new HashMap<>(); 1024 information.put("Exception", "CommandScheduler"); 1025 information.put("stack", StreamUtil.getStackTrace(e)); 1026 logEvent(EventType.UNEXPECTED_EXCEPTION, information); 1027 } 1028 } 1029 mCommandTimer.shutdown(); 1030 // We signal the device manager to stop device recovery threads because it could 1031 // potentially create more invocations. 1032 manager.terminateDeviceRecovery(); 1033 manager.terminateDeviceMonitor(); 1034 CLog.i("Waiting for invocation threads to complete"); 1035 waitForAllInvocationThreads(); 1036 closeRemoteClient(); 1037 if (mRemoteManager != null) { 1038 mRemoteManager.cancelAndWait(); 1039 } 1040 exit(manager); 1041 cleanUp(); 1042 CLog.logAndDisplay(LogLevel.INFO, "All done"); 1043 if (mClient != null) { 1044 mClient.stop(); 1045 } 1046 } finally { 1047 // Make sure that we don't quit with messages still in the buffers 1048 System.err.flush(); 1049 System.out.flush(); 1050 } 1051 } 1052 checkInvocations()1053 void checkInvocations() { 1054 CLog.d("Checking invocations..."); 1055 final List<InvocationThread> copy; 1056 synchronized(this) { 1057 copy = new ArrayList<InvocationThread>(mInvocationThreadMap.values()); 1058 } 1059 for (InvocationThread thread : copy) { 1060 thread.checkDeviceBatteryLevel(); 1061 } 1062 } 1063 processReadyCommands(IDeviceManager manager)1064 protected void processReadyCommands(IDeviceManager manager) { 1065 CLog.d("processReadyCommands..."); 1066 Map<ExecutableCommand, IInvocationContext> scheduledCommandMap = new HashMap<>(); 1067 // minimize length of synchronized block by just matching commands with device first, 1068 // then scheduling invocations/adding looping commands back to queue 1069 synchronized (this) { 1070 // sort ready commands by priority, so high priority commands are matched first 1071 Collections.sort(mReadyCommands, new ExecutableCommandComparator()); 1072 Iterator<ExecutableCommand> cmdIter = mReadyCommands.iterator(); 1073 while (cmdIter.hasNext()) { 1074 ExecutableCommand cmd = cmdIter.next(); 1075 IConfiguration config = cmd.getConfiguration(); 1076 IInvocationContext context = new InvocationContext(); 1077 context.setConfigurationDescriptor(config.getConfigurationDescription()); 1078 Map<String, ITestDevice> devices = allocateDevices(config, manager); 1079 if (!devices.isEmpty()) { 1080 cmdIter.remove(); 1081 mExecutingCommands.add(cmd); 1082 context.addAllocatedDevice(devices); 1083 1084 // track command matched with device 1085 scheduledCommandMap.put(cmd, context); 1086 // clean warned list to avoid piling over time. 1087 mUnscheduledWarning.remove(cmd); 1088 } else { 1089 if (!mUnscheduledWarning.contains(cmd)) { 1090 CLog.logAndDisplay(LogLevel.DEBUG, "No available device matching all the " 1091 + "config's requirements for cmd id %d.", 1092 cmd.getCommandTracker().getId()); 1093 // make sure not to record since it may contains password 1094 System.out.println( 1095 String.format( 1096 "Command will be rescheduled: %s", 1097 Arrays.toString(cmd.getCommandTracker().getArgs()))); 1098 mUnscheduledWarning.add(cmd); 1099 } 1100 } 1101 } 1102 } 1103 1104 // now actually execute the commands 1105 for (Map.Entry<ExecutableCommand, IInvocationContext> cmdDeviceEntry : scheduledCommandMap 1106 .entrySet()) { 1107 ExecutableCommand cmd = cmdDeviceEntry.getKey(); 1108 startInvocation(cmdDeviceEntry.getValue(), cmd, 1109 new FreeDeviceHandler(getDeviceManager())); 1110 if (cmd.isLoopMode()) { 1111 addNewExecCommandToQueue(cmd.getCommandTracker()); 1112 } 1113 } 1114 CLog.d("done processReadyCommands..."); 1115 } 1116 1117 /** 1118 * {@inheritDoc} 1119 */ 1120 @Override await()1121 public void await() throws InterruptedException { 1122 while (mRunLatch.getCount() > 0) { 1123 mRunLatch.await(); 1124 } 1125 } 1126 closeRemoteClient()1127 private void closeRemoteClient() { 1128 if (mRemoteClient != null) { 1129 try { 1130 mRemoteClient.sendHandoverComplete(); 1131 mRemoteClient.close(); 1132 } catch (RemoteException e) { 1133 CLog.e(e); 1134 } 1135 } 1136 } 1137 waitForThread(Thread thread)1138 private void waitForThread(Thread thread) { 1139 try { 1140 thread.join(); 1141 } catch (InterruptedException e) { 1142 // ignore 1143 waitForThread(thread); 1144 } 1145 } 1146 1147 /** Wait until all invocation threads complete. */ waitForAllInvocationThreads()1148 protected void waitForAllInvocationThreads() { 1149 List<InvocationThread> threadListCopy; 1150 synchronized (this) { 1151 threadListCopy = new ArrayList<InvocationThread>(mInvocationThreadMap.size()); 1152 threadListCopy.addAll(mInvocationThreadMap.values()); 1153 } 1154 for (Thread thread : threadListCopy) { 1155 waitForThread(thread); 1156 } 1157 } 1158 exit(IDeviceManager manager)1159 private void exit(IDeviceManager manager) { 1160 if (manager != null) { 1161 manager.terminate(); 1162 } 1163 } 1164 1165 /** 1166 * {@inheritDoc} 1167 */ 1168 @Override addCommand(String[] args)1169 public boolean addCommand(String[] args) throws ConfigurationException { 1170 return internalAddCommand(args, null); 1171 } 1172 1173 /** Returns true if {@link CommandOptions#USE_SANDBOX} is part of the command line. */ isCommandSandboxed(String[] args)1174 private boolean isCommandSandboxed(String[] args) { 1175 boolean foundSandbox = false; 1176 // Since the order is important, mark the found sandbox when we find it, and unset it if 1177 // we find the negation. 1178 for (String arg : args) { 1179 if (("--" + CommandOptions.USE_SANDBOX).equals(arg)) { 1180 foundSandbox = true; 1181 } else if (("--no-" + CommandOptions.USE_SANDBOX).equals(arg)) { 1182 foundSandbox = false; 1183 } 1184 } 1185 return foundSandbox; 1186 } 1187 isProxyCommand(String[] args)1188 private boolean isProxyCommand(String[] args) throws ConfigurationException { 1189 ProxyConfiguration proxy = new ProxyConfiguration(); 1190 ArgsOptionParser argsParser = new ArgsOptionParser(proxy); 1191 List<String> argsList = new ArrayList<>(Arrays.asList(args)); 1192 argsList.remove(0); 1193 argsParser.parseBestEffort(argsList, true); 1194 return proxy.isProxySet(); 1195 } 1196 handleProxyCommand(String[] originalArgs)1197 private IConfiguration handleProxyCommand(String[] originalArgs) throws ConfigurationException { 1198 IConfiguration config = 1199 ((ConfigurationFactory) getConfigFactory()) 1200 .createPartialConfigurationFromArgs( 1201 originalArgs, 1202 getKeyStoreClient(), 1203 ImmutableSet.of(ProxyConfiguration.PROXY_CONFIG_TYPE_KEY)); 1204 try { 1205 config.resolveDynamicOptions(new DynamicRemoteFileResolver()); 1206 } catch (BuildRetrievalError e) { 1207 throw new ConfigurationException(e.getMessage(), e); 1208 } 1209 ProxyConfiguration proxy = 1210 (ProxyConfiguration) 1211 config.getConfigurationObject(ProxyConfiguration.PROXY_CONFIG_TYPE_KEY); 1212 if (proxy.getProxyConfig() == null) { 1213 throw new ConfigurationException("No proxy configuration found."); 1214 } 1215 originalArgs[0] = proxy.getProxyConfig().getAbsolutePath(); 1216 return config; 1217 } 1218 1219 /** Returns true if the configuration used is a retry one. */ isRetryCommand(IConfiguration config)1220 private boolean isRetryCommand(IConfiguration config) { 1221 // If a configuration is made of the RetryRunner only, it is meant to run as a retry. 1222 if (config.getTests().size() != 1) { 1223 return false; 1224 } 1225 IRemoteTest rerunner = config.getTests().get(0); 1226 if (rerunner instanceof RetryRescheduler) { 1227 return true; 1228 } 1229 return false; 1230 } 1231 1232 /** Create a {@link ISandbox} that the invocation will use to run. */ createSandbox()1233 public ISandbox createSandbox() { 1234 return GlobalConfiguration.getInstance().getSandboxFactory().createSandbox(); 1235 } 1236 createConfiguration(String[] args)1237 protected IConfiguration createConfiguration(String[] args) throws ConfigurationException { 1238 TradefedDelegator delegator = checkDelegation(args); 1239 if (delegator.shouldUseDelegation()) { 1240 args = TradefedDelegator.clearCommandline(args); 1241 // Do not use delegation on staging 1242 if (!delegator.isStaging()) { 1243 delegator.setCommandLine(args); 1244 CLog.d("Using commandline arguments as starting command: %s", Arrays.asList(args)); 1245 IConfiguration config = 1246 ((ConfigurationFactory) getConfigFactory()) 1247 .createPartialConfigurationFromArgs( 1248 args, 1249 getKeyStoreClient(), 1250 ImmutableSet.of( 1251 Configuration.DEVICE_REQUIREMENTS_TYPE_NAME, 1252 Configuration.LOGGER_TYPE_NAME, 1253 Configuration.LOG_SAVER_TYPE_NAME, 1254 Configuration.RESULT_REPORTER_TYPE_NAME)); 1255 config.setConfigurationObject(TradefedDelegator.DELEGATE_OBJECT, delegator); 1256 setDelegateLevelReporting(config); 1257 return config; 1258 } 1259 } 1260 1261 // check if the command should be sandboxed 1262 if (isCommandSandboxed(args)) { 1263 // Create an sandboxed configuration based on the sandbox of the scheduler. 1264 ISandbox sandbox = createSandbox(); 1265 return SandboxConfigurationFactory.getInstance() 1266 .createConfigurationFromArgs(args, getKeyStoreClient(), sandbox, new RunUtil()); 1267 } 1268 if (isProxyCommand(args)) { 1269 IConfiguration proxyConfig = handleProxyCommand(args); 1270 String[] argsWithoutDelegation = ProxyConfiguration.clearCommandline(args); 1271 IConfiguration resolvedConfig = null; 1272 try { 1273 resolvedConfig = 1274 getConfigFactory() 1275 .createConfigurationFromArgs( 1276 argsWithoutDelegation, null, getKeyStoreClient()); 1277 } catch (ConfigurationException e) { 1278 proxyConfig.cleanConfigurationData(); 1279 throw e; 1280 } 1281 resolvedConfig.addFilesToClean(proxyConfig.getFilesToClean()); 1282 return resolvedConfig; 1283 } 1284 IConfiguration config = 1285 getConfigFactory().createConfigurationFromArgs(args, null, getKeyStoreClient()); 1286 if (isRetryCommand(config)) { 1287 return RetryConfigurationFactory.getInstance().createRetryConfiguration(config); 1288 } 1289 return config; 1290 } 1291 1292 /** 1293 * Create a delegator based on the command line to see if we need to delegate the run. 1294 * 1295 * @throws ConfigurationException 1296 */ checkDelegation(String[] args)1297 public static TradefedDelegator checkDelegation(String[] args) throws ConfigurationException { 1298 TradefedDelegator delegator = new TradefedDelegator(); 1299 ArgsOptionParser argsParser = new ArgsOptionParser(delegator); 1300 List<String> argsList = new ArrayList<>(Arrays.asList(args)); 1301 argsList.remove(0); 1302 argsParser.parseBestEffort(argsList, true); 1303 return delegator; 1304 } 1305 setDelegateLevelReporting(IConfiguration config)1306 private void setDelegateLevelReporting(IConfiguration config) { 1307 List<ITestInvocationListener> delegateReporters = new ArrayList<>(); 1308 // For debugging in the console, add a printer 1309 delegateReporters.add(new SuiteResultReporter()); 1310 for (ITestInvocationListener listener : config.getTestInvocationListeners()) { 1311 // Add infra reporter if configured. 1312 if ("com.google.android.tradefed.result.teststorage.ResultReporter" 1313 .equals(listener.getClass().getCanonicalName())) { 1314 delegateReporters.add(listener); 1315 } 1316 } 1317 config.setTestInvocationListeners(delegateReporters); 1318 } 1319 internalAddCommand(String[] args, String cmdFilePath)1320 private boolean internalAddCommand(String[] args, String cmdFilePath) 1321 throws ConfigurationException { 1322 assertStarted(); 1323 IConfiguration config = createConfiguration(args); 1324 if (config.getCommandOptions().isHelpMode()) { 1325 getConfigFactory().printHelpForConfig(args, true, System.out); 1326 } else if (config.getCommandOptions().isFullHelpMode()) { 1327 getConfigFactory().printHelpForConfig(args, false, System.out); 1328 } else if (config.getCommandOptions().isDryRunMode()) { 1329 config.validateOptions(); 1330 String cmdLine = QuotationAwareTokenizer.combineTokens(args); 1331 CLog.d("Dry run mode; skipping adding command: %s", cmdLine); 1332 if (config.getCommandOptions().isNoisyDryRunMode()) { 1333 System.out.println(cmdLine.replace("--noisy-dry-run", "")); 1334 System.out.println(""); 1335 } 1336 } else { 1337 config.validateOptions(); 1338 1339 if (config.getCommandOptions().runOnAllDevices()) { 1340 addCommandForAllDevices(args, cmdFilePath); 1341 } else { 1342 CommandTracker cmdTracker = createCommandTracker(args, cmdFilePath); 1343 ExecutableCommand cmdInstance = createExecutableCommand(cmdTracker, config, false); 1344 addExecCommandToQueue(cmdInstance, 0); 1345 } 1346 return true; 1347 } 1348 return false; 1349 } 1350 1351 /** 1352 * {@inheritDoc} 1353 */ 1354 @Override addCommandFile(String cmdFilePath, List<String> extraArgs)1355 public void addCommandFile(String cmdFilePath, List<String> extraArgs) 1356 throws ConfigurationException { 1357 // verify we aren't already watching this command file, don't want to add it twice! 1358 File cmdFile = new File(cmdFilePath); 1359 if (mReloadCmdfiles && getCommandFileWatcher().isFileWatched(cmdFile)) { 1360 CLog.logAndDisplay(LogLevel.INFO, 1361 "cmd file %s is already running and being watched for changes. Reloading", 1362 cmdFilePath); 1363 removeCommandsFromFile(cmdFile); 1364 } 1365 internalAddCommandFile(cmdFile, extraArgs); 1366 } 1367 1368 /** 1369 * Adds a command file without verifying if its already being watched 1370 */ internalAddCommandFile(File cmdFile, List<String> extraArgs)1371 private void internalAddCommandFile(File cmdFile, List<String> extraArgs) 1372 throws ConfigurationException { 1373 try { 1374 CommandFileParser parser = createCommandFileParser(); 1375 1376 List<CommandLine> commands = parser.parseFile(cmdFile); 1377 if (mReloadCmdfiles) { 1378 // note always should re-register for command file, even if already listening, 1379 // since the dependent file list might have changed 1380 getCommandFileWatcher().addCmdFile(cmdFile, extraArgs, parser.getIncludedFiles()); 1381 } 1382 for (CommandLine command : commands) { 1383 command.addAll(extraArgs); 1384 String[] arrayCommand = command.asArray(); 1385 final String prettyCmdLine = QuotationAwareTokenizer.combineTokens(arrayCommand); 1386 CLog.d("Adding command %s", prettyCmdLine); 1387 1388 try { 1389 internalAddCommand(arrayCommand, cmdFile.getAbsolutePath()); 1390 } catch (ConfigurationException e) { 1391 throw new ConfigurationException(String.format( 1392 "Failed to add command '%s': %s", prettyCmdLine, e.getMessage()), e); 1393 } 1394 } 1395 } catch (IOException e) { 1396 throw new ConfigurationException("Failed to read file " + cmdFile.getAbsolutePath(), e); 1397 } 1398 } 1399 1400 /** 1401 * Factory method for creating a {@link CommandFileParser}. 1402 * 1403 * <p>Exposed for unit testing. 1404 */ createCommandFileParser()1405 CommandFileParser createCommandFileParser() { 1406 return new CommandFileParser(); 1407 } 1408 1409 /** 1410 * Creates a new command for each connected device, and adds each to the queue. 1411 * 1412 * <p>Note this won't have the desired effect if user has specified other conflicting {@link 1413 * IConfiguration#getDeviceRequirements()}in the command. 1414 */ addCommandForAllDevices(String[] args, String cmdFilePath)1415 private void addCommandForAllDevices(String[] args, String cmdFilePath) 1416 throws ConfigurationException { 1417 List<DeviceDescriptor> deviceDescs = getDeviceManager().listAllDevices(); 1418 1419 for (DeviceDescriptor deviceDesc : deviceDescs) { 1420 if (!deviceDesc.isStubDevice()) { 1421 String device = deviceDesc.getSerial(); 1422 String[] argsWithDevice = Arrays.copyOf(args, args.length + 2); 1423 argsWithDevice[argsWithDevice.length - 2] = "-s"; 1424 argsWithDevice[argsWithDevice.length - 1] = device; 1425 CommandTracker cmdTracker = createCommandTracker(argsWithDevice, cmdFilePath); 1426 IConfiguration config = getConfigFactory().createConfigurationFromArgs( 1427 cmdTracker.getArgs(), null, getKeyStoreClient()); 1428 CLog.logAndDisplay(LogLevel.INFO, "Scheduling '%s' on '%s'", 1429 cmdTracker.getArgs()[0], device); 1430 config.getDeviceRequirements().setSerial(device); 1431 ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false); 1432 addExecCommandToQueue(execCmd, 0); 1433 } 1434 } 1435 } 1436 1437 /** 1438 * Creates a new {@link CommandTracker} with a unique id. 1439 */ createCommandTracker(String[] args, String commandFilePath)1440 private synchronized CommandTracker createCommandTracker(String[] args, 1441 String commandFilePath) { 1442 mCurrentCommandId++; 1443 CLog.d("Creating command tracker id %d for command args: '%s'", mCurrentCommandId, 1444 ArrayUtil.join(" ", (Object[])args)); 1445 return new CommandTracker(mCurrentCommandId, args, commandFilePath); 1446 } 1447 1448 /** 1449 * Creates a new {@link ExecutableCommand}. 1450 */ createExecutableCommand(CommandTracker cmdTracker, IConfiguration config, boolean rescheduled)1451 private ExecutableCommand createExecutableCommand(CommandTracker cmdTracker, 1452 IConfiguration config, boolean rescheduled) { 1453 ExecutableCommand cmd = new ExecutableCommand(cmdTracker, config, rescheduled); 1454 CLog.d("creating exec command for id %d", cmdTracker.getId()); 1455 return cmd; 1456 } 1457 1458 /** 1459 * Creates a new {@link ExecutableCommand}, and adds it to queue 1460 * 1461 * @param commandTracker 1462 */ addNewExecCommandToQueue(CommandTracker commandTracker)1463 private void addNewExecCommandToQueue(CommandTracker commandTracker) { 1464 try { 1465 IConfiguration config = createConfiguration(commandTracker.getArgs()); 1466 ExecutableCommand execCmd = createExecutableCommand(commandTracker, config, false); 1467 addExecCommandToQueue(execCmd, config.getCommandOptions().getLoopTime()); 1468 } catch (ConfigurationException e) { 1469 CLog.e(e); 1470 } 1471 } 1472 1473 /** 1474 * Adds executable command instance to queue, with optional delay. 1475 * 1476 * @param cmd the {@link ExecutableCommand} to return to queue 1477 * @param delayTime the time in ms to delay before adding command to queue 1478 * @return <code>true</code> if command will be added to queue, <code>false</code> otherwise 1479 */ addExecCommandToQueue(final ExecutableCommand cmd, long delayTime)1480 private synchronized boolean addExecCommandToQueue(final ExecutableCommand cmd, 1481 long delayTime) { 1482 // If the command is local sharding one being rescheduled, do not apply the shutdown yet 1483 // This allows commandAndExit to still works with local sharding. 1484 if (isShutdown()) { 1485 if (cmd.getConfiguration() 1486 .getConfigurationDescription() 1487 .getMetaData(ConfigurationDescriptor.LOCAL_SHARDED_KEY) 1488 == null) { 1489 return false; 1490 } 1491 } 1492 if (delayTime > 0) { 1493 mSleepingCommands.add(cmd); 1494 // delay before making command active 1495 Runnable delayCommand = new Runnable() { 1496 @Override 1497 public void run() { 1498 synchronized (CommandScheduler.this) { 1499 if (mSleepingCommands.remove(cmd)) { 1500 mReadyCommands.add(cmd); 1501 mCommandProcessWait.signalEventReceived(); 1502 } 1503 } 1504 } 1505 }; 1506 mCommandTimer.schedule(delayCommand, delayTime, TimeUnit.MILLISECONDS); 1507 } else { 1508 mReadyCommands.add(cmd); 1509 mCommandProcessWait.signalEventReceived(); 1510 } 1511 return true; 1512 } 1513 1514 /** 1515 * Helper method to return an array of {@link String} elements as a readable {@link String} 1516 * 1517 * @param args the {@link String}[] to use 1518 * @return a display friendly {@link String} of args contents 1519 */ getArgString(String[] args)1520 private String getArgString(String[] args) { 1521 return ArrayUtil.join(" ", (Object[])args); 1522 } 1523 1524 /** {@inheritDoc} */ 1525 @Override execCommand( IInvocationContext context, IScheduledInvocationListener listener, String[] args)1526 public void execCommand( 1527 IInvocationContext context, IScheduledInvocationListener listener, String[] args) 1528 throws ConfigurationException, NoDeviceException { 1529 assertStarted(); 1530 IDeviceManager manager = getDeviceManager(); 1531 CommandTracker cmdTracker = createCommandTracker(args, null); 1532 IConfiguration config = createConfiguration(cmdTracker.getArgs()); 1533 config.validateOptions(); 1534 1535 ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false); 1536 context.setConfigurationDescriptor(config.getConfigurationDescription()); 1537 Map<String, ITestDevice> devices = allocateDevices(config, manager); 1538 if (!devices.isEmpty()) { 1539 context.addAllocatedDevice(devices); 1540 synchronized (this) { 1541 mExecutingCommands.add(execCmd); 1542 } 1543 CLog.d("Executing '%s' on '%s'", cmdTracker.getArgs()[0], devices); 1544 startInvocation(context, execCmd, listener, new FreeDeviceHandler(manager)); 1545 } else { 1546 throw new NoDeviceException( 1547 "no devices is available for command: " + Arrays.asList(args)); 1548 } 1549 } 1550 1551 /** {@inheritDoc} */ 1552 @Override execCommand( IScheduledInvocationListener listener, ITestDevice device, String[] args)1553 public void execCommand( 1554 IScheduledInvocationListener listener, ITestDevice device, String[] args) 1555 throws ConfigurationException { 1556 // TODO: add support for execCommand multi-device allocation 1557 assertStarted(); 1558 CommandTracker cmdTracker = createCommandTracker(args, null); 1559 IConfiguration config = 1560 getConfigFactory() 1561 .createConfigurationFromArgs( 1562 cmdTracker.getArgs(), null, getKeyStoreClient()); 1563 config.validateOptions(); 1564 CLog.i("Executing '%s' on '%s'", cmdTracker.getArgs()[0], device.getSerialNumber()); 1565 ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false); 1566 if (config.getDeviceConfig().size() > 1) { 1567 throw new RuntimeException("execCommand assume single device invocation."); 1568 } 1569 1570 synchronized (this) { 1571 mExecutingCommands.add(execCmd); 1572 } 1573 IInvocationContext context = createInvocationContext(); 1574 context.setConfigurationDescriptor(config.getConfigurationDescription()); 1575 context.addAllocatedDevice(config.getDeviceConfig().get(0).getDeviceName(), device); 1576 startInvocation(context, execCmd, listener); 1577 } 1578 1579 /** {@inheritDoc} */ 1580 @Override execCommand(IScheduledInvocationListener listener, String[] args)1581 public void execCommand(IScheduledInvocationListener listener, String[] args) 1582 throws ConfigurationException, NoDeviceException { 1583 execCommand(createInvocationContext(), listener, args); 1584 } 1585 1586 /** 1587 * Allocate devices for a config. 1588 * @param config a {@link IConfiguration} has device requirements. 1589 * @param manager a {@link IDeviceManager} 1590 * @return allocated devices 1591 */ allocateDevices(IConfiguration config, IDeviceManager manager)1592 Map<String, ITestDevice> allocateDevices(IConfiguration config, IDeviceManager manager) { 1593 Map<String, ITestDevice> devices = new LinkedHashMap<String, ITestDevice>(); 1594 ITestDevice device = null; 1595 if (config.getDeviceConfig().isEmpty()) { 1596 return null; 1597 } 1598 // If we need to replicate the setup on all devices 1599 ParentShardReplicate.replicatedSetup(config, getKeyStoreClient()); 1600 synchronized(this) { 1601 for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) { 1602 device = 1603 manager.allocateDevice( 1604 deviceConfig.getDeviceRequirements(), deviceConfig.isFake()); 1605 if (device != null) { 1606 devices.put(deviceConfig.getDeviceName(), device); 1607 } else { 1608 // If one of the several device cannot be allocated, we de-allocate 1609 // all the previous one. 1610 for (ITestDevice allocatedDevice : devices.values()) { 1611 FreeDeviceState deviceState = FreeDeviceState.AVAILABLE; 1612 if (allocatedDevice.getIDevice() instanceof StubDevice) { 1613 deviceState = FreeDeviceState.AVAILABLE; 1614 } else if (!TestDeviceState.ONLINE.equals( 1615 allocatedDevice.getDeviceState())) { 1616 // If the device is offline at the end of the test 1617 deviceState = FreeDeviceState.UNAVAILABLE; 1618 } 1619 manager.freeDevice(allocatedDevice, deviceState); 1620 } 1621 // Could not allocate all devices 1622 devices.clear(); 1623 break; 1624 } 1625 } 1626 return devices; 1627 } 1628 } 1629 1630 @VisibleForTesting createInvocationContext()1631 protected IInvocationContext createInvocationContext() { 1632 return new InvocationContext(); 1633 } 1634 1635 /** 1636 * Spawns off thread to run invocation for given device. 1637 * 1638 * @param context the {@link IInvocationContext} 1639 * @param cmd the {@link ExecutableCommand} to execute 1640 * @param listeners the {@link 1641 * com.android.tradefed.command.ICommandScheduler.IScheduledInvocationListener}s to invoke 1642 * when complete 1643 */ startInvocation( IInvocationContext context, ExecutableCommand cmd, IScheduledInvocationListener... listeners)1644 private void startInvocation( 1645 IInvocationContext context, 1646 ExecutableCommand cmd, 1647 IScheduledInvocationListener... listeners) { 1648 // Check if device is not used in another invocation. 1649 try { 1650 throwIfDeviceInInvocationThread(context.getDevices()); 1651 } catch (Exception e) { 1652 setLastInvocationExitCode(ExitCode.THROWABLE_EXCEPTION, e); 1653 for (ITestDevice device : context.getDevices()) { 1654 getDeviceManager().freeDevice(device, FreeDeviceState.AVAILABLE); 1655 } 1656 throw e; 1657 } 1658 1659 int invocationId = cmd.getCommandTracker().getId(); 1660 CLog.d("starting invocation for command id %d", invocationId); 1661 // Name invocation with first device serial 1662 final String invocationName = String.format("Invocation-%s", 1663 context.getSerials().get(0)); 1664 InvocationThread invocationThread = new InvocationThread(invocationName, context, cmd, 1665 listeners); 1666 // Link context and command 1667 context.addInvocationAttribute( 1668 IInvocationContext.INVOCATION_ID, Integer.toString(invocationId)); 1669 logInvocationStartedEvent(cmd.getCommandTracker(), context); 1670 invocationThread.start(); 1671 addInvocationThread(invocationThread); 1672 } 1673 1674 /** Helper to log an invocation started event. */ logInvocationStartedEvent(CommandTracker tracker, IInvocationContext context)1675 private void logInvocationStartedEvent(CommandTracker tracker, IInvocationContext context) { 1676 Map<String, String> args = new HashMap<>(); 1677 args.put("id", Integer.toString(tracker.getId())); 1678 int i = 0; 1679 for (String serial : context.getSerials()) { 1680 args.put("serial" + i, serial); 1681 i++; 1682 } 1683 args.put("args", ArrayUtil.join(" ", Arrays.asList(tracker.getArgs()))); 1684 logEvent(EventType.INVOCATION_START, args); 1685 } 1686 1687 /** Removes a {@link InvocationThread} from the active list. */ removeInvocationThread(InvocationThread invThread)1688 private synchronized void removeInvocationThread(InvocationThread invThread) { 1689 mInvocationThreadMap.remove(invThread.getInvocationContext()); 1690 } 1691 throwIfDeviceInInvocationThread(List<ITestDevice> devices)1692 private synchronized void throwIfDeviceInInvocationThread(List<ITestDevice> devices) { 1693 for (ITestDevice device : devices) { 1694 for (IInvocationContext context : mInvocationThreadMap.keySet()) { 1695 if (context.getDevices().contains(device)) { 1696 if (context.wasReleasedEarly()) { 1697 return; 1698 } 1699 throw new IllegalStateException( 1700 String.format( 1701 "Attempting invocation on device %s when one is already " 1702 + "running", 1703 device.getSerialNumber())); 1704 } 1705 } 1706 } 1707 } 1708 1709 /** 1710 * Adds a {@link InvocationThread} to the active list. 1711 */ addInvocationThread(InvocationThread invThread)1712 private synchronized void addInvocationThread(InvocationThread invThread) { 1713 mInvocationThreadMap.put(invThread.getInvocationContext(), invThread); 1714 } 1715 isShutdown()1716 protected synchronized boolean isShutdown() { 1717 return mCommandTimer.isShutdown() || (mShutdownOnEmpty && getAllCommandsSize() == 0); 1718 } 1719 isShuttingDown()1720 protected synchronized boolean isShuttingDown() { 1721 return mCommandTimer.isShutdown() || mShutdownOnEmpty; 1722 } 1723 1724 /** 1725 * {@inheritDoc} 1726 */ 1727 @Override shutdown()1728 public synchronized void shutdown() { 1729 setHostState(HostState.QUITTING); 1730 doShutdown(); 1731 } 1732 doShutdown()1733 private synchronized void doShutdown() { 1734 assertStarted(); 1735 if (!isShuttingDown()) { 1736 CLog.d("initiating shutdown"); 1737 removeAllCommands(); 1738 if (mCommandFileWatcher != null) { 1739 mCommandFileWatcher.cancel(); 1740 } 1741 if (mCommandTimer != null) { 1742 mCommandTimer.shutdownNow(); 1743 } 1744 mCommandProcessWait.signalEventReceived(); 1745 } 1746 } 1747 1748 /** 1749 * {@inheritDoc} 1750 */ 1751 @Override shutdownOnEmpty()1752 public synchronized void shutdownOnEmpty() { 1753 assertStarted(); 1754 setHostState(HostState.QUITTING); 1755 if (!isShuttingDown()) { 1756 CLog.d("initiating shutdown on empty"); 1757 mShutdownOnEmpty = true; 1758 mCommandProcessWait.signalEventReceived(); 1759 } 1760 } 1761 1762 /** 1763 * {@inheritDoc} 1764 */ 1765 @Override removeAllCommands()1766 public synchronized void removeAllCommands() { 1767 assertStarted(); 1768 CLog.d("removing all commands"); 1769 if (mReloadCmdfiles) { 1770 getCommandFileWatcher().removeAllFiles(); 1771 } 1772 if (mCommandTimer != null) { 1773 for (Runnable task : mCommandTimer.getQueue()) { 1774 mCommandTimer.remove(task); 1775 } 1776 } 1777 mReadyCommands.clear(); 1778 mSleepingCommands.clear(); 1779 if (isShuttingDown()) { 1780 mCommandProcessWait.signalEventReceived(); 1781 } 1782 } 1783 1784 /** 1785 * Remove commands originally added via the given command file 1786 * @param cmdFile 1787 */ removeCommandsFromFile(File cmdFile)1788 private synchronized void removeCommandsFromFile(File cmdFile) { 1789 Iterator<ExecutableCommand> cmdIter = mReadyCommands.iterator(); 1790 while (cmdIter.hasNext()) { 1791 ExecutableCommand cmd = cmdIter.next(); 1792 String path = cmd.getCommandFilePath(); 1793 if (path != null && 1794 path.equals(cmdFile.getAbsolutePath())) { 1795 cmdIter.remove(); 1796 } 1797 } 1798 cmdIter = mSleepingCommands.iterator(); 1799 while (cmdIter.hasNext()) { 1800 ExecutableCommand cmd = cmdIter.next(); 1801 String path = cmd.getCommandFilePath(); 1802 if (path != null && 1803 path.equals(cmdFile.getAbsolutePath())) { 1804 cmdIter.remove(); 1805 } 1806 } 1807 if (isShuttingDown()) { 1808 mCommandProcessWait.signalEventReceived(); 1809 } 1810 } 1811 1812 /** 1813 * {@inheritDoc} 1814 */ 1815 @Override handoverShutdown(int handoverPort)1816 public synchronized boolean handoverShutdown(int handoverPort) { 1817 assertStarted(); 1818 if (mRemoteClient != null || mPerformingHandover) { 1819 CLog.e("A handover has already been initiated"); 1820 return false; 1821 } 1822 mPerformingHandover = true; 1823 try { 1824 mRemoteClient = RemoteClient.connect(handoverPort); 1825 CLog.d("Connected to remote manager at %d", handoverPort); 1826 handoverDevices(mRemoteClient); 1827 CLog.i("Done with device handover."); 1828 mRemoteClient.sendHandoverInitComplete(); 1829 shutdown(); 1830 return true; 1831 } catch (RemoteException e) { 1832 CLog.e(e); 1833 // TODO: reset state and recover 1834 } 1835 return false; 1836 } 1837 1838 /** Informs remote manager of the physical devices we are still using. */ handoverDevices(IRemoteClient client)1839 private void handoverDevices(IRemoteClient client) throws RemoteException { 1840 for (DeviceDescriptor deviceDesc : getDeviceManager().listAllDevices()) { 1841 if (DeviceAllocationState.Allocated.equals(deviceDesc.getState()) 1842 && !deviceDesc.isStubDevice()) { 1843 client.sendAllocateDevice(deviceDesc.getSerial()); 1844 CLog.d("Sent filter device %s command", deviceDesc.getSerial()); 1845 } 1846 } 1847 } 1848 1849 /** 1850 * @return the list of active {@link CommandTracker}. 'Active' here means all commands added 1851 * to the scheduler that are either executing, waiting for a device to execute on, or looping. 1852 */ getCommandTrackers()1853 List<CommandTracker> getCommandTrackers() { 1854 List<ExecutableCommandState> cmdCopy = getAllCommands(); 1855 Set<CommandTracker> cmdTrackers = new LinkedHashSet<CommandTracker>(); 1856 for (ExecutableCommandState cmdState : cmdCopy) { 1857 cmdTrackers.add(cmdState.cmd.getCommandTracker()); 1858 } 1859 return new ArrayList<CommandTracker>(cmdTrackers); 1860 } 1861 1862 /** 1863 * Inform the remote listener of the freed device. Has no effect if there is no remote listener. 1864 * 1865 * @param device the freed {@link ITestDevice} 1866 */ remoteFreeDevice(ITestDevice device)1867 private void remoteFreeDevice(ITestDevice device) { 1868 // TODO: send freed device state too 1869 if (mPerformingHandover && mRemoteClient != null) { 1870 try { 1871 mRemoteClient.sendFreeDevice(device.getSerialNumber()); 1872 } catch (RemoteException e) { 1873 CLog.e("Failed to send unfilter device %s to remote manager", 1874 device.getSerialNumber()); 1875 CLog.e(e); 1876 // TODO: send handover failed op? 1877 } 1878 } 1879 } 1880 1881 /** 1882 * {@inheritDoc} 1883 */ 1884 @Override shutdownHard()1885 public synchronized void shutdownHard() { 1886 setHostState(HostState.KILLING); 1887 doShutdown(); 1888 CLog.logAndDisplay(LogLevel.WARN, "Stopping invocation threads..."); 1889 for (InvocationThread thread : mInvocationThreadMap.values()) { 1890 thread.disableReporters(); 1891 // TODO(b/118891716): Improve tear down 1892 thread.stopInvocation("Tradefed is shutting down"); 1893 } 1894 getDeviceManager().terminateHard(); 1895 } 1896 1897 /** 1898 * Initializes the ddmlib log. 1899 * 1900 * <p>Exposed so unit tests can mock. 1901 */ 1902 @SuppressWarnings("deprecation") initLogging()1903 protected void initLogging() { 1904 DdmPreferences.setLogLevel(LogLevel.VERBOSE.getStringValue()); 1905 Log.setLogOutput(LogRegistry.getLogRegistry()); 1906 } 1907 1908 /** 1909 * Closes the logs and does any other necessary cleanup before we quit. 1910 * 1911 * <p>Exposed so unit tests can mock. 1912 */ cleanUp()1913 protected void cleanUp() { 1914 LogRegistry.getLogRegistry().closeAndRemoveAllLogs(); 1915 } 1916 1917 /** log an event to the registry history logger. */ 1918 @VisibleForTesting logEvent(EventType event, Map<String, String> args)1919 void logEvent(EventType event, Map<String, String> args) { 1920 LogRegistry.getLogRegistry().logEvent(LogLevel.DEBUG, event, args); 1921 } 1922 1923 /** {@inheritDoc} */ 1924 @Override displayInvocationsInfo(PrintWriter printWriter)1925 public void displayInvocationsInfo(PrintWriter printWriter) { 1926 assertStarted(); 1927 if (mInvocationThreadMap == null || mInvocationThreadMap.size() == 0) { 1928 return; 1929 } 1930 List<InvocationThread> copy = new ArrayList<InvocationThread>(); 1931 synchronized(this) { 1932 copy.addAll(mInvocationThreadMap.values()); 1933 } 1934 ArrayList<List<String>> displayRows = new ArrayList<List<String>>(); 1935 displayRows.add(Arrays.asList("Command Id", "Exec Time", "Device", "State")); 1936 long curTime = System.currentTimeMillis(); 1937 1938 for (InvocationThread invThread : copy) { 1939 displayRows.add(Arrays.asList( 1940 Integer.toString(invThread.mCmd.getCommandTracker().getId()), 1941 getTimeString(curTime - invThread.getStartTime()), 1942 invThread.getInvocationContext().getSerials().toString(), 1943 invThread.getInvocation().toString())); 1944 } 1945 new TableFormatter().displayTable(displayRows, printWriter); 1946 } 1947 getTimeString(long elapsedTime)1948 private String getTimeString(long elapsedTime) { 1949 long duration = elapsedTime / 1000; 1950 long secs = duration % 60; 1951 long mins = (duration / 60) % 60; 1952 long hrs = duration / (60 * 60); 1953 String time = "unknown"; 1954 if (hrs > 0) { 1955 time = String.format("%dh:%02d:%02d", hrs, mins, secs); 1956 } else { 1957 time = String.format("%dm:%02d", mins, secs); 1958 } 1959 return time; 1960 } 1961 1962 /** 1963 * {@inheritDoc} 1964 */ 1965 @Override stopInvocation(ITestInvocation invocation)1966 public synchronized boolean stopInvocation(ITestInvocation invocation) { 1967 for (InvocationThread thread : mInvocationThreadMap.values()) { 1968 if (thread.getInvocation() == invocation) { 1969 thread.interrupt(); 1970 return true; 1971 } 1972 } 1973 return false; 1974 } 1975 1976 /** {@inheritDoc} */ 1977 @Override stopInvocation(int invocationId, String cause)1978 public synchronized boolean stopInvocation(int invocationId, String cause) { 1979 // TODO: make invocationID part of InvocationContext 1980 for (InvocationThread thread : mInvocationThreadMap.values()) { 1981 if (thread.mCmd.getCommandTracker().mId == invocationId) { 1982 if (cause == null) { 1983 cause = "User requested stopping invocation " + invocationId; 1984 } 1985 thread.stopInvocation(cause); 1986 return true; 1987 } 1988 } 1989 CLog.w("No invocation found matching the id: %s", invocationId); 1990 return false; 1991 } 1992 1993 /** 1994 * {@inheritDoc} 1995 */ 1996 @Override getInvocationInfo(int invocationId)1997 public synchronized String getInvocationInfo(int invocationId) { 1998 for (InvocationThread thread : mInvocationThreadMap.values()) { 1999 if (thread.mCmd.getCommandTracker().mId == invocationId) { 2000 String info = Arrays.toString(thread.mCmd.getCommandTracker().getArgs()); 2001 return info; 2002 } 2003 } 2004 CLog.w("No invocation found matching the id: %s", invocationId); 2005 return null; 2006 } 2007 2008 /** 2009 * {@inheritDoc} 2010 */ 2011 @Override displayCommandsInfo(PrintWriter printWriter, String regex)2012 public void displayCommandsInfo(PrintWriter printWriter, String regex) { 2013 assertStarted(); 2014 Pattern regexPattern = null; 2015 if (regex != null) { 2016 regexPattern = Pattern.compile(regex); 2017 } 2018 2019 List<CommandTracker> cmds = getCommandTrackers(); 2020 Collections.sort(cmds, new CommandTrackerIdComparator()); 2021 for (CommandTracker cmd : cmds) { 2022 String argString = getArgString(cmd.getArgs()); 2023 if (regexPattern == null || regexPattern.matcher(argString).find()) { 2024 String cmdDesc = String.format("Command %d: [%s] %s", cmd.getId(), 2025 getTimeString(cmd.getTotalExecTime()), argString); 2026 printWriter.println(cmdDesc); 2027 } 2028 } 2029 } 2030 2031 /** 2032 * {@inheritDoc} 2033 */ 2034 @Override dumpCommandsXml(PrintWriter printWriter, String regex)2035 public void dumpCommandsXml(PrintWriter printWriter, String regex) { 2036 assertStarted(); 2037 Pattern regexPattern = null; 2038 if (regex != null) { 2039 regexPattern = Pattern.compile(regex); 2040 } 2041 2042 List<ExecutableCommandState> cmdCopy = getAllCommands(); 2043 for (ExecutableCommandState cmd : cmdCopy) { 2044 String[] args = cmd.cmd.getCommandTracker().getArgs(); 2045 String argString = getArgString(args); 2046 if (regexPattern == null || regexPattern.matcher(argString).find()) { 2047 // Use the config name prefixed by config__ for the file path 2048 String xmlPrefix = "config__" + args[0].replace("/", "__") + "__"; 2049 2050 // If the command line contains --template:map test config, use that config for the 2051 // file path. This is because in the template system, many tests will have same 2052 // base config and the distinguishing feature is the test included. 2053 boolean templateIncludeFound = false; 2054 boolean testFound = false; 2055 for (String arg : args) { 2056 if ("--template:map".equals(arg)) { 2057 templateIncludeFound = true; 2058 } else if (templateIncludeFound && "test".equals(arg)) { 2059 testFound = true; 2060 } else { 2061 if (templateIncludeFound && testFound) { 2062 xmlPrefix = "config__" + arg.replace("/", "__") + "__"; 2063 } 2064 templateIncludeFound = false; 2065 testFound = false; 2066 } 2067 } 2068 2069 try { 2070 File xmlFile = FileUtil.createTempFile(xmlPrefix, ".xml"); 2071 PrintWriter writer = new PrintWriter(xmlFile); 2072 cmd.cmd.getConfiguration().dumpXml(writer); 2073 printWriter.println(String.format("Saved command dump to %s", 2074 xmlFile.getAbsolutePath())); 2075 } catch (IOException e) { 2076 // Log exception and continue 2077 CLog.e("Could not dump config xml"); 2078 CLog.e(e); 2079 } 2080 } 2081 } 2082 } 2083 2084 /** 2085 * {@inheritDoc} 2086 */ 2087 @Override displayCommandQueue(PrintWriter printWriter)2088 public void displayCommandQueue(PrintWriter printWriter) { 2089 assertStarted(); 2090 List<ExecutableCommandState> cmdCopy = getAllCommands(); 2091 if (cmdCopy.size() == 0) { 2092 return; 2093 } 2094 ArrayList<List<String>> displayRows = new ArrayList<List<String>>(); 2095 displayRows.add(Arrays.asList("Id", "Config", "Created", "Exec time", "State", "Sleep time", 2096 "Rescheduled", "Loop")); 2097 long curTime = System.currentTimeMillis(); 2098 for (ExecutableCommandState cmd : cmdCopy) { 2099 dumpCommand(curTime, cmd, displayRows); 2100 } 2101 new TableFormatter().displayTable(displayRows, printWriter); 2102 } 2103 dumpCommand(long curTime, ExecutableCommandState cmdAndState, ArrayList<List<String>> displayRows)2104 private void dumpCommand(long curTime, ExecutableCommandState cmdAndState, 2105 ArrayList<List<String>> displayRows) { 2106 ExecutableCommand cmd = cmdAndState.cmd; 2107 String sleepTime = cmd.getSleepTime() == null ? "N/A" : getTimeString(cmd.getSleepTime()); 2108 displayRows.add(Arrays.asList( 2109 Integer.toString(cmd.getCommandTracker().getId()), 2110 cmd.getCommandTracker().getArgs()[0], 2111 getTimeString(curTime - cmd.getCreationTime()), 2112 getTimeString(cmd.mCmdTracker.getTotalExecTime()), 2113 cmdAndState.state.getDisplayName(), 2114 sleepTime, 2115 Boolean.toString(cmd.isRescheduled()), 2116 Boolean.toString(cmd.isLoopMode()))); 2117 } 2118 2119 /** 2120 * Starts remote manager to listen to remote commands. 2121 * 2122 * <p>TODO: refactor to throw exception on failure 2123 */ startRemoteManager()2124 private void startRemoteManager() { 2125 if (mRemoteManager != null && !mRemoteManager.isCanceled()) { 2126 String error = String.format("A remote manager is already running at port %d", 2127 mRemoteManager.getPort()); 2128 throw new IllegalStateException(error); 2129 } 2130 mRemoteManager = new RemoteManager(getDeviceManager(), this); 2131 // Read the args that were set by the global config. 2132 boolean startRmtMgrOnBoot = mRemoteManager.getStartRemoteMgrOnBoot(); 2133 int defaultRmtMgrPort = mRemoteManager.getRemoteManagerPort(); 2134 boolean autoHandover = mRemoteManager.getAutoHandover(); 2135 2136 if (!startRmtMgrOnBoot) { 2137 mRemoteManager = null; 2138 return; 2139 } 2140 if (mRemoteManager.connect()) { 2141 mRemoteManager.start(); 2142 CLog.logAndDisplay(LogLevel.INFO, "Started remote manager at port %d", 2143 mRemoteManager.getPort()); 2144 return; 2145 } 2146 CLog.logAndDisplay(LogLevel.INFO, "Failed to start remote manager at port %d", 2147 defaultRmtMgrPort); 2148 if (!autoHandover) { 2149 if (mRemoteManager.connectAnyPort()) { 2150 mRemoteManager.start(); 2151 CLog.logAndDisplay(LogLevel.INFO, 2152 "Started remote manager at port %d with no handover", 2153 mRemoteManager.getPort()); 2154 return; 2155 } else { 2156 CLog.logAndDisplay(LogLevel.ERROR, "Failed to auto start a remote manager on boot."); 2157 return; 2158 } 2159 } 2160 try { 2161 CLog.logAndDisplay(LogLevel.INFO, "Initiating handover with remote TF instance!"); 2162 mHandoverHandshake.reset(); 2163 initiateHandover(defaultRmtMgrPort); 2164 waitForHandoverHandshake(); 2165 CLog.logAndDisplay(LogLevel.INFO, "Handover initiation complete."); 2166 } catch (RemoteException e) { 2167 CLog.e(e); 2168 } 2169 } 2170 waitForHandoverHandshake()2171 private void waitForHandoverHandshake() { 2172 // block and wait to receive all the commands and 'device still in use' messages from remote 2173 mHandoverHandshake.waitForEvent(MAX_HANDOVER_INIT_TIME); 2174 // TODO: throw exception if not received 2175 } 2176 2177 /** 2178 * Helper object for allowing multiple threads to synchronize on an event. 2179 * 2180 * <p>Basically a modest wrapper around Object's wait and notify methods, that supports 2181 * remembering if a notify call was made. 2182 */ 2183 private static class WaitObj { 2184 boolean mEventReceived = false; 2185 2186 /** 2187 * Wait for signal for a max of given ms. 2188 * @return true if event received before time elapsed, false otherwise 2189 */ waitForEvent(long maxWaitTime)2190 public synchronized boolean waitForEvent(long maxWaitTime) { 2191 if (maxWaitTime == 0) { 2192 return waitForEvent(); 2193 } 2194 long startTime = System.currentTimeMillis(); 2195 long remainingTime = maxWaitTime; 2196 while (!mEventReceived && remainingTime > 0) { 2197 try { 2198 wait(remainingTime); 2199 } catch (InterruptedException e) { 2200 CLog.w("interrupted"); 2201 } 2202 remainingTime = maxWaitTime - (System.currentTimeMillis() - startTime); 2203 } 2204 return mEventReceived; 2205 } 2206 2207 /** 2208 * Wait for signal indefinitely or until interrupted. 2209 * @return true if event received, false otherwise 2210 */ waitForEvent()2211 public synchronized boolean waitForEvent() { 2212 if (!mEventReceived) { 2213 try { 2214 wait(); 2215 } catch (InterruptedException e) { 2216 CLog.w("interrupted"); 2217 } 2218 } 2219 return mEventReceived; 2220 } 2221 2222 /** 2223 * Reset the event received flag. 2224 */ reset()2225 public synchronized void reset() { 2226 mEventReceived = false; 2227 } 2228 2229 /** 2230 * Wait for given ms for event to be received, and reset state back to 'no event received' 2231 * upon completion. 2232 */ waitAndReset(long maxWaitTime)2233 public synchronized void waitAndReset(long maxWaitTime) { 2234 waitForEvent(maxWaitTime); 2235 reset(); 2236 } 2237 2238 /** 2239 * Notify listeners that event was received. 2240 */ signalEventReceived()2241 public synchronized void signalEventReceived() { 2242 mEventReceived = true; 2243 notifyAll(); 2244 } 2245 } 2246 2247 @Override handoverInitiationComplete()2248 public void handoverInitiationComplete() { 2249 mHandoverHandshake.signalEventReceived(); 2250 } 2251 2252 @Override completeHandover()2253 public void completeHandover() { 2254 CLog.logAndDisplay(LogLevel.INFO, "Completing handover."); 2255 if (mRemoteClient != null) { 2256 mRemoteClient.close(); 2257 mRemoteClient = null; 2258 } else { 2259 CLog.e("invalid state: received handover complete when remote client is null"); 2260 } 2261 2262 if (mRemoteManager != null) { 2263 mRemoteManager.cancelAndWait(); 2264 mRemoteManager = null; 2265 } else { 2266 CLog.e("invalid state: received handover complete when remote manager is null"); 2267 } 2268 2269 // Start a new remote manager and attempt to capture the original default port. 2270 mRemoteManager = new RemoteManager(getDeviceManager(), this); 2271 boolean success = false; 2272 for (int i=0; i < 10 && !success; i++) { 2273 try { 2274 sleep(2000); 2275 } catch (InterruptedException e) { 2276 CLog.e(e); 2277 return; 2278 } 2279 success = mRemoteManager.connect(); 2280 } 2281 if (!success) { 2282 CLog.e("failed to connect to default remote manager port"); 2283 return; 2284 } 2285 2286 mRemoteManager.start(); 2287 CLog.logAndDisplay(LogLevel.INFO, 2288 "Successfully started remote manager after handover on port %d", 2289 mRemoteManager.getPort()); 2290 } 2291 initiateHandover(int port)2292 private void initiateHandover(int port) throws RemoteException { 2293 mRemoteClient = RemoteClient.connect(port); 2294 CLog.i("Connecting local client with existing remote TF at %d - Attempting takeover", port); 2295 // Start up a temporary local remote manager for handover. 2296 if (mRemoteManager.connectAnyPort()) { 2297 mRemoteManager.start(); 2298 CLog.logAndDisplay(LogLevel.INFO, 2299 "Started local tmp remote manager for handover at port %d", 2300 mRemoteManager.getPort()); 2301 mRemoteClient.sendStartHandover(mRemoteManager.getPort()); 2302 } 2303 } 2304 assertStarted()2305 private synchronized void assertStarted() { 2306 if(!mStarted) { 2307 throw new IllegalStateException("start() must be called before this method"); 2308 } 2309 } 2310 2311 @Override notifyFileChanged(File cmdFile, List<String> extraArgs)2312 public void notifyFileChanged(File cmdFile, List<String> extraArgs) { 2313 CLog.logAndDisplay(LogLevel.INFO, "Detected update for cmdfile '%s'. Reloading", 2314 cmdFile.getAbsolutePath()); 2315 removeCommandsFromFile(cmdFile); 2316 try { 2317 // just add the file again, including re-registering for command file watcher 2318 // don't want to remove the registration here in case file fails to load 2319 internalAddCommandFile(cmdFile, extraArgs); 2320 } catch (ConfigurationException e) { 2321 CLog.wtf(String.format("Failed to automatically reload cmdfile %s", 2322 cmdFile.getAbsolutePath()), e); 2323 } 2324 } 2325 2326 /** 2327 * Set the command file reloading flag. 2328 */ 2329 @VisibleForTesting setCommandFileReload(boolean b)2330 void setCommandFileReload(boolean b) { 2331 mReloadCmdfiles = b; 2332 } 2333 getAllCommandsSize()2334 synchronized int getAllCommandsSize() { 2335 return mReadyCommands.size() + mExecutingCommands.size() + mSleepingCommands.size(); 2336 } 2337 getAllCommands()2338 synchronized List<ExecutableCommandState> getAllCommands() { 2339 List<ExecutableCommandState> cmds = new ArrayList<>(getAllCommandsSize()); 2340 for (ExecutableCommand cmd : mExecutingCommands) { 2341 cmds.add(new ExecutableCommandState(cmd, CommandState.EXECUTING)); 2342 } 2343 for (ExecutableCommand cmd : mReadyCommands) { 2344 cmds.add(new ExecutableCommandState(cmd, CommandState.WAITING_FOR_DEVICE)); 2345 } 2346 for (ExecutableCommand cmd : mSleepingCommands) { 2347 cmds.add(new ExecutableCommandState(cmd, CommandState.SLEEPING)); 2348 } 2349 return cmds; 2350 } 2351 2352 @Override shouldShutdownOnCmdfileError()2353 public boolean shouldShutdownOnCmdfileError() { 2354 return mShutdownOnCmdfileError; 2355 } 2356 getShutdownTimeout()2357 public long getShutdownTimeout() { 2358 return mShutdownTimeout; 2359 } 2360 2361 @Override getLastInvocationExitCode()2362 public ExitCode getLastInvocationExitCode() { 2363 return mLastInvocationExitCode; 2364 } 2365 2366 @Override getLastInvocationThrowable()2367 public Throwable getLastInvocationThrowable() { 2368 return mLastInvocationThrowable; 2369 } 2370 2371 @Override setLastInvocationExitCode(ExitCode code, Throwable throwable)2372 public void setLastInvocationExitCode(ExitCode code, Throwable throwable) { 2373 mLastInvocationExitCode = code; 2374 mLastInvocationThrowable = throwable; 2375 } 2376 2377 @Override getReadyCommandCount()2378 public synchronized int getReadyCommandCount() { 2379 return mReadyCommands.size(); 2380 } 2381 2382 @Override getExecutingCommandCount()2383 public synchronized int getExecutingCommandCount() { 2384 return mExecutingCommands.size(); 2385 } 2386 2387 @Override setClearcutClient(ClearcutClient client)2388 public void setClearcutClient(ClearcutClient client) { 2389 mClient = client; 2390 } 2391 } 2392