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