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 package com.android.tradefed.invoker;
17 
18 import com.android.ddmlib.Log.LogLevel;
19 import com.android.tradefed.build.BuildInfo;
20 import com.android.tradefed.build.BuildRetrievalError;
21 import com.android.tradefed.build.IBuildInfo;
22 import com.android.tradefed.command.CommandRunner.ExitCode;
23 import com.android.tradefed.command.CommandScheduler;
24 import com.android.tradefed.command.ICommandScheduler.IScheduledInvocationListener;
25 import com.android.tradefed.config.ConfigurationException;
26 import com.android.tradefed.config.DynamicRemoteFileResolver;
27 import com.android.tradefed.config.GlobalConfiguration;
28 import com.android.tradefed.config.IConfiguration;
29 import com.android.tradefed.device.DeviceManager;
30 import com.android.tradefed.device.DeviceNotAvailableException;
31 import com.android.tradefed.device.DeviceUnresponsiveException;
32 import com.android.tradefed.device.FreeDeviceState;
33 import com.android.tradefed.device.ITestDevice;
34 import com.android.tradefed.device.ITestDevice.RecoveryMode;
35 import com.android.tradefed.device.NativeDevice;
36 import com.android.tradefed.device.StubDevice;
37 import com.android.tradefed.device.TcpDevice;
38 import com.android.tradefed.device.TestDeviceState;
39 import com.android.tradefed.device.cloud.ManagedRemoteDevice;
40 import com.android.tradefed.device.cloud.NestedRemoteDevice;
41 import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
42 import com.android.tradefed.error.HarnessException;
43 import com.android.tradefed.error.IHarnessException;
44 import com.android.tradefed.guice.InvocationScope;
45 import com.android.tradefed.invoker.logger.CurrentInvocation;
46 import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
47 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
48 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
49 import com.android.tradefed.invoker.logger.TfObjectTracker;
50 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecution;
51 import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
52 import com.android.tradefed.invoker.shard.LastShardDetector;
53 import com.android.tradefed.invoker.shard.ShardHelper;
54 import com.android.tradefed.log.BaseLeveledLogOutput;
55 import com.android.tradefed.log.ILeveledLogOutput;
56 import com.android.tradefed.log.ILogRegistry;
57 import com.android.tradefed.log.ITestLogger;
58 import com.android.tradefed.log.LogRegistry;
59 import com.android.tradefed.log.LogUtil.CLog;
60 import com.android.tradefed.log.StdoutLogger;
61 import com.android.tradefed.postprocessor.IPostProcessor;
62 import com.android.tradefed.result.ActionInProgress;
63 import com.android.tradefed.result.FailureDescription;
64 import com.android.tradefed.result.FileInputStreamSource;
65 import com.android.tradefed.result.ITestInvocationListener;
66 import com.android.tradefed.result.InputStreamSource;
67 import com.android.tradefed.result.LogDataType;
68 import com.android.tradefed.result.LogSaverResultForwarder;
69 import com.android.tradefed.result.ResultAndLogForwarder;
70 import com.android.tradefed.result.error.ErrorIdentifier;
71 import com.android.tradefed.result.error.InfraErrorIdentifier;
72 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
73 import com.android.tradefed.retry.IRetryDecision;
74 import com.android.tradefed.retry.ResultAggregator;
75 import com.android.tradefed.retry.RetryStrategy;
76 import com.android.tradefed.targetprep.BuildError;
77 import com.android.tradefed.targetprep.DeviceFailedToBootError;
78 import com.android.tradefed.targetprep.TargetSetupError;
79 import com.android.tradefed.testtype.SubprocessTfLauncher;
80 import com.android.tradefed.util.FileUtil;
81 import com.android.tradefed.util.IRunUtil;
82 import com.android.tradefed.util.PrettyPrintDelimiter;
83 import com.android.tradefed.util.RunInterruptedException;
84 import com.android.tradefed.util.RunUtil;
85 import com.android.tradefed.util.TimeUtil;
86 import com.android.tradefed.util.executor.ParallelDeviceExecutor;
87 
88 import com.google.common.annotations.VisibleForTesting;
89 
90 import java.io.File;
91 import java.io.IOException;
92 import java.util.ArrayList;
93 import java.util.Arrays;
94 import java.util.List;
95 import java.util.Map;
96 import java.util.Map.Entry;
97 import java.util.concurrent.Callable;
98 import java.util.concurrent.TimeUnit;
99 
100 /**
101  * Default implementation of {@link ITestInvocation}.
102  * <p/>
103  * Loads major objects based on {@link IConfiguration}
104  *   - retrieves build
105  *   - prepares target
106  *   - runs tests
107  *   - reports results
108  */
109 public class TestInvocation implements ITestInvocation {
110 
111     /** Key of the command line args attributes */
112     public static final String COMMAND_ARGS_KEY = "command_line_args";
113 
114     /**
115      * Format of the key in {@link IBuildInfo} to log the battery level for each step of the
116      * invocation. (Setup, test, tear down).
117      */
118     private static final String BATTERY_ATTRIBUTE_FORMAT_KEY = "%s-battery-%s";
119 
120     public static final String TRADEFED_LOG_NAME = "host_log";
121     public static final String TRADEFED_END_HOST_LOG = "end_host_log";
122     /** Suffix used on host_log for the part before sharding occurs. */
123     static final String BEFORE_SHARDING_SUFFIX = "_before_sharding";
124     static final String DEVICE_LOG_NAME_PREFIX = "device_logcat_";
125     static final String EMULATOR_LOG_NAME_PREFIX = "emulator_log_";
126     static final String BUILD_ERROR_BUGREPORT_NAME = "build_error_bugreport";
127     static final String DEVICE_UNRESPONSIVE_BUGREPORT_NAME = "device_unresponsive_bugreport";
128     static final String INVOCATION_ENDED_BUGREPORT_NAME = "invocation_ended_bugreport";
129     static final String TARGET_SETUP_ERROR_BUGREPORT_NAME = "target_setup_error_bugreport";
130     static final String BATT_TAG = "[battery level]";
131 
132     public enum Stage {
133         ERROR("error"),
134         SETUP("setup"),
135         TEST("test"),
136         TEARDOWN("teardown");
137 
138         private final String mName;
139 
Stage(String name)140         Stage(String name) {
141             mName = name;
142         }
143 
getName()144         public String getName() {
145             return mName;
146         }
147     }
148 
149     /** The different mode an invocation can run into. */
150     public enum RunMode {
151         REGULAR,
152         PARENT_SANDBOX,
153         SANDBOX,
154         REMOTE_INVOCATION,
155     }
156 
157     private String mStatus = "(not invoked)";
158     private String mStopCause = null;
159     private Long mStopRequestTime = null;
160     private boolean mTestStarted = false;
161     private boolean mInvocationFailed = false;
162     private List<IScheduledInvocationListener> mSchedulerListeners = new ArrayList<>();
163 
164     /**
165      * Display a log message informing the user of a invocation being started.
166      *
167      * @param context the {@link IInvocationContext}
168      * @param config the {@link IConfiguration}
169      */
logStartInvocation(IInvocationContext context, IConfiguration config)170     private void logStartInvocation(IInvocationContext context, IConfiguration config) {
171         String shardSuffix = "";
172         if (config.getCommandOptions().getShardIndex() != null) {
173             shardSuffix =
174                     String.format(
175                             " (shard %d of %d)",
176                             config.getCommandOptions().getShardIndex() + 1,
177                             config.getCommandOptions().getShardCount());
178         }
179         StringBuilder buildInfos = new StringBuilder();
180         StringBuilder msg = new StringBuilder("Starting invocation for '");
181         msg.append(context.getTestTag());
182         msg.append("' with ");
183         for (Entry<ITestDevice, IBuildInfo> entry : context.getDeviceBuildMap().entrySet()) {
184             msg.append("'[ ");
185             msg.append(entry.getValue().toString());
186             buildInfos.append(entry.getValue().toString());
187             msg.append(" on device '");
188             msg.append(entry.getKey().getSerialNumber());
189             msg.append("'] ");
190         }
191         msg.append(shardSuffix);
192         CLog.logAndDisplay(LogLevel.INFO, msg.toString());
193         mStatus = String.format("running %s on build(s) '%s'", context.getTestTag(),
194                 buildInfos.toString()) + shardSuffix;
195     }
196 
197     /**
198      * Performs the invocation
199      *
200      * @param config the {@link IConfiguration}
201      * @param testInfo the {@link TestInformation} to use for the invocation.
202      */
performInvocation( IConfiguration config, TestInformation testInfo, IInvocationExecution invocationPath, ITestInvocationListener listener, boolean devicePreSetupDone)203     private void performInvocation(
204             IConfiguration config,
205             TestInformation testInfo,
206             IInvocationExecution invocationPath,
207             ITestInvocationListener listener,
208             boolean devicePreSetupDone)
209             throws Throwable {
210         ReportHostLog reportThread = new ReportHostLog(listener, config);
211         Runtime.getRuntime().addShutdownHook(reportThread);
212         boolean resumed = false;
213         String bugreportName = null;
214         long startTime = System.currentTimeMillis();
215         long elapsedTime = -1;
216         Throwable exception = null;
217         Throwable tearDownException = null;
218         ITestDevice badDevice = null;
219         IInvocationContext context = testInfo.getContext();
220 
221         // Ensure that no unexpected attributes are added afterward
222         ((InvocationContext) context).lockAttributes();
223         try {
224             logDeviceBatteryLevel(context, "initial");
225             // Run the preInvocationSetup on devices.
226             if (!devicePreSetupDone) {
227                 if (!config.getCommandOptions().shouldUseSandboxing()) {
228                     invocationPath.runDevicePreInvocationSetup(context, config, listener);
229                 }
230             }
231             // Then run the regular setup and run
232             prepareAndRun(config, testInfo, invocationPath, listener);
233         } catch (BuildError e) {
234             exception = e;
235             CLog.w("BuildError on device '%s'. Reason: %s", e.getDeviceSerial(), e.toString());
236             bugreportName = BUILD_ERROR_BUGREPORT_NAME;
237             if (e.getDeviceSerial() != null) {
238                 badDevice = context.getDeviceBySerial(e.getDeviceSerial());
239             }
240             if (e instanceof DeviceFailedToBootError) {
241                 if (badDevice == null) {
242                     context.setRecoveryModeForAllDevices(RecoveryMode.NONE);
243                 } else {
244                     badDevice.setRecoveryMode(RecoveryMode.NONE);
245                 }
246             }
247             reportFailure(createFailureFromException(e, FailureStatus.INFRA_FAILURE), listener);
248         } catch (TargetSetupError e) {
249             exception = e;
250             CLog.e("Caught exception while running invocation");
251             CLog.e(e);
252             bugreportName = TARGET_SETUP_ERROR_BUGREPORT_NAME;
253             if (e.getDeviceSerial() != null) {
254                 badDevice = context.getDeviceBySerial(e.getDeviceSerial());
255             }
256             reportFailure(createFailureFromException(e, FailureStatus.INFRA_FAILURE), listener);
257         } catch (DeviceNotAvailableException e) {
258             exception = e;
259             // log a warning here so its captured before reportLogs is called
260             CLog.w("Invocation did not complete due to device %s becoming not available. " +
261                     "Reason: %s", e.getSerial(), e.getMessage());
262             badDevice = context.getDeviceBySerial(e.getSerial());
263             if ((e instanceof DeviceUnresponsiveException) && badDevice != null
264                     && TestDeviceState.ONLINE.equals(badDevice.getDeviceState())) {
265                 // under certain cases it might still be possible to grab a bugreport
266                 bugreportName = DEVICE_UNRESPONSIVE_BUGREPORT_NAME;
267             }
268             reportFailure(createFailureFromException(e, FailureStatus.INFRA_FAILURE), listener);
269             // Upon reaching here after an exception, it is safe to assume that recovery
270             // has already been attempted so we disable it to avoid re-entry during clean up.
271             if (badDevice != null) {
272                 badDevice.setRecoveryMode(RecoveryMode.NONE);
273             }
274             throw e;
275         } catch (RunInterruptedException e) {
276             exception = e;
277             CLog.w("Invocation interrupted");
278             reportFailure(createFailureFromException(e, FailureStatus.UNSET), listener);
279         } catch (AssertionError e) {
280             exception = e;
281             CLog.e("Caught AssertionError while running invocation: %s", e.toString());
282             CLog.e(e);
283             reportFailure(createFailureFromException(e, FailureStatus.UNSET), listener);
284         } catch (Throwable t) {
285             exception = t;
286             // log a warning here so its captured before reportLogs is called
287             CLog.e("Unexpected exception when running invocation: %s", t.toString());
288             CLog.e(t);
289             reportFailure(createFailureFromException(t, FailureStatus.UNSET), listener);
290             throw t;
291         } finally {
292             // Only capture logcat for TEST if we started the test phase.
293             if (mTestStarted) {
294                 for (ITestDevice device : context.getDevices()) {
295                     invocationPath.reportLogs(device, listener, Stage.TEST);
296                 }
297             }
298             CurrentInvocation.setActionInProgress(ActionInProgress.TEAR_DOWN);
299             getRunUtil().allowInterrupt(false);
300             if (config.getCommandOptions().takeBugreportOnInvocationEnded() ||
301                     config.getCommandOptions().takeBugreportzOnInvocationEnded()) {
302                 if (bugreportName != null) {
303                     CLog.i("Bugreport to be taken for failure instead of invocation ended.");
304                 } else {
305                     bugreportName = INVOCATION_ENDED_BUGREPORT_NAME;
306                 }
307             }
308             if (bugreportName != null) {
309                 if (context.getDevices().size() == 1 || badDevice != null) {
310                     ITestDevice collectBugreport = badDevice;
311                     if (collectBugreport == null) {
312                         collectBugreport = context.getDevices().get(0);
313                     }
314                     // If we have identified a faulty device only take the bugreport on it.
315                     takeBugreport(collectBugreport, listener, bugreportName);
316                 } else if (context.getDevices().size() > 1) {
317                     ParallelDeviceExecutor<Boolean> executor =
318                             new ParallelDeviceExecutor<>(context.getDevices());
319                     List<Callable<Boolean>> callableTasks = new ArrayList<>();
320                     final String reportName = bugreportName;
321                     for (ITestDevice device : context.getDevices()) {
322                         Callable<Boolean> callableTask =
323                                 () -> {
324                                     takeBugreport(device, listener, reportName);
325                                     return true;
326                                 };
327                         callableTasks.add(callableTask);
328                     }
329                     // Capture the bugreports best effort, ignore the results.
330                     executor.invokeAll(callableTasks, 5, TimeUnit.MINUTES);
331                 }
332             }
333             // Save the device executeShellCommand logs
334             logExecuteShellCommand(context.getDevices(), listener);
335 
336             mStatus = "tearing down";
337             try {
338                 invocationPath.doTeardown(testInfo, config, listener, exception);
339             } catch (Throwable e) {
340                 tearDownException = e;
341                 CLog.e("Exception when tearing down invocation: %s", tearDownException.toString());
342                 CLog.e(tearDownException);
343                 if (exception == null) {
344                     // only report when the exception is new during tear down
345                     reportFailure(
346                             createFailureFromException(
347                                     tearDownException, FailureStatus.INFRA_FAILURE),
348                             listener);
349                 }
350             }
351             mStatus = "done running tests";
352             CurrentInvocation.setActionInProgress(ActionInProgress.FREE_RESOURCES);
353             // Track the timestamp when we are done with devices
354             addInvocationMetric(
355                     InvocationMetricKey.DEVICE_DONE_TIMESTAMP, System.currentTimeMillis());
356             Map<ITestDevice, FreeDeviceState> devicesStates =
357                     handleAndLogReleaseState(context, exception);
358             if (config.getCommandOptions().earlyDeviceRelease()) {
359                 context.markReleasedEarly();
360                 for (IScheduledInvocationListener scheduleListener : mSchedulerListeners) {
361                     scheduleListener.releaseDevices(context, devicesStates);
362                 }
363             }
364             try {
365                 // Clean up host.
366                 invocationPath.doCleanUp(context, config, exception);
367                 for (ITestDevice device : context.getDevices()) {
368                     invocationPath.reportLogs(device, listener, Stage.TEARDOWN);
369                 }
370                 if (mStopCause != null) {
371                     String message =
372                             String.format(
373                                     "Invocation was interrupted due to: %s, results will be "
374                                             + "affected.",
375                                     mStopCause);
376                     FailureDescription failure =
377                             FailureDescription.create(message, FailureStatus.CANCELLED);
378                     reportFailure(failure, listener);
379                     PrettyPrintDelimiter.printStageDelimiter(message);
380                     if (mStopRequestTime != null) {
381                         // This is not 100% perfect since result reporting can still run a bit
382                         // longer, but this is our last opportunity to report it.
383                         long latency = System.currentTimeMillis() - mStopRequestTime;
384                         InvocationMetricLogger.addInvocationMetrics(
385                                 InvocationMetricKey.SHUTDOWN_HARD_LATENCY, latency);
386                     }
387                 }
388                 reportHostLog(listener, config);
389                 // If host_log is reported, remove the hook
390                 Runtime.getRuntime().removeShutdownHook(reportThread);
391 
392                 elapsedTime = System.currentTimeMillis() - startTime;
393                 if (!resumed) {
394                     // Init a log for the end of the host_log.
395                     ILeveledLogOutput endHostLog = config.getLogOutput();
396                     endHostLog.init();
397                     getLogRegistry().registerLogger(endHostLog);
398                     PrettyPrintDelimiter.printStageDelimiter("===== Result Reporters =====");
399                     try {
400                         // Copy the invocation metrics to the context
401                         ((InvocationContext) context).logInvocationMetrics();
402                         listener.invocationEnded(elapsedTime);
403                     } finally {
404                         InvocationMetricLogger.clearInvocationMetrics();
405                         endHostLog.closeLog();
406                         getLogRegistry().unregisterLogger();
407                     }
408                 }
409             } finally {
410                 TfObjectTracker.clearTracking();
411                 CurrentInvocation.clearInvocationInfos();
412             }
413         }
414         if (tearDownException != null) {
415             // this means a DNAE or RTE has happened during teardown, need to throw
416             // if there was a preceding RTE or DNAE stored in 'exception', it would have already
417             // been thrown before exiting the previous try...catch...finally block
418             throw tearDownException;
419         }
420     }
421 
422     /** Do setup and run the tests */
prepareAndRun( IConfiguration config, TestInformation testInfo, IInvocationExecution invocationPath, ITestInvocationListener listener)423     private void prepareAndRun(
424             IConfiguration config,
425             TestInformation testInfo,
426             IInvocationExecution invocationPath,
427             ITestInvocationListener listener)
428             throws Throwable {
429         getRunUtil().allowInterrupt(true);
430         logDeviceBatteryLevel(testInfo.getContext(), "initial -> setup");
431         CurrentInvocation.setActionInProgress(ActionInProgress.SETUP);
432         invocationPath.doSetup(testInfo, config, listener);
433         logDeviceBatteryLevel(testInfo.getContext(), "setup -> test");
434         mTestStarted = true;
435         CurrentInvocation.setActionInProgress(ActionInProgress.TEST);
436         invocationPath.runTests(testInfo, config, listener);
437         logDeviceBatteryLevel(testInfo.getContext(), "after test");
438         CurrentInvocation.setActionInProgress(ActionInProgress.UNSET);
439     }
440 
441     /**
442      * Starts the invocation.
443      * <p/>
444      * Starts logging, and informs listeners that invocation has been started.
445      *
446      * @param config
447      * @param context
448      */
startInvocation(IConfiguration config, IInvocationContext context, ITestInvocationListener listener)449     private void startInvocation(IConfiguration config, IInvocationContext context,
450             ITestInvocationListener listener) {
451         logStartInvocation(context, config);
452         listener.invocationStarted(context);
453     }
454 
455     /** Report the exception failure as an invocation failure. */
reportFailure(FailureDescription failure, ITestInvocationListener listener)456     private void reportFailure(FailureDescription failure, ITestInvocationListener listener) {
457         if (mInvocationFailed) {
458             CLog.e("An invocation failure was already reported, ignoring %s", failure);
459             return;
460         }
461         // Always report the failure
462         listener.invocationFailed(failure);
463         mInvocationFailed = true;
464     }
465 
466     /**
467      * Create a {@link FailureDescription} from an invocation exception.
468      *
469      * @param exception The exception to convert
470      * @param defaultStatus The status to use by default if the exception is not a {@link
471      *     IHarnessException}.
472      */
createFailureFromException( Throwable exception, FailureStatus defaultStatus)473     private FailureDescription createFailureFromException(
474             Throwable exception, FailureStatus defaultStatus) {
475         ErrorIdentifier id = null;
476         if (exception instanceof IHarnessException) {
477             id = ((HarnessException) exception).getErrorId();
478         }
479         FailureDescription failure =
480                 CurrentInvocation.createFailure(exception.getMessage(), id)
481                         .setCause(exception)
482                         .setFailureStatus(defaultStatus);
483         if (id != null) {
484             failure.setErrorIdentifier(id);
485             failure.setFailureStatus(id.status());
486         }
487         return failure;
488     }
489 
reportHostLog(ITestInvocationListener listener, IConfiguration config)490     private void reportHostLog(ITestInvocationListener listener, IConfiguration config) {
491         reportHostLog(listener, config, TRADEFED_LOG_NAME);
492     }
493 
reportHostLog( ITestInvocationListener listener, IConfiguration config, String name)494     private void reportHostLog(
495             ITestInvocationListener listener, IConfiguration config, String name) {
496         ILeveledLogOutput logger = config.getLogOutput();
497         try (InputStreamSource globalLogSource = logger.getLog()) {
498             if (globalLogSource != null) {
499                 if (config.getCommandOptions().getHostLogSuffix() != null) {
500                     name += config.getCommandOptions().getHostLogSuffix();
501                 }
502                 listener.testLog(name, LogDataType.HOST_LOG, globalLogSource);
503             } else {
504                 // Only print the non-logging if we are not a stdout logger
505                 if (!(logger instanceof StdoutLogger)) {
506                     CLog.i("Skip logging %s to a file with logger '%s'", name, logger);
507                 }
508             }
509         }
510         // once tradefed log is reported, all further log calls for this invocation can get lost
511         // unregister logger so future log calls get directed to the tradefed global log
512         getLogRegistry().unregisterLogger();
513         logger.closeLog();
514     }
515 
takeBugreport( ITestDevice device, ITestInvocationListener listener, String bugreportName)516     private void takeBugreport(
517             ITestDevice device, ITestInvocationListener listener, String bugreportName) {
518         if (device == null) {
519             return;
520         }
521         if (device.getIDevice() instanceof StubDevice) {
522             return;
523         }
524         // logBugreport will report a regular bugreport if bugreportz is not supported.
525         boolean res =
526                 device.logBugreport(
527                         String.format("%s_%s", bugreportName, device.getSerialNumber()), listener);
528         if (!res) {
529             CLog.w("Error when collecting bugreport for device '%s'", device.getSerialNumber());
530         }
531     }
532 
533     /**
534      * Gets the {@link ILogRegistry} to use.
535      * <p/>
536      * Exposed for unit testing.
537      */
getLogRegistry()538     ILogRegistry getLogRegistry() {
539         return LogRegistry.getLogRegistry();
540     }
541 
542     /**
543      * Utility method to fetch the default {@link IRunUtil} singleton
544      * <p />
545      * Exposed for unit testing.
546      */
getRunUtil()547     IRunUtil getRunUtil() {
548         return RunUtil.getDefault();
549     }
550 
551     @Override
toString()552     public String toString() {
553         return mStatus;
554     }
555 
556     /**
557      * Log the battery level of each device in the invocation.
558      *
559      * @param context the {@link IInvocationContext} of the invocation.
560      * @param event a {@link String} describing the context of the logging (initial, setup, etc.).
561      */
562     @VisibleForTesting
logDeviceBatteryLevel(IInvocationContext context, String event)563     void logDeviceBatteryLevel(IInvocationContext context, String event) {
564         for (ITestDevice testDevice : context.getDevices()) {
565             if (testDevice == null) {
566                 continue;
567             }
568             if (testDevice.getIDevice() instanceof StubDevice) {
569                 continue;
570             }
571             if (testDevice instanceof RemoteAndroidVirtualDevice
572                     || testDevice instanceof NestedRemoteDevice) {
573                 // Vritual devices have a fake battery there is no point in logging it.
574                 continue;
575             }
576             Integer batteryLevel = testDevice.getBattery();
577             if (batteryLevel == null) {
578                 CLog.v("Failed to get battery level for %s", testDevice.getSerialNumber());
579                 continue;
580             }
581             CLog.v("%s - %s - %d%%", BATT_TAG, event, batteryLevel);
582             context.getBuildInfo(testDevice)
583                     .addBuildAttribute(
584                             String.format(
585                                     BATTERY_ATTRIBUTE_FORMAT_KEY,
586                                     testDevice.getSerialNumber(),
587                                     event),
588                             batteryLevel.toString());
589         }
590     }
591 
592     /**
593      * Invoke {@link IInvocationExecution#fetchBuild(TestInformation, IConfiguration, IRescheduler,
594      * ITestInvocationListener)} and handles the output as well as failures.
595      *
596      * @param testInfo the {@link TestInformation} of the invocation.
597      * @param config the {@link IConfiguration} of this test run.
598      * @param rescheduler the {@link IRescheduler}, for rescheduling portions of the invocation for
599      *     execution on another resource(s)
600      * @param listener the {@link ITestInvocation} to report build download failures.
601      * @param invocationPath the {@link IInvocationExecution} driving the invocation.
602      * @return True if we successfully downloaded the build, false otherwise.
603      * @throws DeviceNotAvailableException
604      */
invokeFetchBuild( TestInformation testInfo, IConfiguration config, IRescheduler rescheduler, ITestInvocationListener listener, IInvocationExecution invocationPath)605     private boolean invokeFetchBuild(
606             TestInformation testInfo,
607             IConfiguration config,
608             IRescheduler rescheduler,
609             ITestInvocationListener listener,
610             IInvocationExecution invocationPath)
611             throws DeviceNotAvailableException {
612         CurrentInvocation.setActionInProgress(ActionInProgress.FETCHING_ARTIFACTS);
613         Exception buildException = null;
614         boolean res = false;
615         try {
616             res = invocationPath.fetchBuild(testInfo, config, rescheduler, listener);
617             if (res) {
618                 // Successful fetch of build.
619                 CurrentInvocation.setActionInProgress(ActionInProgress.UNSET);
620                 return true;
621             }
622             // In case of build not found issues.
623             mStatus = "(no build to test)";
624             // Set the exit code to error
625             buildException =
626                     new BuildRetrievalError(
627                             "No build found to test.", InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
628         } catch (BuildRetrievalError | RuntimeException e) {
629             buildException = e;
630         }
631         setExitCode(ExitCode.NO_BUILD, buildException);
632         // Report an empty invocation, so this error is sent to listeners
633         startInvocation(config, testInfo.getContext(), listener);
634         reportFailure(
635                 createFailureFromException(buildException, FailureStatus.INFRA_FAILURE), listener);
636         for (ITestDevice device : testInfo.getContext().getDevices()) {
637             invocationPath.reportLogs(device, listener, Stage.ERROR);
638         }
639         reportHostLog(listener, config);
640         listener.invocationEnded(0L);
641         return false;
642     }
643 
644     /**
645      * Invoke {@link IConfiguration#resolveDynamicOptions(DynamicRemoteFileResolver)} to resolve the
646      * dynamic files.
647      *
648      * @param context the {@link IInvocationContext} of the invocation.
649      * @param config the {@link IConfiguration} of this test run.
650      * @param rescheduler the {@link IRescheduler}, for rescheduling portions of the invocation for
651      *     execution on another resource(s)
652      * @param listener the {@link ITestInvocation} to report build download failures.
653      * @param invocationPath the {@link IInvocationExecution} driving the invocation.
654      * @param mode The current {@link RunMode} of the invocation.
655      * @return True if we successfully downloaded the build, false otherwise.
656      */
invokeRemoteDynamic( IInvocationContext context, IConfiguration config, IRescheduler rescheduler, ITestInvocationListener listener, IInvocationExecution invocationPath, RunMode mode)657     private boolean invokeRemoteDynamic(
658             IInvocationContext context,
659             IConfiguration config,
660             IRescheduler rescheduler,
661             ITestInvocationListener listener,
662             IInvocationExecution invocationPath,
663             RunMode mode) {
664         try {
665             // Don't resolve for remote invocation, wait until we are inside the remote.
666             if (RunMode.REMOTE_INVOCATION.equals(mode)) {
667                 return true;
668             }
669             CurrentInvocation.setActionInProgress(ActionInProgress.FETCHING_ARTIFACTS);
670             DynamicRemoteFileResolver resolver = new DynamicRemoteFileResolver();
671             resolver.setDevice(context.getDevices().get(0));
672             resolver.addExtraArgs(config.getCommandOptions().getDynamicDownloadArgs());
673             config.resolveDynamicOptions(resolver);
674             CurrentInvocation.setActionInProgress(ActionInProgress.UNSET);
675             return true;
676         } catch (RuntimeException | BuildRetrievalError | ConfigurationException e) {
677             // In case of build not found issues.
678             mStatus = "(failed dynamic download)";
679             // Set the exit code to error
680             setExitCode(ExitCode.NO_BUILD, e);
681 
682             // We don't have a reporting buildInfo at this point
683             IBuildInfo info = new BuildInfo();
684             context.addDeviceBuildInfo(context.getDeviceConfigNames().get(0), info);
685 
686             // Report an empty invocation, so this error is sent to listeners
687             startInvocation(config, context, listener);
688             reportFailure(createFailureFromException(e, FailureStatus.INFRA_FAILURE), listener);
689             for (ITestDevice device : context.getDevices()) {
690                 invocationPath.reportLogs(device, listener, Stage.ERROR);
691             }
692             reportHostLog(listener, config);
693             listener.invocationEnded(0L);
694             return false;
695         }
696     }
697 
698     /** {@inheritDoc} */
699     @Override
invoke( IInvocationContext context, IConfiguration config, IRescheduler rescheduler, ITestInvocationListener... extraListeners)700     public void invoke(
701             IInvocationContext context,
702             IConfiguration config,
703             IRescheduler rescheduler,
704             ITestInvocationListener... extraListeners)
705             throws DeviceNotAvailableException, Throwable {
706         for (ITestInvocationListener listener : extraListeners) {
707             if (listener instanceof IScheduledInvocationListener) {
708                 mSchedulerListeners.add((IScheduledInvocationListener) listener);
709             }
710         }
711         // Create the TestInformation for the invocation
712         // TODO: Use invocation-id in the workfolder name
713         Object sharedInfoObject =
714                 config.getConfigurationObject(ShardHelper.SHARED_TEST_INFORMATION);
715         TestInformation sharedTestInfo = null;
716         TestInformation info = null;
717         if (sharedInfoObject != null) {
718             sharedTestInfo = (TestInformation) sharedInfoObject;
719             // During sharding we share everything except the invocation context
720             info = TestInformation.createModuleTestInfo(sharedTestInfo, context);
721         }
722         if (info == null) {
723             File mWorkFolder = FileUtil.createTempDir("tradefed-invocation-workfolder");
724             info =
725                     TestInformation.newBuilder()
726                             .setInvocationContext(context)
727                             .setDependenciesFolder(mWorkFolder)
728                             .build();
729         }
730         CurrentInvocation.addInvocationInfo(InvocationInfo.WORK_FOLDER, info.dependenciesFolder());
731 
732         CleanUpInvocationFiles cleanUpThread = new CleanUpInvocationFiles(info, config);
733         Runtime.getRuntime().addShutdownHook(cleanUpThread);
734         registerExecutionFiles(info.executionFiles());
735 
736         List<ITestInvocationListener> allListeners =
737                 new ArrayList<>(config.getTestInvocationListeners().size() + extraListeners.length);
738         allListeners.addAll(config.getTestInvocationListeners());
739         allListeners.addAll(Arrays.asList(extraListeners));
740         ITestInvocationListener listener = null;
741 
742         // Auto retry feature
743         IRetryDecision decision = config.getRetryDecision();
744         ResultAggregator aggregator = null;
745         decision.setInvocationContext(context);
746         // We don't need the aggregator in the subprocess because the parent will take care of it.
747         if (!config.getCommandOptions()
748                         .getInvocationData()
749                         .containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME)
750                 && decision.isAutoRetryEnabled()
751                 && decision.getMaxRetryCount() > 1
752                 && !RetryStrategy.NO_RETRY.equals(decision.getRetryStrategy())) {
753             CLog.d("Auto-retry enabled, using the ResultAggregator to handle multiple retries.");
754             aggregator = new ResultAggregator(allListeners, decision.getRetryStrategy());
755             allListeners = Arrays.asList(aggregator);
756         }
757 
758         if (!config.getPostProcessors().isEmpty()) {
759             ITestInvocationListener forwarder = new ResultAndLogForwarder(allListeners);
760             // Post-processors are the first layer around the final reporters.
761             for (IPostProcessor postProcessor : config.getPostProcessors()) {
762                 if (postProcessor.isDisabled()) {
763                     CLog.d("%s has been disabled. skipping.", postProcessor);
764                 } else {
765                     forwarder = postProcessor.init(forwarder);
766                 }
767             }
768             listener = new LogSaverResultForwarder(config.getLogSaver(), Arrays.asList(forwarder));
769         } else {
770             listener = new LogSaverResultForwarder(config.getLogSaver(), allListeners);
771         }
772 
773         RunMode mode = RunMode.REGULAR;
774         if (config.getConfigurationDescription().shouldUseSandbox()) {
775             mode = RunMode.SANDBOX;
776         }
777         if (config.getCommandOptions().shouldUseSandboxing()) {
778             mode = RunMode.PARENT_SANDBOX;
779         }
780         if (context.getDevices().get(0) instanceof ManagedRemoteDevice) {
781             mode = RunMode.REMOTE_INVOCATION;
782         }
783         IInvocationExecution invocationPath = createInvocationExec(mode);
784         updateInvocationContext(context, config);
785 
786         // Create the Guice scope
787         InvocationScope scope = getInvocationScope();
788         scope.enter();
789         // Seed our TF objects to the Guice scope
790         scope.seed(IRescheduler.class, rescheduler);
791         scope.seedConfiguration(config);
792         boolean sharding = false;
793         try {
794             ILeveledLogOutput leveledLogOutput = config.getLogOutput();
795             leveledLogOutput.init();
796             if (leveledLogOutput instanceof BaseLeveledLogOutput) {
797                 ((BaseLeveledLogOutput) leveledLogOutput).initFilters(config);
798             }
799             getLogRegistry().registerLogger(leveledLogOutput);
800             mStatus = "resolving dynamic options";
801             boolean resolverSuccess =
802                     invokeRemoteDynamic(
803                             context, config, rescheduler, listener, invocationPath, mode);
804             if (!resolverSuccess) {
805                 return;
806             }
807 
808             mStatus = "fetching build";
809             for (String deviceName : context.getDeviceConfigNames()) {
810                 context.getDevice(deviceName).clearLastConnectedWifiNetwork();
811                 context.getDevice(deviceName)
812                         .setOptions(config.getDeviceConfigByName(deviceName).getDeviceOptions());
813                 if (config.getDeviceConfigByName(deviceName)
814                         .getDeviceOptions()
815                         .isLogcatCaptureEnabled()) {
816                     if (!(context.getDevice(deviceName).getIDevice() instanceof StubDevice)) {
817                         context.getDevice(deviceName).startLogcat();
818                     }
819                 }
820             }
821 
822             String cmdLineArgs = config.getCommandLine();
823             if (cmdLineArgs != null) {
824                 CLog.i("Invocation was started with cmd: %s", cmdLineArgs);
825             }
826 
827             long start = System.currentTimeMillis();
828             boolean providerSuccess =
829                     invokeFetchBuild(info, config, rescheduler, listener, invocationPath);
830             long fetchBuildDuration = System.currentTimeMillis() - start;
831             InvocationMetricLogger.addInvocationMetrics(
832                     InvocationMetricKey.FETCH_BUILD, fetchBuildDuration);
833             CLog.d("Fetch build duration: %s", TimeUtil.formatElapsedTime(fetchBuildDuration));
834             if (!providerSuccess) {
835                 return;
836             }
837 
838             boolean deviceInit = false;
839             // If the top level invocation has --use-sandbox do not shard there. It will shard in
840             // the child invocation.
841             if (RunMode.REGULAR.equals(mode) || RunMode.SANDBOX.equals(mode)) {
842                 mStatus = "sharding";
843 
844                 // TODO: Handle local sharding and special devices
845                 Integer shardCount = config.getCommandOptions().getShardCount();
846                 Integer shardIndex = config.getCommandOptions().getShardIndex();
847                 // Special Handling in case of sharding within the same invocation (in-place): Some
848                 // devices (Remote devices for example) require extra preparation step to be
849                 // available, but sharding requires the device to be available in some cases. So
850                 // we call the device setup early to meet all the requirements.
851                 if (shardCount != null && shardIndex != null) {
852                     deviceInit = true;
853                     startInvocation(config, context, listener);
854                     try {
855                         invocationPath.runDevicePreInvocationSetup(context, config, listener);
856                     } catch (DeviceNotAvailableException | TargetSetupError e) {
857                         CLog.e(e);
858                         FailureDescription failure = FailureDescription.create(e.getMessage());
859                         failure.setCause(e).setFailureStatus(FailureStatus.INFRA_FAILURE);
860                         if (e instanceof DeviceNotAvailableException) {
861                             setExitCode(ExitCode.DEVICE_UNAVAILABLE, e);
862                         } else {
863                             setExitCode(ExitCode.THROWABLE_EXCEPTION, e);
864                         }
865                         try {
866                             invocationPath.runDevicePostInvocationTearDown(context, config, e);
867                         } finally {
868                             reportFailure(
869                                     createFailureFromException(e, FailureStatus.INFRA_FAILURE),
870                                     listener);
871                             // Reports the logs
872                             for (ITestDevice device : context.getDevices()) {
873                                 invocationPath.reportLogs(device, listener, Stage.ERROR);
874                             }
875                             reportHostLog(listener, config);
876                             listener.invocationEnded(0L);
877                         }
878                         return;
879                     }
880                 }
881 
882                 try {
883                     sharding = invocationPath.shardConfig(config, info, rescheduler, listener);
884                 } catch (RuntimeException unexpected) {
885                     if (deviceInit) {
886                         // If we did an early setup, do the tear down.
887                         invocationPath.runDevicePostInvocationTearDown(context, config, null);
888                     }
889                     throw unexpected;
890                 }
891                 if (sharding) {
892                     CLog.i(
893                             "Invocation for %s has been sharded, rescheduling",
894                             context.getSerials());
895                     // Log the chunk of parent host_log before sharding
896                     reportHostLog(listener, config, TRADEFED_LOG_NAME + BEFORE_SHARDING_SUFFIX);
897                     config.getLogSaver().invocationEnded(0L);
898                     if (aggregator != null) {
899                         // The host_log is not available yet to reporters that don't support
900                         // granular results, so forward it.
901                         aggregator.forwardAggregatedInvocationLogs();
902                     }
903                     return;
904                 }
905             }
906             // Once we have all the information we can start the invocation.
907             if (!deviceInit) {
908                 startInvocation(config, context, listener);
909             }
910             if (config.getTests() == null || config.getTests().isEmpty()) {
911                 CLog.e("No tests to run");
912                 if (deviceInit) {
913                     // If we did an early setup, do the tear down.
914                     invocationPath.runDevicePostInvocationTearDown(context, config, null);
915                 }
916                 listener.invocationEnded(0L);
917                 return;
918             }
919 
920             performInvocation(config, info, invocationPath, listener, deviceInit);
921             setExitCode(ExitCode.NO_ERROR, null);
922         } catch (IOException e) {
923             CLog.e(e);
924         } finally {
925             scope.exit();
926             // Ensure build infos are always cleaned up at the end of invocation.
927             invocationPath.cleanUpBuilds(context, config);
928             // ensure we always deregister the logger
929             for (String deviceName : context.getDeviceConfigNames()) {
930                 if (!(context.getDevice(deviceName).getIDevice() instanceof StubDevice)) {
931                     context.getDevice(deviceName).stopLogcat();
932                 }
933             }
934             if (!sharding) {
935                 // If we are the parent shard, we do not delete the test information
936                 deleteInvocationFiles(info, config);
937             }
938             // save remaining logs contents to global log
939             getLogRegistry().dumpToGlobalLog(config.getLogOutput());
940             // Ensure log is unregistered and closed
941             getLogRegistry().unregisterLogger();
942             config.getLogOutput().closeLog();
943             config.cleanConfigurationData();
944 
945             Runtime.getRuntime().removeShutdownHook(cleanUpThread);
946         }
947     }
948 
949     /** Returns the current {@link InvocationScope}. */
950     @VisibleForTesting
getInvocationScope()951     InvocationScope getInvocationScope() {
952         return InvocationScope.getDefault();
953     }
954 
955     @VisibleForTesting
registerExecutionFiles(ExecutionFiles executionFiles)956     public void registerExecutionFiles(ExecutionFiles executionFiles) {
957         CurrentInvocation.registerExecutionFiles(executionFiles);
958     }
959 
960     /**
961      * Helper to set the exit code. Exposed for testing.
962      */
setExitCode(ExitCode code, Throwable stack)963     protected void setExitCode(ExitCode code, Throwable stack) {
964         GlobalConfiguration.getInstance().getCommandScheduler()
965                 .setLastInvocationExitCode(code, stack);
966     }
967 
addInvocationMetric(InvocationMetricKey key, long value)968     protected void addInvocationMetric(InvocationMetricKey key, long value) {
969         InvocationMetricLogger.addInvocationMetrics(key, value);
970     }
971 
addInvocationMetric(InvocationMetricKey key, String value)972     protected void addInvocationMetric(InvocationMetricKey key, String value) {
973         InvocationMetricLogger.addInvocationMetrics(key, value);
974     }
975 
getDeviceLogName(Stage stage)976     public static String getDeviceLogName(Stage stage) {
977         return DEVICE_LOG_NAME_PREFIX + stage.getName();
978     }
979 
getEmulatorLogName(Stage stage)980     public static String getEmulatorLogName(Stage stage) {
981         return EMULATOR_LOG_NAME_PREFIX + stage.getName();
982     }
983 
984     @Override
notifyInvocationStopped(String message)985     public void notifyInvocationStopped(String message) {
986         mStopCause = message;
987         if (mStopRequestTime == null) {
988             mStopRequestTime = System.currentTimeMillis();
989         }
990     }
991 
992     /**
993      * Create the invocation path that should be followed.
994      *
995      * @param mode The mode we are currently running as.
996      * @return The {@link IInvocationExecution} describing the invocation.
997      */
createInvocationExec(RunMode mode)998     public IInvocationExecution createInvocationExec(RunMode mode) {
999         switch (mode) {
1000             case PARENT_SANDBOX:
1001                 return new ParentSandboxInvocationExecution();
1002             case SANDBOX:
1003                 return new SandboxedInvocationExecution();
1004             case REMOTE_INVOCATION:
1005                 return new RemoteInvocationExecution();
1006             default:
1007                 return new InvocationExecution();
1008         }
1009     }
1010 
1011     /** Prints a delimiter for a given Stage of the invocation. */
printStageDelimiter(Stage phase, boolean end)1012     public static void printStageDelimiter(Stage phase, boolean end) {
1013         String startEnd = end ? "ENDING" : "STARTING";
1014         String message = String.format("===== %s PHASE %s =====", phase, startEnd);
1015         PrettyPrintDelimiter.printStageDelimiter(message);
1016     }
1017 
logExecuteShellCommand(List<ITestDevice> devices, ITestLogger logger)1018     private void logExecuteShellCommand(List<ITestDevice> devices, ITestLogger logger) {
1019         for (ITestDevice device : devices) {
1020             if (!(device instanceof NativeDevice)) {
1021                 return;
1022             }
1023             File log = ((NativeDevice) device).getExecuteShellCommandLog();
1024             if (log == null || !log.exists()) {
1025                 return;
1026             }
1027             if (log.length() == 0) {
1028                 CLog.d("executeShellCommandLog file was empty, skip logging.");
1029                 return;
1030             }
1031             try (InputStreamSource source = new FileInputStreamSource(log)) {
1032                 logger.testLog(
1033                         String.format("executeShellCommandLog_%s", device.getSerialNumber()),
1034                         LogDataType.TEXT,
1035                         source);
1036             }
1037         }
1038     }
1039 
1040     /**
1041      * Update the {@link IInvocationContext} with additional info from the {@link IConfiguration}.
1042      *
1043      * @param context the {@link IInvocationContext}
1044      * @param config the {@link IConfiguration}
1045      */
updateInvocationContext(IInvocationContext context, IConfiguration config)1046     private void updateInvocationContext(IInvocationContext context, IConfiguration config) {
1047         if (config.getCommandLine() != null) {
1048             context.addInvocationAttribute(
1049                     TestInvocation.COMMAND_ARGS_KEY, config.getCommandLine());
1050         }
1051         if (config.getCommandOptions().getShardCount() != null) {
1052             context.addInvocationAttribute(
1053                     "shard_count", config.getCommandOptions().getShardCount().toString());
1054         }
1055         if (config.getCommandOptions().getShardIndex() != null) {
1056             context.addInvocationAttribute(
1057                     "shard_index", config.getCommandOptions().getShardIndex().toString());
1058         }
1059         context.setTestTag(getTestTag(config));
1060     }
1061 
1062     /** Helper to create the test tag from the configuration. */
getTestTag(IConfiguration config)1063     private String getTestTag(IConfiguration config) {
1064         String testTag = config.getCommandOptions().getTestTag();
1065         if (config.getCommandOptions().getTestTagSuffix() != null) {
1066             testTag =
1067                     String.format("%s-%s", testTag, config.getCommandOptions().getTestTagSuffix());
1068         }
1069         return testTag;
1070     }
1071 
1072     /**
1073      * Delete the invocation files if this is the last shard for local sharding or if we are not in
1074      * a local sharding situation.
1075      */
deleteInvocationFiles(TestInformation testInfo, IConfiguration config)1076     private void deleteInvocationFiles(TestInformation testInfo, IConfiguration config) {
1077         Object obj = config.getConfigurationObject(ShardHelper.LAST_SHARD_DETECTOR);
1078         if (obj != null) {
1079             LastShardDetector lastShardDetector = (LastShardDetector) obj;
1080             if (!lastShardDetector.isLastShardDone()) {
1081                 return;
1082             }
1083         }
1084         // Delete the invocation work directory at the end
1085         FileUtil.recursiveDelete(testInfo.dependenciesFolder());
1086         // Delete all the execution files
1087         testInfo.executionFiles().clearFiles();
1088     }
1089 
handleAndLogReleaseState( IInvocationContext context, Throwable exception)1090     private Map<ITestDevice, FreeDeviceState> handleAndLogReleaseState(
1091             IInvocationContext context, Throwable exception) {
1092         // Capture the FreeDeviceState of the primary device
1093         Map<ITestDevice, FreeDeviceState> devicesStates =
1094                 CommandScheduler.createReleaseMap(context, exception);
1095         if (devicesStates.size() >= 1) {
1096             addInvocationMetric(
1097                     InvocationMetricKey.DEVICE_RELEASE_STATE,
1098                     devicesStates.values().iterator().next().toString());
1099         }
1100         int countPhysicalLost = 0;
1101         int countVirtualLost = 0;
1102         for (Entry<ITestDevice, FreeDeviceState> fds : devicesStates.entrySet()) {
1103             // TODO: Rely on the FailureStatus for lost devices instead
1104             if (fds.getKey().getIDevice() instanceof TcpDevice
1105                     && exception instanceof DeviceNotAvailableException) {
1106                 countVirtualLost++;
1107                 continue;
1108             }
1109             if (fds.getKey().getIDevice() instanceof StubDevice) {
1110                 continue;
1111             }
1112             if (FreeDeviceState.UNAVAILABLE.equals(fds.getValue())) {
1113                 countPhysicalLost++;
1114             }
1115         }
1116         if (countPhysicalLost > 0) {
1117             addInvocationMetric(InvocationMetricKey.DEVICE_LOST_DETECTED, countPhysicalLost);
1118             if (GlobalConfiguration.getDeviceManagerInstance() instanceof DeviceManager) {
1119                 String adbOutput =
1120                         ((DeviceManager) GlobalConfiguration.getDeviceManagerInstance())
1121                                 .executeGlobalAdbCommand("devices");
1122                 CLog.e("'adb devices' output:\n%s", adbOutput);
1123             }
1124         } else if (countVirtualLost > 0) {
1125             addInvocationMetric(InvocationMetricKey.VIRTUAL_DEVICE_LOST_DETECTED, countVirtualLost);
1126         }
1127         return devicesStates;
1128     }
1129 
1130     /** Helper Thread that ensures host_log is reported in case of killed JVM */
1131     private class ReportHostLog extends Thread {
1132 
1133         private ITestInvocationListener mListener;
1134         private IConfiguration mConfiguration;
1135 
ReportHostLog(ITestInvocationListener listener, IConfiguration config)1136         public ReportHostLog(ITestInvocationListener listener, IConfiguration config) {
1137             mListener = listener;
1138             mConfiguration = config;
1139         }
1140 
1141         @Override
run()1142         public void run() {
1143             // Report all the logs that always be reported anyway.
1144             reportHostLog(mListener, mConfiguration);
1145         }
1146     }
1147 
1148     /** Helper Thread to ensure invocation files are deleted in case of killed JVM */
1149     private class CleanUpInvocationFiles extends Thread {
1150 
1151         private TestInformation mTestInfo;
1152         private IConfiguration mConfig;
1153 
CleanUpInvocationFiles(TestInformation currentInfo, IConfiguration config)1154         public CleanUpInvocationFiles(TestInformation currentInfo, IConfiguration config) {
1155             mTestInfo = currentInfo;
1156             mConfig = config;
1157         }
1158 
1159         @Override
run()1160         public void run() {
1161             deleteInvocationFiles(mTestInfo, mConfig);
1162         }
1163     }
1164 }
1165