• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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