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