blob: 988d1a726973f1971c16b24c5cc3110827e8593d [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.command;
import com.android.ddmlib.DdmPreferences;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.clearcut.ClearcutClient;
import com.android.tradefed.command.CommandFileParser.CommandLine;
import com.android.tradefed.command.CommandFileWatcher.ICommandFileListener;
import com.android.tradefed.command.CommandRunner.ExitCode;
import com.android.tradefed.command.remote.DeviceDescriptor;
import com.android.tradefed.config.ArgsOptionParser;
import com.android.tradefed.config.Configuration;
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
import com.android.tradefed.config.DynamicRemoteFileResolver;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationFactory;
import com.android.tradefed.config.IDeviceConfiguration;
import com.android.tradefed.config.IGlobalConfiguration;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.RetryConfigurationFactory;
import com.android.tradefed.config.SandboxConfigurationFactory;
import com.android.tradefed.config.proxy.ProxyConfiguration;
import com.android.tradefed.config.proxy.TradefedDelegator;
import com.android.tradefed.device.DeviceAllocationState;
import com.android.tradefed.device.DeviceManager;
import com.android.tradefed.device.DeviceManager.FastbootDevice;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.FreeDeviceState;
import com.android.tradefed.device.IDeviceManager;
import com.android.tradefed.device.IDeviceMonitor;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.device.NoDeviceException;
import com.android.tradefed.device.NullDevice;
import com.android.tradefed.device.RemoteAndroidDevice;
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.device.TestDeviceState;
import com.android.tradefed.error.HarnessRuntimeException;
import com.android.tradefed.host.IHostOptions;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.IRescheduler;
import com.android.tradefed.invoker.ITestInvocation;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInvocation;
import com.android.tradefed.invoker.shard.ParentShardReplicate;
import com.android.tradefed.invoker.tracing.ActiveTrace;
import com.android.tradefed.invoker.tracing.CloseableTraceScope;
import com.android.tradefed.invoker.tracing.TracingLogger;
import com.android.tradefed.log.ILogRegistry.EventType;
import com.android.tradefed.log.LogRegistry;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ConsoleResultReporter;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ILogSaver;
import com.android.tradefed.result.ILogSaverListener;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.LogFile;
import com.android.tradefed.result.LogSaverResultForwarder;
import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.result.error.ErrorIdentifier;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.result.suite.SuiteResultReporter;
import com.android.tradefed.sandbox.ISandbox;
import com.android.tradefed.service.TradefedFeatureServer;
import com.android.tradefed.service.management.DeviceManagementGrpcServer;
import com.android.tradefed.service.management.TestInvocationManagementServer;
import com.android.tradefed.targetprep.DeviceFailedToBootError;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.SubprocessTfLauncher;
import com.android.tradefed.testtype.suite.retry.RetryRescheduler;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.Pair;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.SystemUtil;
import com.android.tradefed.util.TableFormatter;
import com.android.tradefed.util.TimeUtil;
import com.android.tradefed.util.hostmetric.IHostMonitor;
import com.android.tradefed.util.hostmetric.IHostMonitor.HostDataPoint;
import com.android.tradefed.util.hostmetric.IHostMonitor.HostMetricType;
import com.android.tradefed.util.keystore.IKeyStoreClient;
import com.android.tradefed.util.keystore.IKeyStoreFactory;
import com.android.tradefed.util.keystore.KeyStoreException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* A scheduler for running TradeFederation commands across all available devices.
*
* <p>Will attempt to prioritize commands to run based on a total running count of their execution
* time. e.g. infrequent or fast running commands will get prioritized over long running commands.
*
* <p>Runs forever in background until shutdown.
*/
public class CommandScheduler extends Thread implements ICommandScheduler, ICommandFileListener {
/** the queue of commands ready to be executed. */
private List<ExecutableCommand> mReadyCommands;
private Set<ExecutableCommand> mUnscheduledWarning;
/** the queue of commands sleeping. */
private Set<ExecutableCommand> mSleepingCommands;
/** the queue of commands current executing. */
private Set<ExecutableCommand> mExecutingCommands;
/** map of device to active invocation threads */
private Map<IInvocationContext, InvocationThread> mInvocationThreadMap;
/** Track invocation that are done and terminating */
private Map<IInvocationContext, InvocationThread> mInvocationThreadMapTerminating;
/** timer for scheduling commands to be re-queued for execution */
private ScheduledThreadPoolExecutor mCommandTimer;
private CommandFileWatcher mCommandFileWatcher = null;
/** latch used to notify other threads that this thread is running */
private final CountDownLatch mRunLatch;
/** Maximum time to wait for adb to initialize and get the physical devices discovered */
private static final long ADB_INIT_TIME_MS = 25;
/** used to assign unique ids to each CommandTracker created */
private int mCurrentCommandId = 0;
/** flag for instructing scheduler to exit when no commands are present */
private boolean mShutdownOnEmpty = false;
/** Prevent new tests from being scheduled. */
private boolean mStopScheduling = false;
private boolean mStarted = false;
private WaitObj mCommandProcessWait = new WaitObj();
/** The last {@link InvocationThread} that ran error code and error stack */
private ExitCode mLastInvocationExitCode = ExitCode.NO_ERROR;
private Throwable mLastInvocationThrowable = null;
/** Client to report metric data of the harness. */
private ClearcutClient mClient = null;
@Option(
name = "reload-cmdfiles",
description = "Whether to enable the command file autoreload mechanism")
// FIXME: enable this to be enabled or disabled on a per-cmdfile basis
private boolean mReloadCmdfiles = false;
@Option(
name = "max-poll-time",
description = "ms between forced command scheduler execution time")
private long mPollTime = 60 * 1000; // 60 seconds
@Option(
name = "shutdown-on-cmdfile-error",
description =
"terminate TF session if a configuration exception on command file occurs")
private boolean mShutdownOnCmdfileError = false;
@Option(
name = "shutdown-delay",
description =
"maximum time to wait before allowing final interruption of scheduler to"
+ " terminate TF session. If value is zero, interruption will only be"
+ " triggered when Invocation become interruptible. (Default behavior).",
isTimeVal = true)
private long mShutdownTimeout = 0;
private HostState mHostState = HostState.UNKNOWN;
private enum CommandState {
WAITING_FOR_DEVICE("Wait_for_device"),
EXECUTING("Executing"),
SLEEPING("Sleeping");
private final String mDisplayName;
CommandState(String displayName) {
mDisplayName = displayName;
}
public String getDisplayName() {
return mDisplayName;
}
}
/** Enums of different status of host */
public enum HostState {
UNKNOWN,
RUNNING,
QUITTING,
KILLING;
}
private void setHostState(HostState state) {
mHostState = state;
}
public HostState getHostState() {
return mHostState;
}
/**
* Represents one active command added to the scheduler. Will track total execution time of all
* instances of this command
*/
static class CommandTracker {
private final int mId;
private final String[] mArgs;
private final String mCommandFilePath;
private long mCount;
/** the total amount of time this command was executing. Used to prioritize */
private long mTotalExecTime = 0;
CommandTracker(int id, String[] args, String commandFilePath) {
mId = id;
mArgs = args;
mCommandFilePath = commandFilePath;
mCount = 0;
}
synchronized void incrementExecTime(long execTime) {
mTotalExecTime += execTime;
}
/**
* @return the total amount of execution time for this command.
*/
synchronized long getTotalExecTime() {
return mTotalExecTime;
}
/** Get the full list of config arguments associated with this command. */
String[] getArgs() {
return mArgs;
}
int getId() {
return mId;
}
/** Return the path of the file this command is associated with. null if not applicable */
String getCommandFilePath() {
return mCommandFilePath;
}
public void incrementScheduledCount() {
mCount++;
}
public long getScheduledCount() {
return mCount;
}
}
/** Represents one instance of a command to be executed. */
private class ExecutableCommand {
private final CommandTracker mCmdTracker;
private final IConfiguration mConfig;
private final boolean mRescheduled;
private final long mCreationTime;
private Long mSleepTime;
private ExecutableCommand(
CommandTracker tracker, IConfiguration config, boolean rescheduled) {
mConfig = config;
mCmdTracker = tracker;
mRescheduled = rescheduled;
mCreationTime = System.currentTimeMillis();
}
/** Gets the {@link IConfiguration} for this command instance */
public IConfiguration getConfiguration() {
return mConfig;
}
/** Gets the associated {@link CommandTracker}. */
CommandTracker getCommandTracker() {
return mCmdTracker;
}
/** Callback to inform listener that command has started execution. */
void commandStarted() {
mSleepTime = null;
}
public void commandFinished(long elapsedTime) {
getCommandTracker().incrementExecTime(elapsedTime);
CLog.d("removing exec command for id %d", getCommandTracker().getId());
synchronized (CommandScheduler.this) {
mExecutingCommands.remove(this);
}
if (isShuttingDown()) {
mCommandProcessWait.signalEventReceived();
}
}
public boolean isRescheduled() {
return mRescheduled;
}
public long getCreationTime() {
return mCreationTime;
}
public boolean isLoopMode() {
return mConfig.getCommandOptions().isLoopMode();
}
public long getMaxLoopCount() {
return mConfig.getCommandOptions().getMaxLoopCount();
}
public Long getSleepTime() {
return mSleepTime;
}
public String getCommandFilePath() {
return mCmdTracker.getCommandFilePath();
}
}
/** struct for a command and its state */
private static class ExecutableCommandState {
final ExecutableCommand cmd;
final CommandState state;
ExecutableCommandState(ExecutableCommand cmd, CommandState state) {
this.cmd = cmd;
this.state = state;
}
}
/** A {@link IRescheduler} that will add a command back to the queue. */
private class Rescheduler implements IRescheduler {
private CommandTracker mCmdTracker;
Rescheduler(CommandTracker cmdTracker) {
mCmdTracker = cmdTracker;
}
/** {@inheritDoc} */
@Override
public boolean scheduleConfig(IConfiguration config) {
// force loop mode to off, otherwise each rescheduled config will be treated as
// a new command and added back to queue
config.getCommandOptions().setLoopMode(false);
ExecutableCommand rescheduledCmd = createExecutableCommand(mCmdTracker, config, true);
return addExecCommandToQueue(rescheduledCmd, 0);
}
/** {@inheritDoc} */
@Override
public boolean rescheduleCommand() {
try {
CLog.d("rescheduling for command %d", mCmdTracker.getId());
IConfiguration config =
getConfigFactory().createConfigurationFromArgs(mCmdTracker.getArgs());
ExecutableCommand execCmd = createExecutableCommand(mCmdTracker, config, true);
return addExecCommandToQueue(execCmd, config.getCommandOptions().getLoopTime());
} catch (ConfigurationException e) {
// FIXME: do this with jline somehow for ANSI support
// note: make sure not to log (aka record) this line, as (args) may contain
// passwords.
System.out.println(
String.format(
"Error while processing args: %s",
Arrays.toString(mCmdTracker.getArgs())));
System.out.println(e.getMessage());
System.out.println();
return false;
}
}
}
/**
* Comparator for {@link ExecutableCommand}.
*
* <p>Delegates to {@link CommandTrackerTimeComparator}.
*/
private static class ExecutableCommandComparator implements Comparator<ExecutableCommand> {
CommandTrackerTimeComparator mTrackerComparator = new CommandTrackerTimeComparator();
/** {@inheritDoc} */
@Override
public int compare(ExecutableCommand c1, ExecutableCommand c2) {
return mTrackerComparator.compare(c1.getCommandTracker(), c2.getCommandTracker());
}
}
/**
* Comparator for {@link CommandTracker}.
*
* <p>Compares by mTotalExecTime, prioritizing configs with lower execution time
*/
private static class CommandTrackerTimeComparator implements Comparator<CommandTracker> {
@Override
public int compare(CommandTracker c1, CommandTracker c2) {
if (c1.getTotalExecTime() == c2.getTotalExecTime()) {
return 0;
} else if (c1.getTotalExecTime() < c2.getTotalExecTime()) {
return -1;
} else {
return 1;
}
}
}
/**
* Comparator for {@link CommandTracker}.
*
* <p>Compares by id.
*/
static class CommandTrackerIdComparator implements Comparator<CommandTracker> {
@Override
public int compare(CommandTracker c1, CommandTracker c2) {
if (c1.getId() == c2.getId()) {
return 0;
} else if (c1.getId() < c2.getId()) {
return -1;
} else {
return 1;
}
}
}
/**
* An {@link com.android.tradefed.command.ICommandScheduler.IScheduledInvocationListener} for
* locally scheduled commands added via addCommand.
*
* <p>Returns device to device manager and remote handover server if applicable.
*/
private class FreeDeviceHandler extends ResultForwarder
implements IScheduledInvocationListener {
private final IDeviceManager mDeviceManager;
private boolean mDeviceReleased = false;
private Map<ITestDevice, FreeDeviceState> mDevicesStates = null;
FreeDeviceHandler(IDeviceManager deviceManager, IScheduledInvocationListener... listeners) {
super(listeners);
mDeviceManager = deviceManager;
}
@Override
public void releaseDevices(
IInvocationContext context, Map<ITestDevice, FreeDeviceState> devicesStates) {
if (mDeviceReleased) {
return;
}
mDevicesStates = devicesStates;
for (ITestDevice device : context.getDevices()) {
mDeviceManager.freeDevice(device, devicesStates.get(device));
}
mDeviceReleased = true;
}
@Override
public void invocationComplete(
IInvocationContext context, Map<ITestDevice, FreeDeviceState> devicesStates) {
if (mDevicesStates != null) {
devicesStates = mDevicesStates;
}
for (ITestInvocationListener listener : getListeners()) {
try {
((IScheduledInvocationListener) listener)
.invocationComplete(context, devicesStates);
} catch (Exception e) {
CLog.e("Exception during invocationComplete:");
CLog.e(e);
}
}
releaseDevices(context, devicesStates);
}
}
/** A custom {@link FreeDeviceHandler} that frees only {@link NullDevice}. */
private class FreeNullDeviceHandler extends FreeDeviceHandler {
private final IDeviceManager mDeviceManager;
private boolean mDeviceReleased = false;
FreeNullDeviceHandler(
IDeviceManager deviceManager, IScheduledInvocationListener... listeners) {
super(deviceManager, listeners);
mDeviceManager = deviceManager;
}
@Override
public void releaseDevices(
IInvocationContext context, Map<ITestDevice, FreeDeviceState> devicesStates) {
if (mDeviceReleased) {
return;
}
for (ITestDevice device : context.getDevices()) {
if (!(device.getIDevice() instanceof NullDevice)) {
continue;
}
mDeviceManager.freeDevice(device, devicesStates.get(device));
}
mDeviceReleased = true;
}
}
/** Monitor Class for {@link InvocationThread} to make sure it does not run for too long. */
private class InvocationThreadMonitor extends TimerTask {
private InvocationThread mInvocationThread = null;
private boolean mTriggered = false;
public InvocationThreadMonitor(InvocationThread toMonitor) {
mInvocationThread = toMonitor;
}
@Override
public void run() {
if (mInvocationThread != null) {
mTriggered = true;
mInvocationThread.stopInvocation(
"Invocation Timeout Reached.", InfraErrorIdentifier.INVOCATION_TIMEOUT);
}
}
public boolean isTriggered() {
return mTriggered;
}
}
private class InvocationThread extends Thread {
private static final int EXPECTED_THREAD_COUNT = 1;
private static final String INVOC_END_EVENT_ID_KEY = "id";
private static final String INVOC_END_EVENT_ELAPSED_KEY = "elapsed-time";
private static final String INVOC_END_EVENT_TAG_KEY = "test-tag";
private final IScheduledInvocationListener[] mListeners;
private final IInvocationContext mInvocationContext;
private final ExecutableCommand mCmd;
private final ITestInvocation mInvocation;
private final InvocationThreadMonitor mInvocationThreadMonitor;
private final Timer mExecutionTimer;
private long mStartTime = -1;
public InvocationThread(
String name,
IInvocationContext invocationContext,
ExecutableCommand command,
IScheduledInvocationListener... listeners) {
// create a thread group so LoggerRegistry can identify this as an invocationThread
super(new ThreadGroup(name), name);
mListeners = listeners;
mInvocationContext = invocationContext;
mCmd = command;
mInvocation = createRunInstance();
// Daemon timer
mExecutionTimer = new Timer(true);
mInvocationThreadMonitor = new InvocationThreadMonitor(this);
}
public long getStartTime() {
return mStartTime;
}
@Override
public void run() {
if (mClient != null) {
mClient.notifyTradefedInvocationStartEvent();
}
IConfiguration config = mCmd.getConfiguration();
if (config.getCommandOptions().isTracingEnabled()) {
long pid = ProcessHandle.current().pid();
long tid = Thread.currentThread().getId();
ActiveTrace trace = TracingLogger.createActiveTrace(pid, tid);
trace.startTracing(
config.getCommandOptions()
.getInvocationData()
.containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME));
}
// Set experimental flags
if (config.getCommandOptions().isExperimentEnabled()) {
for (Map.Entry<String, String> entry :
config.getCommandOptions().getExperimentalFlags().entrySet()) {
try {
String optionName = entry.getKey();
String optionValue = entry.getValue();
// Support map experiments, where optionValue is a key=value pair
int equalsIndex = optionValue.indexOf('=');
if (equalsIndex != -1) {
String mapKey = optionValue.substring(0, equalsIndex);
String mapValue = optionValue.substring(equalsIndex + 1);
config.injectOptionValue(optionName, mapKey, mapValue);
} else {
config.injectOptionValue(optionName, optionValue);
}
mInvocationContext.addInvocationAttribute(
"experiment:" + optionName, optionValue);
} catch (ConfigurationException e) {
CLog.e("Configuration Exception caught while setting experimental flags.");
CLog.e(e);
}
}
}
mStartTime = System.currentTimeMillis();
ITestInvocation instance = getInvocation();
instance.setClearcutClient(mClient);
try (CloseableTraceScope ignore = new CloseableTraceScope("init")) {
for (final IScheduledInvocationListener listener : mListeners) {
try {
listener.invocationInitiated(mInvocationContext);
} catch (Throwable anyException) {
CLog.e("Exception caught while calling invocationInitiated:");
CLog.e(anyException);
}
}
}
Exception trackDeviceException = null;
boolean lastInvocationSet = false;
try (CloseableTraceScope ignore = new CloseableTraceScope("test-invocation")) {
// Copy the command options invocation attributes to the invocation if it has not
// been already done.
if (!config.getConfigurationDescription().shouldUseSandbox()
&& !config.getCommandOptions().getInvocationData().isEmpty()) {
mInvocationContext.addInvocationAttributes(
config.getCommandOptions().getInvocationData());
}
mCmd.commandStarted();
long invocTimeout = config.getCommandOptions().getInvocationTimeout();
if (invocTimeout > 0) {
CLog.i("Setting a timer for the invocation in %sms", invocTimeout);
mExecutionTimer.schedule(mInvocationThreadMonitor, invocTimeout);
}
instance.invoke(
mInvocationContext,
config,
new Rescheduler(mCmd.getCommandTracker()),
mListeners);
} catch (DeviceUnresponsiveException e) {
CLog.w("Device %s is unresponsive. Reason: %s", e.getSerial(), e.getMessage());
trackDeviceException = e;
setLastInvocationExitCode(ExitCode.DEVICE_UNRESPONSIVE, e);
lastInvocationSet = true;
} catch (DeviceNotAvailableException e) {
CLog.w("Device %s is not available. Reason: %s", e.getSerial(), e.getMessage());
trackDeviceException = e;
setLastInvocationExitCode(ExitCode.DEVICE_UNAVAILABLE, e);
lastInvocationSet = true;
} catch (FatalHostError e) {
String errorMessage =
String.format("Fatal error occurred: %s, shutting down", e.getMessage());
CLog.wtf(errorMessage, e);
setLastInvocationExitCode(ExitCode.FATAL_HOST_ERROR, e);
lastInvocationSet = true;
shutdownHard(true, errorMessage, e.getErrorId());
} catch (Throwable e) {
setLastInvocationExitCode(ExitCode.THROWABLE_EXCEPTION, e);
lastInvocationSet = true;
CLog.e(e);
} finally {
mExecutionTimer.cancel();
if (mInvocationThreadMonitor.isTriggered()) {
CLog.e("Invocation reached its timeout. Cleaning up.");
}
long elapsedTime = System.currentTimeMillis() - mStartTime;
CLog.logAndDisplay(
LogLevel.INFO,
"Updating command %d with elapsed time %d ms",
mCmd.getCommandTracker().getId(),
elapsedTime);
try (CloseableTraceScope ignore = new CloseableTraceScope("finalize_invocation")) {
// remove invocation thread first so another invocation can be started on device
// when freed
removeInvocationThread(this);
checkStrayThreads();
Map<ITestDevice, FreeDeviceState> deviceStates =
createReleaseMap(mInvocationContext, trackDeviceException);
for (final IScheduledInvocationListener listener : mListeners) {
try {
listener.invocationComplete(mInvocationContext, deviceStates);
} catch (Throwable anyException) {
CLog.e("Exception caught while calling invocationComplete:");
CLog.e(anyException);
}
}
if (!lastInvocationSet && instance.getExitInfo() != null) {
setLastInvocationExitCode(
instance.getExitInfo().mExitCode, instance.getExitInfo().mStack);
}
if (getFeatureServer() != null) {
getFeatureServer().unregisterInvocation(config);
}
mCmd.commandFinished(elapsedTime);
logInvocationEndedEvent(
mCmd.getCommandTracker().getId(), elapsedTime, mInvocationContext);
}
CLog.logAndDisplay(LogLevel.INFO, "Finalizing the logger and invocation.");
ActiveTrace trace = TracingLogger.getActiveTrace();
if (trace != null) {
File traceFile = trace.finalizeTracing();
if (traceFile != null) {
ActiveTrace main = TracingLogger.getMainTrace();
if (main != null) {
main.addSubprocessTrace(traceFile);
}
logTrace(traceFile, config);
}
}
if (config.getCommandOptions().reportInvocationComplete()) {
LogSaverResultForwarder.reportEndHostLog(
config.getTestInvocationListeners(),
config.getLogSaver(),
TestInvocation.TRADEFED_INVOC_COMPLETE_HOST_LOG);
config.getLogOutput().closeLog();
LogRegistry.getLogRegistry().unregisterLogger();
}
clearTerminating(this);
}
}
/** Special handling to send the trace file from subprocess when needed. */
private void logTrace(File traceFile, IConfiguration config) {
if (config.getCommandOptions()
.getInvocationData()
.containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME)) {
CLog.logAndDisplay(LogLevel.INFO, "Sending trace from subprocess");
LogFile perfettoTrace =
new LogFile(traceFile.getAbsolutePath(), null, LogDataType.PERFETTO);
for (ITestInvocationListener listener : config.getTestInvocationListeners()) {
try {
if (listener instanceof ILogSaverListener) {
((ILogSaverListener) listener)
.logAssociation(ActiveTrace.TRACE_KEY, perfettoTrace);
}
} catch (Exception e) {
CLog.logAndDisplay(LogLevel.ERROR, e.getMessage());
CLog.e(e);
}
}
} else {
try (FileInputStreamSource source = new FileInputStreamSource(traceFile, true)) {
LogSaverResultForwarder.logFile(
config.getTestInvocationListeners(),
config.getLogSaver(),
source,
ActiveTrace.TRACE_KEY,
LogDataType.PERFETTO);
}
}
}
/** Check the number of thread in the ThreadGroup, only one should exists (itself). */
private void checkStrayThreads() {
int numThread = this.getThreadGroup().activeCount();
if (numThread == EXPECTED_THREAD_COUNT) {
// No stray thread detected at the end of invocation
return;
}
// After calling parallelStream(), Some ForkJoinWorkerThreads stay around.
// They are common pool workers and shouldn't be counted as stray threads.
List<Thread> listThreads = getListOfThreadsExcludingForkJoinWorkers(numThread);
numThread = listThreads.size();
if (numThread == EXPECTED_THREAD_COUNT) {
return;
}
List<String> cmd = Arrays.asList(mCmd.getCommandTracker().getArgs());
CLog.e(
"Stray thread detected for command %d, %s. %d threads instead of %d",
mCmd.getCommandTracker().getId(), cmd, numThread, EXPECTED_THREAD_COUNT);
// This is the best we have for debug, it prints to stdout.
CLog.e("List of remaining threads: %s", listThreads);
List<IHostMonitor> hostMonitors = GlobalConfiguration.getHostMonitorInstances();
if (hostMonitors != null) {
for (IHostMonitor hm : hostMonitors) {
HostDataPoint data = new HostDataPoint("numThread", numThread, cmd.toString());
hm.addHostEvent(HostMetricType.INVOCATION_STRAY_THREAD, data);
}
}
// printing to stderr will help to catch them.
System.err.println(
String.format(
"We have %s threads instead of 1: %s. Check the logs for list of "
+ "threads.",
numThread, listThreads));
}
private List<Thread> getListOfThreadsExcludingForkJoinWorkers(int numThread) {
Thread[] listThreads = new Thread[numThread];
this.getThreadGroup().enumerate(listThreads);
return Arrays.asList(listThreads).stream()
.filter(t -> !(t instanceof ForkJoinWorkerThread))
.filter(t -> !t.getName().contains("-pool-task"))
.collect(Collectors.toList());
}
/** Helper to log an invocation ended event. */
private void logInvocationEndedEvent(
int invocId, long elapsedTime, final IInvocationContext context) {
Map<String, String> args = new HashMap<>();
args.put(INVOC_END_EVENT_ID_KEY, Integer.toString(invocId));
args.put(INVOC_END_EVENT_ELAPSED_KEY, TimeUtil.formatElapsedTime(elapsedTime));
args.put(INVOC_END_EVENT_TAG_KEY, context.getTestTag());
// Add all the invocation attributes to the final event logging.
// UniqueMultiMap cannot be easily converted to Map so we just iterate.
for (String key : context.getAttributes().keySet()) {
args.put(key, context.getAttributes().get(key).get(0));
}
logEvent(EventType.INVOCATION_END, args);
}
ITestInvocation getInvocation() {
return mInvocation;
}
IInvocationContext getInvocationContext() {
return mInvocationContext;
}
/** Notify invocation on {@link CommandScheduler#shutdown()}. */
public void notifyInvocationStop(String message) {
getInvocation().notifyInvocationStopped(message);
}
/**
* Stops a running invocation. {@link CommandScheduler#shutdownHard()} will stop all running
* invocations.
*/
public void stopInvocation(String message) {
stopInvocation(message, null);
}
/**
* Stops a running invocation. {@link CommandScheduler#shutdownHard()} will stop all running
* invocations.
*/
public void stopInvocation(String message, ErrorIdentifier errorId) {
getInvocation().notifyInvocationForceStopped(message, errorId);
for (ITestDevice device : mInvocationContext.getDevices()) {
if (TestDeviceState.ONLINE.equals(device.getDeviceState())) {
// Kill all running processes on device.
if (!(device.getIDevice() instanceof StubDevice)) {
try {
device.executeShellCommand("am kill-all");
} catch (DeviceNotAvailableException e) {
CLog.e("failed to kill process on device %s", device.getSerialNumber());
CLog.e(e);
}
}
}
// Finish with device tear down: We try to ensure that regardless of the invocation
// state during the interruption we at least do minimal tear down of devices with
// their built-in clean up.
CLog.d("Attempting postInvocationTearDown in stopInvocation");
device.postInvocationTearDown(null);
}
// If invocation is not currently in an interruptible state we provide a timer
// after which it will become interruptible.
// If timeout is 0, we do not enforce future interruption.
if (getShutdownTimeout() != 0) {
RunUtil.getDefault().setInterruptibleInFuture(this, getShutdownTimeout());
}
RunUtil.getDefault().interrupt(this, message, errorId);
}
/**
* Disable the reporting from reporters that implements a non-default {@link
* ITestInvocationListener#invocationInterrupted()}. Should be called on shutdown.
*/
public void disableReporters() {
for (ITestInvocationListener listener :
mCmd.getConfiguration().getTestInvocationListeners()) {
listener.invocationInterrupted();
}
}
/**
* Checks whether the device battery level is above the required value to keep running the
* invocation.
*/
public void checkDeviceBatteryLevel() {
for (String deviceName : mInvocationContext.getDeviceConfigNames()) {
if (mCmd.getConfiguration().getDeviceConfigByName(deviceName).getDeviceOptions()
== null) {
CLog.d("No deviceOptions in the configuration, cannot do Battery level check");
return;
}
final Integer cutoffBattery =
mCmd.getConfiguration()
.getDeviceConfigByName(deviceName)
.getDeviceOptions()
.getCutoffBattery();
if (mInvocationContext.getDevice(deviceName) != null && cutoffBattery != null) {
final ITestDevice device = mInvocationContext.getDevice(deviceName);
Integer batteryLevel = device.getBattery();
if (batteryLevel == null) {
return;
}
CLog.d("device %s: battery level=%d%%", device.getSerialNumber(), batteryLevel);
// This logic is based on the assumption that batterLevel will be 0 or -1 if TF
// fails to fetch a valid battery level or the device is not using a battery.
// So batteryLevel=0 will not trigger a stop.
if (0 < batteryLevel && batteryLevel < cutoffBattery) {
if (RunUtil.getDefault().isInterruptAllowed()) {
CLog.i(
"Stopping %s: battery too low (%d%% < %d%%)",
getName(), batteryLevel, cutoffBattery);
stopInvocation(
String.format(
"battery too low (%d%% < %d%%)",
batteryLevel, cutoffBattery));
} else {
// In this case, the battery is check periodically by CommandScheduler
// so there will be more opportunity to terminate the invocation when
// it's interruptible.
CLog.w(
"device: %s has a low battery but is in uninterruptible state.",
device.getSerialNumber());
}
}
}
}
}
}
/** Create a map of the devices state so they can be released appropriately. */
public static Map<ITestDevice, FreeDeviceState> createReleaseMap(
IInvocationContext context, Throwable e) {
Map<ITestDevice, FreeDeviceState> deviceStates = new LinkedHashMap<>();
for (ITestDevice device : context.getDevices()) {
deviceStates.put(device, FreeDeviceState.AVAILABLE);
}
if (context.wasReleasedEarly()) {
// Logic was already used, no need to run through it again.
return deviceStates;
}
for (ITestDevice device : context.getDevices()) {
if ((device.getIDevice() instanceof StubDevice
&& !(device.getIDevice() instanceof FastbootDevice))
|| device instanceof RemoteAndroidDevice) {
// Never release stub and Tcp devices, otherwise they will disappear
// during deallocation since they are only placeholder.
deviceStates.put(device, FreeDeviceState.AVAILABLE);
} else {
TestDeviceState deviceState = device.getDeviceState();
CLog.d(
"TestDeviceState for releasing '%s(%s)' is '%s'",
device.getSerialNumber(), device.getClass(), deviceState);
if (SystemUtil.isLocalMode()) {
// Locally release directly
deviceStates.put(device, FreeDeviceState.AVAILABLE);
} else if (!TestDeviceState.ONLINE.equals(deviceState)) {
// If the device is offline at the end of the test
deviceStates.put(device, FreeDeviceState.UNAVAILABLE);
} else if (!device.waitForDeviceShell(30000L)) {
// If device cannot pass basic shell responsiveness test.
deviceStates.put(device, FreeDeviceState.UNAVAILABLE);
}
}
// Reset the recovery mode at the end of the invocation.
device.setRecoveryMode(RecoveryMode.AVAILABLE);
}
// For releasing, also investigate cause for possible DNAE
if (e instanceof DeviceFailedToBootError
&& e.getCause() instanceof DeviceNotAvailableException) {
e = e.getCause();
}
DeviceNotAvailableException dnae = null;
FreeDeviceState unavailable = null;
if (e instanceof DeviceUnresponsiveException) {
dnae = (DeviceUnresponsiveException) e;
unavailable = FreeDeviceState.UNRESPONSIVE;
} else if (e instanceof DeviceNotAvailableException) {
dnae = (DeviceNotAvailableException) e;
unavailable = FreeDeviceState.UNAVAILABLE;
}
if (dnae != null) {
// If we are carrying an INVOCATION_CANCELLED it has priority
if (!InfraErrorIdentifier.INVOCATION_CANCELLED.equals(dnae.getErrorId())) {
ITestDevice badDevice = context.getDeviceBySerial(dnae.getSerial());
if (badDevice != null && !(badDevice.getIDevice() instanceof StubDevice)) {
deviceStates.put(badDevice, unavailable);
}
} else {
CLog.d("Invocation cancelled detected.");
}
}
CLog.d("Release map of the devices: %s", deviceStates);
return deviceStates;
}
/**
* A {@link IDeviceMonitor} that signals scheduler to process commands when an available device
* is added.
*/
private class AvailDeviceMonitor implements IDeviceMonitor {
@Override
public void run() {
// ignore
}
@Override
public void stop() {
// ignore
}
@Override
public void setDeviceLister(DeviceLister lister) {
// ignore
}
@Override
public void notifyDeviceStateChange(
String serial, DeviceAllocationState oldState, DeviceAllocationState newState) {
if (newState.equals(DeviceAllocationState.Available)) {
// new avail device was added, wake up scheduler
mCommandProcessWait.signalEventReceived();
}
}
}
/**
* Creates a {@link CommandScheduler}.
*
* <p>Note: start must be called before use.
*/
public CommandScheduler() {
super("CommandScheduler"); // set the thread name
mReadyCommands = new LinkedList<>();
mUnscheduledWarning = new HashSet<>();
mSleepingCommands = new HashSet<>();
mExecutingCommands = new HashSet<>();
mInvocationThreadMap = new HashMap<IInvocationContext, InvocationThread>();
mInvocationThreadMapTerminating = new HashMap<IInvocationContext, InvocationThread>();
// use a ScheduledThreadPoolExecutorTimer as a single-threaded timer. This class
// is used instead of a java.util.Timer because it offers advanced shutdown options
mCommandTimer = new ScheduledThreadPoolExecutor(1);
mRunLatch = new CountDownLatch(1);
}
/** Starts the scheduler including setting up of logging, init of {@link DeviceManager} etc */
@Override
public synchronized void start() {
synchronized (this) {
if (mStarted) {
throw new IllegalStateException("scheduler has already been started");
}
initLogging();
try (CloseableTraceScope ignored = new CloseableTraceScope("initDeviceManager")) {
initDeviceManager();
}
mStarted = true;
}
super.start();
setHostState(HostState.RUNNING);
}
/** {@inheritDoc} */
@Override
public synchronized CommandFileWatcher getCommandFileWatcher() {
assertStarted();
if (mCommandFileWatcher == null) {
mCommandFileWatcher = new CommandFileWatcher(this);
mCommandFileWatcher.start();
}
return mCommandFileWatcher;
}
/** Initialize the device manager, optionally using a global device filter if specified. */
void initDeviceManager() {
getDeviceManager().init();
if (getDeviceManager().waitForFirstDeviceAdded(ADB_INIT_TIME_MS)) {
// If a first device is added we wait a short extra time to allow more devices to be
// discovered.
RunUtil.getDefault().sleep(ADB_INIT_TIME_MS);
}
}
/**
* Factory method for creating a {@link TestInvocation}.
*
* @return the {@link ITestInvocation} to use
*/
ITestInvocation createRunInstance() {
return new TestInvocation();
}
/**
* Factory method for getting a reference to the {@link IDeviceManager}
*
* @return the {@link IDeviceManager} to use
*/
protected IDeviceManager getDeviceManager() {
return GlobalConfiguration.getDeviceManagerInstance();
}
protected IHostOptions getHostOptions() {
return GlobalConfiguration.getInstance().getHostOptions();
}
/**
* Factory method for getting a reference to the {@link IHostMonitor}
*
* @return the {@link IHostMonitor} to use
*/
List<IHostMonitor> getHostMonitor() {
return GlobalConfiguration.getHostMonitorInstances();
}
/**
* Factory method for getting a reference to the {@link IConfigurationFactory}
*
* @return the {@link IConfigurationFactory} to use
*/
protected IConfigurationFactory getConfigFactory() {
return ConfigurationFactory.getInstance();
}
protected TradefedFeatureServer getFeatureServer() {
return GlobalConfiguration.getInstance().getFeatureServer();
}
protected TestInvocationManagementServer getTestInvocationManagementServer() {
return GlobalConfiguration.getInstance().getTestInvocationManagementSever();
}
protected DeviceManagementGrpcServer getDeviceManagementServer() {
return GlobalConfiguration.getInstance().getDeviceManagementServer();
}
/**
* Fetches a {@link IKeyStoreClient} using the {@link IKeyStoreFactory} declared in {@link
* IGlobalConfiguration} or null if none is defined.
*
* @return IKeyStoreClient
*/
protected IKeyStoreClient getKeyStoreClient() {
try {
IKeyStoreFactory f = GlobalConfiguration.getInstance().getKeyStoreFactory();
if (f != null) {
try {
return f.createKeyStoreClient();
} catch (KeyStoreException e) {
CLog.e("Failed to create key store client");
CLog.e(e);
}
}
} catch (IllegalStateException e) {
CLog.w("Global configuration has not been created, failed to get keystore");
CLog.e(e);
}
return null;
}
/** The main execution block of this thread. */
@Override
public void run() {
assertStarted();
try {
IDeviceManager manager = getDeviceManager();
// Notify other threads that we're running.
mRunLatch.countDown();
// add a listener that will wake up scheduler when a new avail device is added
manager.addDeviceMonitor(new AvailDeviceMonitor());
while (!isShutdown()) {
// wait until processing is required again
mCommandProcessWait.waitAndReset(mPollTime);
checkInvocations();
try {
processReadyCommands(manager);
} catch (RuntimeException e) {
CLog.e(e);
Map<String, String> information = new HashMap<>();
information.put("Exception", "CommandScheduler");
information.put("stack", StreamUtil.getStackTrace(e));
logEvent(EventType.UNEXPECTED_EXCEPTION, information);
}
}
mCommandTimer.shutdown();
// We signal the device manager to stop device recovery threads because it could
// potentially create more invocations.
manager.terminateDeviceRecovery();
manager.terminateDeviceMonitor();
CLog.logAndDisplay(LogLevel.INFO, "Waiting for invocation threads to complete");
waitForAllInvocationThreads();
waitForTerminatingInvocationThreads();
exit(manager);
cleanUp();
CLog.logAndDisplay(LogLevel.INFO, "All done");
// Stop Feature Server after invocations are completed in case
// they still need it during shutdown.
if (getFeatureServer() != null) {
try {
getFeatureServer().shutdown();
} catch (InterruptedException e) {
CLog.e(e);
}
}
if (getDeviceManagementServer() != null) {
try {
getDeviceManagementServer().shutdown();
} catch (InterruptedException e) {
CLog.e(e);
}
}
// Stop TestInvocationManagementServer after invocations are completed as the client
// need the server to get invocation details.
if (getTestInvocationManagementServer() != null) {
try {
getTestInvocationManagementServer().shutdown();
} catch (InterruptedException e) {
CLog.e(e);
}
}
if (mClient != null) {
mClient.notifyTradefedFinishedEvent();
mClient.stop();
}
} finally {
// Make sure that we don't quit with messages still in the buffers
System.err.flush();
System.out.flush();
}
}
void checkInvocations() {
CLog.d("Checking invocations...");
final List<InvocationThread> copy;
synchronized (this) {
copy = new ArrayList<InvocationThread>(mInvocationThreadMap.values());
}
for (InvocationThread thread : copy) {
thread.checkDeviceBatteryLevel();
}
}
protected void processReadyCommands(IDeviceManager manager) {
CLog.d("processReadyCommands...");
Map<ExecutableCommand, IInvocationContext> scheduledCommandMap = new HashMap<>();
// minimize length of synchronized block by just matching commands with device first,
// then scheduling invocations/adding looping commands back to queue
synchronized (this) {
// sort ready commands by priority, so high priority commands are matched first
Collections.sort(mReadyCommands, new ExecutableCommandComparator());
Iterator<ExecutableCommand> cmdIter = mReadyCommands.iterator();
while (cmdIter.hasNext()) {
ExecutableCommand cmd = cmdIter.next();
IConfiguration config = cmd.getConfiguration();
IInvocationContext context = new InvocationContext();
context.setConfigurationDescriptor(config.getConfigurationDescription());
DeviceAllocationResult allocationResults = allocateDevices(config, manager);
if (allocationResults.wasAllocationSuccessful()) {
Map<String, ITestDevice> devices = allocationResults.getAllocatedDevices();
cmdIter.remove();
mExecutingCommands.add(cmd);
context.addAllocatedDevice(devices);
// track command matched with device
scheduledCommandMap.put(cmd, context);
// clean warned list to avoid piling over time.
mUnscheduledWarning.remove(cmd);
} else {
if (!mUnscheduledWarning.contains(cmd)) {
CLog.logAndDisplay(
LogLevel.DEBUG,
"No available device matching all the "
+ "config's requirements for cmd id %d.",
cmd.getCommandTracker().getId());
// make sure not to record since it may contains password
System.out.println(
String.format(
"Command will be rescheduled: %s",
Arrays.toString(cmd.getCommandTracker().getArgs())));
mUnscheduledWarning.add(cmd);
}
}
}
}
// now actually execute the commands
for (Map.Entry<ExecutableCommand, IInvocationContext> cmdDeviceEntry :
scheduledCommandMap.entrySet()) {
ExecutableCommand cmd = cmdDeviceEntry.getKey();
try (CloseableTraceScope ignored = new CloseableTraceScope("startInvocation")) {
startInvocation(
cmdDeviceEntry.getValue(), cmd, new FreeDeviceHandler(getDeviceManager()));
}
cmd.getCommandTracker().incrementScheduledCount();
if (cmd.isLoopMode()
&& cmd.getCommandTracker().getScheduledCount() < cmd.getMaxLoopCount()) {
addNewExecCommandToQueue(cmd.getCommandTracker());
}
}
CLog.d("done processReadyCommands...");
}
/** {@inheritDoc} */
@Override
public void await() throws InterruptedException {
while (mRunLatch.getCount() > 0) {
mRunLatch.await();
}
}
private void waitForThread(Thread thread) {
try {
thread.join();
} catch (InterruptedException e) {
// ignore
waitForThread(thread);
}
}
/** Wait until all invocation threads complete. */
private void waitForAllInvocationThreads() {
List<InvocationThread> threadListCopy;
synchronized (this) {
threadListCopy = new ArrayList<>();
threadListCopy.addAll(mInvocationThreadMap.values());
}
for (Thread thread : threadListCopy) {
waitForThread(thread);
}
}
private void waitForTerminatingInvocationThreads() {
List<InvocationThread> threadListCopy;
synchronized (this) {
threadListCopy = new ArrayList<>();
threadListCopy.addAll(mInvocationThreadMapTerminating.values());
}
for (Thread thread : threadListCopy) {
waitForThread(thread);
}
}
private void exit(IDeviceManager manager) {
if (manager != null) {
manager.terminate();
}
}
/** {@inheritDoc} */
@Override
public Pair<Boolean, Integer> addCommand(String[] args) throws ConfigurationException {
Integer cmdTracker = internalAddCommand(args, null);
return Pair.create(cmdTracker >= 0, cmdTracker);
}
/** Returns true if {@link CommandOptions#USE_SANDBOX} is part of the command line. */
private boolean isCommandSandboxed(String[] args) {
boolean foundSandbox = false;
// Since the order is important, mark the found sandbox when we find it, and unset it if
// we find the negation.
for (String arg : args) {
if (("--" + CommandOptions.USE_SANDBOX).equals(arg)) {
foundSandbox = true;
} else if (("--no-" + CommandOptions.USE_SANDBOX).equals(arg)) {
foundSandbox = false;
}
}
return foundSandbox;
}
private boolean isProxyCommand(String[] args) throws ConfigurationException {
ProxyConfiguration proxy = new ProxyConfiguration();
ArgsOptionParser argsParser = new ArgsOptionParser(proxy);
List<String> argsList = new ArrayList<>(Arrays.asList(args));
argsList.remove(0);
argsParser.parseBestEffort(argsList, true);
return proxy.isProxySet();
}
private IConfiguration handleProxyCommand(String[] originalArgs) throws ConfigurationException {
IConfiguration config =
((ConfigurationFactory) getConfigFactory())
.createPartialConfigurationFromArgs(
originalArgs,
getKeyStoreClient(),
ImmutableSet.of(ProxyConfiguration.PROXY_CONFIG_TYPE_KEY),
null);
try {
config.resolveDynamicOptions(new DynamicRemoteFileResolver());
} catch (BuildRetrievalError e) {
throw new ConfigurationException(e.getMessage(), e);
}
ProxyConfiguration proxy =
(ProxyConfiguration)
config.getConfigurationObject(ProxyConfiguration.PROXY_CONFIG_TYPE_KEY);
if (proxy.getProxyConfig() == null) {
throw new ConfigurationException("No proxy configuration found.");
}
originalArgs[0] = proxy.getProxyConfig().getAbsolutePath();
return config;
}
/** Returns true if the configuration used is a retry one. */
private boolean isRetryCommand(IConfiguration config) {
// If a configuration is made of the RetryRunner only, it is meant to run as a retry.
if (config.getTests().size() != 1) {
return false;
}
IRemoteTest rerunner = config.getTests().get(0);
if (rerunner instanceof RetryRescheduler) {
return true;
}
return false;
}
/** Create a {@link ISandbox} that the invocation will use to run. */
public ISandbox createSandbox() {
return GlobalConfiguration.getInstance().getSandboxFactory().createSandbox();
}
protected IConfiguration createConfiguration(String[] args) throws ConfigurationException {
TradefedDelegator delegator = checkDelegation(args);
if (delegator.shouldUseDelegation()) {
args = TradefedDelegator.clearCommandline(args);
// Do not use delegation on staging
if (!delegator.isStaging()) {
delegator.setCommandLine(args);
CLog.d("Using commandline arguments as starting command: %s", Arrays.asList(args));
IConfiguration config =
((ConfigurationFactory) getConfigFactory())
.createPartialConfigurationFromArgs(
args,
getKeyStoreClient(),
ImmutableSet.of(
Configuration.DEVICE_REQUIREMENTS_TYPE_NAME,
Configuration.LOGGER_TYPE_NAME,
Configuration.LOG_SAVER_TYPE_NAME,
Configuration.RESULT_REPORTER_TYPE_NAME),
delegator);
setDelegateLevelReporting(config);
return config;
}
}
// check if the command should be sandboxed
if (isCommandSandboxed(args)) {
// Create an sandboxed configuration based on the sandbox of the scheduler.
ISandbox sandbox = createSandbox();
return SandboxConfigurationFactory.getInstance()
.createConfigurationFromArgs(args, getKeyStoreClient(), sandbox, new RunUtil());
}
if (isProxyCommand(args)) {
IConfiguration proxyConfig = handleProxyCommand(args);
String[] argsWithoutDelegation = ProxyConfiguration.clearCommandline(args);
IConfiguration resolvedConfig = null;
try {
resolvedConfig =
getConfigFactory()
.createConfigurationFromArgs(
argsWithoutDelegation, null, getKeyStoreClient());
} catch (ConfigurationException e) {
proxyConfig.cleanConfigurationData();
throw e;
}
resolvedConfig.addFilesToClean(proxyConfig.getFilesToClean());
return resolvedConfig;
}
IConfiguration config =
getConfigFactory().createConfigurationFromArgs(args, null, getKeyStoreClient());
if (isRetryCommand(config)) {
return RetryConfigurationFactory.getInstance().createRetryConfiguration(config);
}
return config;
}
/**
* Create a delegator based on the command line to see if we need to delegate the run.
*
* @throws ConfigurationException
*/
public static TradefedDelegator checkDelegation(String[] args) throws ConfigurationException {
TradefedDelegator delegator = new TradefedDelegator();
ArgsOptionParser argsParser = new ArgsOptionParser(delegator);
List<String> argsList = new ArrayList<>(Arrays.asList(args));
argsList.remove(0);
argsParser.parseBestEffort(argsList, true);
return delegator;
}
private void setDelegateLevelReporting(IConfiguration config) {
List<ITestInvocationListener> delegateReporters = new ArrayList<>();
// For debugging in the console, add a printer
delegateReporters.add(new ConsoleResultReporter());
delegateReporters.add(new SuiteResultReporter());
try {
Class<?> objectClass =
Class.forName("com.google.android.tradefed.result.teststorage.ResultReporter");
Object infraReporter = objectClass.getDeclaredConstructor().newInstance();
delegateReporters.add((ITestInvocationListener) infraReporter);
} catch (Exception e) {
CLog.e(e);
}
config.setTestInvocationListeners(delegateReporters);
try {
Class<?> objectClass =
Class.forName("com.google.android.tradefed.result.AndroidBuildApiLogSaver");
Object infraLogger = objectClass.getDeclaredConstructor().newInstance();
config.setLogSaver((ILogSaver) infraLogger);
} catch (Exception e) {
CLog.e(e);
}
}
/**
* Adds a command.
*
* @param args the config arguments.
* @param cmdFilePath the filesystem path of command file
* @return Command tracker id (non-negative value) if the command was added successfully, return
* 0 when command is added for all devices. Otherwise -1.
*/
private Integer internalAddCommand(String[] args, String cmdFilePath)
throws ConfigurationException {
assertStarted();
IConfiguration config = createConfiguration(args);
if (config.getCommandOptions().isHelpMode()) {
getConfigFactory().printHelpForConfig(args, true, System.out);
} else if (config.getCommandOptions().isFullHelpMode()) {
getConfigFactory().printHelpForConfig(args, false, System.out);
} else if (config.getCommandOptions().isDryRunMode()) {
config.validateOptions();
String cmdLine = QuotationAwareTokenizer.combineTokens(args);
CLog.d("Dry run mode; skipping adding command: %s", cmdLine);
if (config.getCommandOptions().isNoisyDryRunMode()) {
System.out.println(cmdLine.replace("--noisy-dry-run", ""));
System.out.println("");
}
} else {
config.validateOptions();
if (config.getCommandOptions().runOnAllDevices()) {
addCommandForAllDevices(args, cmdFilePath);
return 0;
} else {
CommandTracker cmdTracker = createCommandTracker(args, cmdFilePath);
ExecutableCommand cmdInstance = createExecutableCommand(cmdTracker, config, false);
addExecCommandToQueue(cmdInstance, 0);
return cmdTracker.getId();
}
}
return -1;
}
/** {@inheritDoc} */
@Override
public void addCommandFile(String cmdFilePath, List<String> extraArgs)
throws ConfigurationException {
// verify we aren't already watching this command file, don't want to add it twice!
File cmdFile = new File(cmdFilePath);
if (mReloadCmdfiles && getCommandFileWatcher().isFileWatched(cmdFile)) {
CLog.logAndDisplay(
LogLevel.INFO,
"cmd file %s is already running and being watched for changes. Reloading",
cmdFilePath);
removeCommandsFromFile(cmdFile);
}
internalAddCommandFile(cmdFile, extraArgs);
}
/** Adds a command file without verifying if its already being watched */
private void internalAddCommandFile(File cmdFile, List<String> extraArgs)
throws ConfigurationException {
try {
CommandFileParser parser = createCommandFileParser();
List<CommandLine> commands = parser.parseFile(cmdFile);
if (mReloadCmdfiles) {
// note always should re-register for command file, even if already listening,
// since the dependent file list might have changed
getCommandFileWatcher().addCmdFile(cmdFile, extraArgs, parser.getIncludedFiles());
}
for (CommandLine command : commands) {
command.addAll(extraArgs);
String[] arrayCommand = command.asArray();
final String prettyCmdLine = QuotationAwareTokenizer.combineTokens(arrayCommand);
CLog.d("Adding command %s", prettyCmdLine);
try {
internalAddCommand(arrayCommand, cmdFile.getAbsolutePath());
} catch (ConfigurationException e) {
throw new ConfigurationException(
String.format(
"Failed to add command '%s': %s",
prettyCmdLine, e.getMessage()),
e);
}
}
} catch (IOException e) {
throw new ConfigurationException(
"Failed to read file " + cmdFile.getAbsolutePath(),
e,
InfraErrorIdentifier.CONFIGURATION_NOT_FOUND);
}
}
/**
* Factory method for creating a {@link CommandFileParser}.
*
* <p>Exposed for unit testing.
*/
CommandFileParser createCommandFileParser() {
return new CommandFileParser();
}
/**
* Creates a new command for each connected device, and adds each to the queue.
*
* <p>Note this won't have the desired effect if user has specified other conflicting {@link
* IConfiguration#getDeviceRequirements()}in the command.
*/
private void addCommandForAllDevices(String[] args, String cmdFilePath)
throws ConfigurationException {
List<DeviceDescriptor> deviceDescs = getDeviceManager().listAllDevices();
for (DeviceDescriptor deviceDesc : deviceDescs) {
if (!deviceDesc.isStubDevice()) {
String device = deviceDesc.getSerial();
String[] argsWithDevice = Arrays.copyOf(args, args.length + 2);
argsWithDevice[argsWithDevice.length - 2] = "-s";
argsWithDevice[argsWithDevice.length - 1] = device;
CommandTracker cmdTracker = createCommandTracker(argsWithDevice, cmdFilePath);
IConfiguration config =
getConfigFactory()
.createConfigurationFromArgs(
cmdTracker.getArgs(), null, getKeyStoreClient());
CLog.logAndDisplay(
LogLevel.INFO, "Scheduling '%s' on '%s'", cmdTracker.getArgs()[0], device);
config.getDeviceRequirements().setSerial(device);
ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
addExecCommandToQueue(execCmd, 0);
}
}
}
/** Creates a new {@link CommandTracker} with a unique id. */
private synchronized CommandTracker createCommandTracker(
String[] args, String commandFilePath) {
mCurrentCommandId++;
CLog.d(
"Creating command tracker id %d for command args: '%s'",
mCurrentCommandId, ArrayUtil.join(" ", (Object[]) args));
return new CommandTracker(mCurrentCommandId, args, commandFilePath);
}
/** Creates a new {@link ExecutableCommand}. */
private ExecutableCommand createExecutableCommand(
CommandTracker cmdTracker, IConfiguration config, boolean rescheduled) {
ExecutableCommand cmd = new ExecutableCommand(cmdTracker, config, rescheduled);
CLog.d("creating exec command for id %d", cmdTracker.getId());
return cmd;
}
/**
* Creates a new {@link ExecutableCommand}, and adds it to queue
*
* @param commandTracker
*/
private void addNewExecCommandToQueue(CommandTracker commandTracker) {
try {
IConfiguration config = createConfiguration(commandTracker.getArgs());
ExecutableCommand execCmd = createExecutableCommand(commandTracker, config, false);
addExecCommandToQueue(execCmd, config.getCommandOptions().getLoopTime());
} catch (ConfigurationException e) {
CLog.e(e);
}
}
/**
* Adds executable command instance to queue, with optional delay.
*
* @param cmd the {@link ExecutableCommand} to return to queue
* @param delayTime the time in ms to delay before adding command to queue
* @return <code>true</code> if command will be added to queue, <code>false</code> otherwise
*/
private synchronized boolean addExecCommandToQueue(
final ExecutableCommand cmd, long delayTime) {
// If the command is local sharding one being rescheduled, do not apply the shutdown yet
// This allows commandAndExit to still works with local sharding.
if (isShutdown()) {
if (cmd.getConfiguration()
.getConfigurationDescription()
.getMetaData(ConfigurationDescriptor.LOCAL_SHARDED_KEY)
== null) {
return false;
}
}
if (delayTime > 0) {
mSleepingCommands.add(cmd);
// delay before making command active
Runnable delayCommand =
new Runnable() {
@Override
public void run() {
synchronized (CommandScheduler.this) {
if (mSleepingCommands.remove(cmd)) {
mReadyCommands.add(cmd);
mCommandProcessWait.signalEventReceived();
}
}
}
};
mCommandTimer.schedule(delayCommand, delayTime, TimeUnit.MILLISECONDS);
} else {
mReadyCommands.add(cmd);
mCommandProcessWait.signalEventReceived();
}
return true;
}
/**
* Helper method to return an array of {@link String} elements as a readable {@link String}
*
* @param args the {@link String}[] to use
* @return a display friendly {@link String} of args contents
*/
private String getArgString(String[] args) {
return ArrayUtil.join(" ", (Object[]) args);
}
/** {@inheritDoc} */
@Override
public long execCommand(
IInvocationContext context, IScheduledInvocationListener listener, String[] args)
throws ConfigurationException, NoDeviceException {
return execCommand(context, listener, new ArrayList<ITestDevice>(), args);
}
/** {@inheritDoc} */
@Override
public long execCommand(
IScheduledInvocationListener listener, List<ITestDevice> reservedDevices, String[] args)
throws ConfigurationException {
return execCommand(null, listener, reservedDevices, args);
}
/**
* Determines if a given command is a dry-run. If the command is a dry-run, validate it. If
* there are any configs issue, it will throw a ConfigurationException.
*
* @param handler {@link InvocationEventHandler} to report events for dry-run validation.
* @param args the command to validate.
* @return true if the command are a dry run, false otherwise.
* @throws ConfigurationException
*/
protected void dryRunCommandReporting(
final IScheduledInvocationListener handler, IConfiguration config)
throws ConfigurationException {
IInvocationContext context = new InvocationContext();
context.addDeviceBuildInfo("stub", new BuildInfo());
handler.invocationStarted(context);
config.validateOptions();
handler.invocationEnded(0);
IInvocationContext nullMeta = null;
handler.invocationComplete(nullMeta, null);
}
@VisibleForTesting
protected long execCommand(
IInvocationContext context,
IScheduledInvocationListener listener,
List<ITestDevice> reservedDevices,
String[] args)
throws ConfigurationException {
assertStarted();
IDeviceManager manager = getDeviceManager();
IConfiguration config = createConfiguration(args);
config.validateOptions();
if (config.getCommandOptions().isDryRunMode()) {
dryRunCommandReporting(listener, config);
return -2L;
}
CommandTracker cmdTracker = createCommandTracker(args, null);
if (isShuttingDown()) {
if (context != null) {
// createConfiguration can be long for things like sandbox, so ensure we did not
// start a shutdown in the meantime.
CLog.w("Tradefed is shutting down, ignoring command.");
return -1;
}
// Prevent scheduling if we are shutting down
throw new ConfigurationException("Tradefed shutting down, skip scheduling.");
}
Map<String, ITestDevice> allocatedDevices = new LinkedHashMap<String, ITestDevice>();
if (!reservedDevices.isEmpty()) {
allocatedDevices = getAllocatedDevices(config.getDeviceConfig(), reservedDevices);
}
if (reservedDevices.size() < config.getDeviceConfig().size()) {
CLog.d(
"%s of %s devices reserved.",
reservedDevices.size(), config.getDeviceConfig().size());
// Allow devices (like NullDevices) and ignore already allocated devices (reserved).
DeviceAllocationResult allocationResults =
allocateDevices(config, manager, new ArrayList<>(allocatedDevices.keySet()));
if (!allocationResults.wasAllocationSuccessful()) {
// Log adb output just to help debug
if (getDeviceManager() instanceof DeviceManager) {
String adbOutput =
((DeviceManager) getDeviceManager()).executeGlobalAdbCommand("devices");
CLog.e("'adb devices' output:\n%s", adbOutput);
}
throw new NoDeviceException(
String.format(
"No device match for allocation. Reason: %s.\ncommand: %s",
allocationResults.formattedReason(), Arrays.asList(args)),
InfraErrorIdentifier.SCHEDULER_ALLOCATION_ERROR);
}
// Ensure devices (reserved or not) are ordered the same as in device config.
Map<String, ITestDevice> orderedDevices = new LinkedHashMap<String, ITestDevice>();
for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
String deviceName = deviceConfig.getDeviceName();
ITestDevice device = allocatedDevices.get(deviceName);
if (device == null) {
device = allocationResults.getAllocatedDevices().get(deviceName);
}
orderedDevices.put(deviceName, device);
}
allocatedDevices = orderedDevices;
}
ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
synchronized (this) {
mExecutingCommands.add(execCmd);
}
context = (context != null) ? context : createInvocationContext();
context.setConfigurationDescriptor(config.getConfigurationDescription());
context.addAllocatedDevice(allocatedDevices);
CLog.d("Executing '%s' on '%s'", cmdTracker.getArgs()[0], allocatedDevices);
if (reservedDevices.isEmpty()) {
startInvocation(context, execCmd, listener, new FreeDeviceHandler(manager));
} else {
// Free NullDevice only. Leave alone reserved devices.
startInvocation(context, execCmd, listener, new FreeNullDeviceHandler(manager));
}
return execCmd.getCommandTracker().getId();
}
/** Returns a map of device config name & device for reserved devices, excluding null devices */
Map<String, ITestDevice> getAllocatedDevices(
List<IDeviceConfiguration> deviceConfig, List<ITestDevice> reservedDevices) {
Map<String, ITestDevice> allocatedDevices = new LinkedHashMap<String, ITestDevice>();
int iReserved = 0;
for (IDeviceConfiguration config : deviceConfig) {
if (!config.getDeviceRequirements().nullDeviceRequested()) {
if (iReserved >= reservedDevices.size()) {
throw new NoDeviceException(
String.format(
"Reserved devices (%s) fewer than required.",
reservedDevices.size()),
InfraErrorIdentifier.SCHEDULER_ALLOCATION_ERROR);
}
allocatedDevices.put(config.getDeviceName(), reservedDevices.get(iReserved++));
}
}
if (iReserved < reservedDevices.size()) {
throw new NoDeviceException(
String.format(
"Reserved devices (%s) more than required (%s).",
reservedDevices.size(), iReserved),
InfraErrorIdentifier.SCHEDULER_ALLOCATION_ERROR);
}
return allocatedDevices;
}
/** {@inheritDoc} */
@Override
public long execCommand(
IScheduledInvocationListener listener, ITestDevice device, String[] args)
throws ConfigurationException {
return execCommand(listener, Arrays.asList(device), args);
}
/** {@inheritDoc} */
@Override
public long execCommand(IScheduledInvocationListener listener, String[] args)
throws ConfigurationException, NoDeviceException {
return execCommand(createInvocationContext(), listener, args);
}
/**
* Allocate devices for a config.
*
* @param config a {@link IConfiguration} has device requirements.
* @param manager a {@link IDeviceManager}
* @return allocated devices
*/
DeviceAllocationResult allocateDevices(IConfiguration config, IDeviceManager manager) {
return allocateDevices(config, manager, new ArrayList<String>());
}
DeviceAllocationResult allocateDevices(
IConfiguration config, IDeviceManager manager, ArrayList<String> excludeDevices) {
Map<String, ITestDevice> devices = new LinkedHashMap<String, ITestDevice>();
ITestDevice device = null;
if (config.getDeviceConfig().isEmpty()) {
return null;
}
// If we need to replicate the setup on all devices
ParentShardReplicate.replicatedSetup(config, getKeyStoreClient());
synchronized (this) {
DeviceAllocationResult allocationResults = new DeviceAllocationResult();
for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
if (excludeDevices.contains(deviceConfig.getDeviceName())) {
continue;
}
device =
manager.allocateDevice(
deviceConfig.getDeviceRequirements(), deviceConfig.isFake());
if (device != null) {
devices.put(deviceConfig.getDeviceName(), device);
} else {
allocationResults.addAllocationFailureReason(
deviceConfig.getDeviceName(),
deviceConfig.getDeviceRequirements().getNoMatchReason());
// If one of the several device cannot be allocated, we de-allocate
// all the previous one.
for (ITestDevice allocatedDevice : devices.values()) {
FreeDeviceState deviceState = FreeDeviceState.AVAILABLE;
if (allocatedDevice.getIDevice() instanceof StubDevice) {
deviceState = FreeDeviceState.AVAILABLE;
} else if (!TestDeviceState.ONLINE.equals(
allocatedDevice.getDeviceState())) {
// If the device is offline at the end of the test
deviceState = FreeDeviceState.UNAVAILABLE;
}
manager.freeDevice(allocatedDevice, deviceState);
}
// Could not allocate all devices
devices.clear();
break;
}
}
allocationResults.addAllocatedDevices(devices);
return allocationResults;
}
}
@VisibleForTesting
protected IInvocationContext createInvocationContext() {
return new InvocationContext();
}
/**
* Spawns off thread to run invocation for given device.
*
* @param context the {@link IInvocationContext}
* @param cmd the {@link ExecutableCommand} to execute
* @param listeners the {@link
* com.android.tradefed.command.ICommandScheduler.IScheduledInvocationListener}s to invoke
* when complete
*/
private void startInvocation(
IInvocationContext context,
ExecutableCommand cmd,
IScheduledInvocationListener... listeners) {
// Check if device is not used in another invocation.
try {
throwIfDeviceInInvocationThread(context.getDevices());
} catch (Exception e) {
setLastInvocationExitCode(ExitCode.THROWABLE_EXCEPTION, e);
for (ITestDevice device : context.getDevices()) {
getDeviceManager().freeDevice(device, FreeDeviceState.AVAILABLE);
}
throw e;
}
int invocationId = cmd.getCommandTracker().getId();
CLog.d("starting invocation for command id %d", invocationId);
// Name invocation with first device serial
final String invocationName = String.format("Invocation-%s", context.getSerials().get(0));
InvocationThread invocationThread =
new InvocationThread(invocationName, context, cmd, listeners);
if (getFeatureServer() != null) {
getFeatureServer()
.registerInvocation(
cmd.getConfiguration(),
invocationThread.getThreadGroup(),
Arrays.asList(listeners));
}
// Link context and command
context.addInvocationAttribute(
IInvocationContext.INVOCATION_ID, Integer.toString(invocationId));
logInvocationStartedEvent(cmd.getCommandTracker(), context);
invocationThread.start();
addInvocationThread(invocationThread);
}
/** Helper to log an invocation started event. */
private void logInvocationStartedEvent(CommandTracker tracker, IInvocationContext context) {
Map<String, String> args = new HashMap<>();
args.put("id", Integer.toString(tracker.getId()));
int i = 0;
for (String serial : context.getSerials()) {
args.put("serial" + i, serial);
i++;
}
args.put("args", ArrayUtil.join(" ", Arrays.asList(tracker.getArgs())));
logEvent(EventType.INVOCATION_START, args);
}
/** Removes a {@link InvocationThread} from the active list. */
private synchronized void removeInvocationThread(InvocationThread invThread) {
mInvocationThreadMap.remove(invThread.getInvocationContext());
mInvocationThreadMapTerminating.put(invThread.getInvocationContext(), invThread);
}
private synchronized void clearTerminating(InvocationThread invThread) {
mInvocationThreadMapTerminating.remove(invThread.getInvocationContext());
}
private synchronized void throwIfDeviceInInvocationThread(List<ITestDevice> devices) {
for (ITestDevice device : devices) {
if (isDeviceInInvocationThread(device)) {
throw new HarnessRuntimeException(
String.format(
"Attempting invocation on device %s when one is already "
+ "running",
device.getSerialNumber()),
InfraErrorIdentifier.SCHEDULING_ERROR);
}
}
}
/** Adds a {@link InvocationThread} to the active list. */
private synchronized void addInvocationThread(InvocationThread invThread) {
mInvocationThreadMap.put(invThread.getInvocationContext(), invThread);
}
protected synchronized boolean isShutdown() {
return mCommandTimer.isShutdown() || (mShutdownOnEmpty && getAllCommandsSize() == 0);
}
public synchronized boolean isShuttingDown() {
return mCommandTimer.isShutdown() || mShutdownOnEmpty || mStopScheduling;
}
/** {@inheritDoc} */
@Override
public synchronized void stopScheduling() {
mStopScheduling = true;
setHostState(HostState.QUITTING);
}
/** {@inheritDoc} */
@Override
public synchronized void shutdown(boolean notifyStop) {
setHostState(HostState.QUITTING);
doShutdown();
if (notifyStop) {
String reason = "Tradefed is notified to stop";
for (InvocationThread thread : mInvocationThreadMap.values()) {
thread.notifyInvocationStop(reason);
}
}
}
private synchronized void doShutdown() {
assertStarted();
if (!isShuttingDown()) {
CLog.d("initiating shutdown");
removeAllCommands();
if (mCommandFileWatcher != null) {
mCommandFileWatcher.cancel();
}
if (mCommandTimer != null) {
mCommandTimer.shutdownNow();
}
mCommandProcessWait.signalEventReceived();
}
CLog.logAndDisplay(LogLevel.INFO, "Received shutdown request.");
}
/** {@inheritDoc} */
@Override
public synchronized void shutdownOnEmpty() {
if (!mStarted) {
CLog.w("Scheduler was not started, yet shutdownOnEmpty was called.");
return;
}
setHostState(HostState.QUITTING);
if (!isShuttingDown()) {
CLog.d("initiating shutdown on empty");
mShutdownOnEmpty = true;
mCommandProcessWait.signalEventReceived();
}
}
/** {@inheritDoc} */
@Override
public synchronized void removeAllCommands() {
assertStarted();
CLog.d("removing all commands");
if (mReloadCmdfiles) {
getCommandFileWatcher().removeAllFiles();
}
if (mCommandTimer != null) {
for (Runnable task : mCommandTimer.getQueue()) {
mCommandTimer.remove(task);
}
}
mReadyCommands.clear();
mSleepingCommands.clear();
if (isShuttingDown()) {
mCommandProcessWait.signalEventReceived();
}
}
/**
* Remove commands originally added via the given command file
*
* @param cmdFile
*/
private synchronized void removeCommandsFromFile(File cmdFile) {
Iterator<ExecutableCommand> cmdIter = mReadyCommands.iterator();
while (cmdIter.hasNext()) {
ExecutableCommand cmd = cmdIter.next();
String path = cmd.getCommandFilePath();
if (path != null && path.equals(cmdFile.getAbsolutePath())) {
cmdIter.remove();
}
}
cmdIter = mSleepingCommands.iterator();
while (cmdIter.hasNext()) {
ExecutableCommand cmd = cmdIter.next();
String path = cmd.getCommandFilePath();
if (path != null && path.equals(cmdFile.getAbsolutePath())) {
cmdIter.remove();
}
}
if (isShuttingDown()) {
mCommandProcessWait.signalEventReceived();
}
}
/**
* @return the list of active {@link CommandTracker}. 'Active' here means all commands added to
* the scheduler that are either executing, waiting for a device to execute on, or looping.
*/
List<CommandTracker> getCommandTrackers() {
List<ExecutableCommandState> cmdCopy = getAllCommands();
Set<CommandTracker> cmdTrackers = new LinkedHashSet<CommandTracker>();
for (ExecutableCommandState cmdState : cmdCopy) {
cmdTrackers.add(cmdState.cmd.getCommandTracker());
}
return new ArrayList<CommandTracker>(cmdTrackers);
}
/** {@inheritDoc} */
@Override
public synchronized void shutdownHard() {
shutdownHard(true);
}
private synchronized void shutdownHard(boolean killAdb, String reason, ErrorIdentifier error) {
setHostState(HostState.KILLING);
doShutdown();
CLog.logAndDisplay(LogLevel.WARN, "Stopping invocation threads...");
for (InvocationThread thread : mInvocationThreadMap.values()) {
thread.disableReporters();
// TODO(b/118891716): Improve tear down
thread.stopInvocation(reason, error);
}
if (killAdb) {
getDeviceManager().terminateHard(reason);
} else {
getDeviceManager().terminate();
}
}
/** {@inheritDoc} */
@Override
public synchronized void shutdownHard(boolean killAdb) {
String reason = "Tradefed is shutting down";
shutdownHard(killAdb, reason, InfraErrorIdentifier.TRADEFED_SHUTTING_DOWN);
}
/**
* Initializes the ddmlib log.
*
* <p>Exposed so unit tests can mock.
*/
@SuppressWarnings("deprecation")
protected void initLogging() {
DdmPreferences.setLogLevel(LogLevel.VERBOSE.getStringValue());
Log.setLogOutput(LogRegistry.getLogRegistry());
}
/**
* Closes the logs and does any other necessary cleanup before we quit.
*
* <p>Exposed so unit tests can mock.
*/
protected void cleanUp() {
LogRegistry.getLogRegistry().closeAndRemoveAllLogs();
}
/** log an event to the registry history logger. */
@VisibleForTesting
void logEvent(EventType event, Map<String, String> args) {
LogRegistry.getLogRegistry().logEvent(LogLevel.DEBUG, event, args);
}
/** {@inheritDoc} */
@Override
public void displayInvocationsInfo(PrintWriter printWriter) {
assertStarted();
if (mInvocationThreadMap == null || mInvocationThreadMap.size() == 0) {
return;
}
List<InvocationThread> copy = new ArrayList<InvocationThread>();
synchronized (this) {
copy.addAll(mInvocationThreadMap.values());
}
ArrayList<List<String>> displayRows = new ArrayList<List<String>>();
displayRows.add(Arrays.asList("Command Id", "Exec Time", "Device", "State"));
long curTime = System.currentTimeMillis();
for (InvocationThread invThread : copy) {
displayRows.add(
Arrays.asList(
Integer.toString(invThread.mCmd.getCommandTracker().getId()),
getTimeString(curTime - invThread.getStartTime()),
invThread.getInvocationContext().getSerials().toString(),
invThread.getInvocation().toString()));
}
new TableFormatter().displayTable(displayRows, printWriter);
}
private String getTimeString(long elapsedTime) {
long duration = elapsedTime / 1000;
long secs = duration % 60;
long mins = (duration / 60) % 60;
long hrs = duration / (60 * 60);
String time = "unknown";
if (hrs > 0) {
time = String.format("%dh:%02d:%02d", hrs, mins, secs);
} else {
time = String.format("%dm:%02d", mins, secs);
}
return time;
}
/** {@inheritDoc} */
@Override
public synchronized boolean stopInvocation(ITestInvocation invocation) {
for (InvocationThread thread : mInvocationThreadMap.values()) {
if (thread.getInvocation() == invocation) {
thread.interrupt();
return true;
}
}
return false;
}
/** {@inheritDoc} */
@Override
public synchronized boolean stopInvocation(int invocationId, String cause) {
// TODO: make invocationID part of InvocationContext
for (InvocationThread thread : mInvocationThreadMap.values()) {
if (thread.mCmd.getCommandTracker().mId == invocationId) {
if (cause == null) {
cause = "User requested stopping invocation " + invocationId;
}
thread.stopInvocation(cause);
return true;
}
}
CLog.w("No invocation found matching the id: %s", invocationId);
return false;
}
/** {@inheritDoc} */
@Override
public synchronized String getInvocationInfo(int invocationId) {
for (InvocationThread thread : mInvocationThreadMap.values()) {
if (thread.mCmd.getCommandTracker().mId == invocationId) {
String info = Arrays.toString(thread.mCmd.getCommandTracker().getArgs());
return info;
}
}
CLog.w("No invocation found matching the id: %s", invocationId);
return null;
}
/** {@inheritDoc} */
@Override
public void displayCommandsInfo(PrintWriter printWriter, String regex) {
assertStarted();
Pattern regexPattern = null;
if (regex != null) {
regexPattern = Pattern.compile(regex);
}
List<CommandTracker> cmds = getCommandTrackers();
Collections.sort(cmds, new CommandTrackerIdComparator());
for (CommandTracker cmd : cmds) {
String argString = getArgString(cmd.getArgs());
if (regexPattern == null || regexPattern.matcher(argString).find()) {
String cmdDesc =
String.format(
"Command %d: [%s] %s",
cmd.getId(), getTimeString(cmd.getTotalExecTime()), argString);
printWriter.println(cmdDesc);
}
}
}
/** {@inheritDoc} */
@Override
public void dumpCommandsXml(PrintWriter printWriter, String regex) {
assertStarted();
Pattern regexPattern = null;
if (regex != null) {
regexPattern = Pattern.compile(regex);
}
List<ExecutableCommandState> cmdCopy = getAllCommands();
for (ExecutableCommandState cmd : cmdCopy) {
String[] args = cmd.cmd.getCommandTracker().getArgs();
String argString = getArgString(args);
if (regexPattern == null || regexPattern.matcher(argString).find()) {
// Use the config name prefixed by config__ for the file path
String xmlPrefix = "config__" + args[0].replace("/", "__") + "__";
// If the command line contains --template:map test config, use that config for the
// file path. This is because in the template system, many tests will have same
// base config and the distinguishing feature is the test included.
boolean templateIncludeFound = false;
boolean testFound = false;
for (String arg : args) {
if ("--template:map".equals(arg)) {
templateIncludeFound = true;
} else if (templateIncludeFound && "test".equals(arg)) {
testFound = true;
} else {
if (templateIncludeFound && testFound) {
xmlPrefix = "config__" + arg.replace("/", "__") + "__";
}
templateIncludeFound = false;
testFound = false;
}
}
try {
File xmlFile = FileUtil.createTempFile(xmlPrefix, ".xml");
PrintWriter writer = new PrintWriter(xmlFile);
cmd.cmd.getConfiguration().dumpXml(writer);
printWriter.println(
String.format("Saved command dump to %s", xmlFile.getAbsolutePath()));
} catch (IOException e) {
// Log exception and continue
CLog.e("Could not dump config xml");
CLog.e(e);
}
}
}
}
/** {@inheritDoc} */
@Override
public void displayCommandQueue(PrintWriter printWriter) {
assertStarted();
List<ExecutableCommandState> cmdCopy = getAllCommands();
if (cmdCopy.size() == 0) {
return;
}
ArrayList<List<String>> displayRows = new ArrayList<List<String>>();
displayRows.add(
Arrays.asList(
"Id",
"Config",
"Created",
"Exec time",
"State",
"Sleep time",
"Rescheduled",
"Loop"));
long curTime = System.currentTimeMillis();
for (ExecutableCommandState cmd : cmdCopy) {
dumpCommand(curTime, cmd, displayRows);
}
new TableFormatter().displayTable(displayRows, printWriter);
}
private void dumpCommand(
long curTime, ExecutableCommandState cmdAndState, ArrayList<List<String>> displayRows) {
ExecutableCommand cmd = cmdAndState.cmd;
String sleepTime = cmd.getSleepTime() == null ? "N/A" : getTimeString(cmd.getSleepTime());
displayRows.add(
Arrays.asList(
Integer.toString(cmd.getCommandTracker().getId()),
cmd.getCommandTracker().getArgs()[0],
getTimeString(curTime - cmd.getCreationTime()),
getTimeString(cmd.mCmdTracker.getTotalExecTime()),
cmdAndState.state.getDisplayName(),
sleepTime,
Boolean.toString(cmd.isRescheduled()),
Boolean.toString(cmd.isLoopMode())));
}
/**
* Helper object for allowing multiple threads to synchronize on an event.
*
* <p>Basically a modest wrapper around Object's wait and notify methods, that supports
* remembering if a notify call was made.
*/
private static class WaitObj {
boolean mEventReceived = false;
/**
* Wait for signal for a max of given ms.
*
* @return true if event received before time elapsed, false otherwise
*/
public synchronized boolean waitForEvent(long maxWaitTime) {
if (maxWaitTime == 0) {
return waitForEvent();
}
long startTime = System.currentTimeMillis();
long remainingTime = maxWaitTime;
while (!mEventReceived && remainingTime > 0) {
try {
wait(remainingTime);
} catch (InterruptedException e) {
CLog.w("interrupted");
}
remainingTime = maxWaitTime - (System.currentTimeMillis() - startTime);
}
return mEventReceived;
}
/**
* Wait for signal indefinitely or until interrupted.
*
* @return true if event received, false otherwise
*/
public synchronized boolean waitForEvent() {
if (!mEventReceived) {
try {
wait();
} catch (InterruptedException e) {
CLog.w("interrupted");
}
}
return mEventReceived;
}
/** Reset the event received flag. */
public synchronized void reset() {
mEventReceived = false;
}
/**
* Wait for given ms for event to be received, and reset state back to 'no event received'
* upon completion.
*/
public synchronized void waitAndReset(long maxWaitTime) {
waitForEvent(maxWaitTime);
reset();
}
/** Notify listeners that event was received. */
public synchronized void signalEventReceived() {
mEventReceived = true;
notifyAll();
}
}
private synchronized void assertStarted() {
if (!mStarted) {
throw new IllegalStateException("start() must be called before this method");
}
}
@Override
public void notifyFileChanged(File cmdFile, List<String> extraArgs) {
CLog.logAndDisplay(
LogLevel.INFO,
"Detected update for cmdfile '%s'. Reloading",
cmdFile.getAbsolutePath());
removeCommandsFromFile(cmdFile);
try {
// just add the file again, including re-registering for command file watcher
// don't want to remove the registration here in case file fails to load
internalAddCommandFile(cmdFile, extraArgs);
} catch (ConfigurationException e) {
CLog.wtf(
String.format(
"Failed to automatically reload cmdfile %s", cmdFile.getAbsolutePath()),
e);
}
}
/** Set the command file reloading flag. */
@VisibleForTesting
void setCommandFileReload(boolean b) {
mReloadCmdfiles = b;
}
synchronized int getAllCommandsSize() {
return mReadyCommands.size() + mExecutingCommands.size() + mSleepingCommands.size();
}
synchronized List<ExecutableCommandState> getAllCommands() {
List<ExecutableCommandState> cmds = new ArrayList<>(getAllCommandsSize());
for (ExecutableCommand cmd : mExecutingCommands) {
cmds.add(new ExecutableCommandState(cmd, CommandState.EXECUTING));
}
for (ExecutableCommand cmd : mReadyCommands) {
cmds.add(new ExecutableCommandState(cmd, CommandState.WAITING_FOR_DEVICE));
}
for (ExecutableCommand cmd : mSleepingCommands) {
cmds.add(new ExecutableCommandState(cmd, CommandState.SLEEPING));
}
return cmds;
}
@Override
public boolean shouldShutdownOnCmdfileError() {
return mShutdownOnCmdfileError;
}
public long getShutdownTimeout() {
return mShutdownTimeout;
}
@Override
public ExitCode getLastInvocationExitCode() {
return mLastInvocationExitCode;
}
@Override
public Throwable getLastInvocationThrowable() {
return mLastInvocationThrowable;
}
private void setLastInvocationExitCode(ExitCode code, Throwable throwable) {
mLastInvocationExitCode = code;
mLastInvocationThrowable = throwable;
}
@Override
public synchronized int getReadyCommandCount() {
return mReadyCommands.size();
}
@Override
public synchronized int getExecutingCommandCount() {
return mExecutingCommands.size();
}
@Override
public void setClearcutClient(ClearcutClient client) {
mClient = client;
}
@Override
public synchronized boolean isDeviceInInvocationThread(ITestDevice device) {
for (IInvocationContext context : mInvocationThreadMap.keySet()) {
if (context.getDevices().contains(device)) {
return !context.wasReleasedEarly();
}
}
return false;
}
}