• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1  /*
2   * Copyright (C) 2018 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.annotations.VisibleForTesting;
19  import com.android.tradefed.build.BuildRetrievalError;
20  import com.android.tradefed.build.IBuildInfo;
21  import com.android.tradefed.build.StubBuildProvider;
22  import com.android.tradefed.clearcut.ClearcutClient;
23  import com.android.tradefed.command.CommandOptions;
24  import com.android.tradefed.command.CommandRunner;
25  import com.android.tradefed.config.ConfigurationException;
26  import com.android.tradefed.config.GlobalConfiguration;
27  import com.android.tradefed.config.IConfigOptionValueTransformer;
28  import com.android.tradefed.config.IConfiguration;
29  import com.android.tradefed.config.IDeviceConfiguration;
30  import com.android.tradefed.config.Option;
31  import com.android.tradefed.config.OptionCopier;
32  import com.android.tradefed.config.OptionSetter;
33  import com.android.tradefed.device.DeviceNotAvailableException;
34  import com.android.tradefed.device.DeviceSelectionOptions;
35  import com.android.tradefed.device.DeviceSelectionOptions.DeviceRequestedType;
36  import com.android.tradefed.device.ITestDevice;
37  import com.android.tradefed.device.TestDeviceOptions;
38  import com.android.tradefed.device.TestDeviceOptions.InstanceType;
39  import com.android.tradefed.device.cloud.GceAvdInfo;
40  import com.android.tradefed.device.cloud.GceManager;
41  import com.android.tradefed.device.cloud.ManagedRemoteDevice;
42  import com.android.tradefed.device.cloud.RemoteFileUtil;
43  import com.android.tradefed.device.connection.AdbSshConnection;
44  import com.android.tradefed.error.IHarnessException;
45  import com.android.tradefed.invoker.logger.CurrentInvocation;
46  import com.android.tradefed.invoker.logger.InvocationMetricLogger;
47  import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
48  import com.android.tradefed.log.ITestLogger;
49  import com.android.tradefed.log.LogUtil.CLog;
50  import com.android.tradefed.result.FailureDescription;
51  import com.android.tradefed.result.FileInputStreamSource;
52  import com.android.tradefed.result.ITestInvocationListener;
53  import com.android.tradefed.result.InputStreamSource;
54  import com.android.tradefed.result.LogDataType;
55  import com.android.tradefed.result.error.ErrorIdentifier;
56  import com.android.tradefed.result.proto.FileProtoResultReporter;
57  import com.android.tradefed.result.proto.ProtoResultParser;
58  import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
59  import com.android.tradefed.service.TradefedFeatureServer;
60  import com.android.tradefed.targetprep.BuildError;
61  import com.android.tradefed.targetprep.TargetSetupError;
62  import com.android.tradefed.testtype.SubprocessTfLauncher;
63  import com.android.tradefed.util.CommandResult;
64  import com.android.tradefed.util.CommandStatus;
65  import com.android.tradefed.util.FileUtil;
66  import com.android.tradefed.util.IRunUtil;
67  import com.android.tradefed.util.RunUtil;
68  import com.android.tradefed.util.SerializationUtil;
69  import com.android.tradefed.util.SubprocessExceptionParser;
70  import com.android.tradefed.util.SystemUtil;
71  import com.android.tradefed.util.TimeUtil;
72  import com.android.tradefed.util.proto.TestRecordProtoUtil;
73  
74  import com.google.common.base.Strings;
75  import com.google.protobuf.InvalidProtocolBufferException;
76  
77  import java.io.File;
78  import java.io.IOException;
79  import java.io.PrintWriter;
80  import java.util.ArrayList;
81  import java.util.Arrays;
82  import java.util.HashMap;
83  import java.util.HashSet;
84  import java.util.List;
85  import java.util.Map;
86  import java.util.UUID;
87  
88  /** Implementation of {@link InvocationExecution} that drives a remote execution. */
89  public class RemoteInvocationExecution extends InvocationExecution {
90  
91      public static final String START_FEATURE_SERVER = "START_FEATURE_SERVER";
92      public static final long PUSH_TF_TIMEOUT = 150000L;
93      public static final long PULL_RESULT_TIMEOUT = 180000L;
94      public static final long REMOTE_PROCESS_RUNNING_WAIT = 15000L;
95      public static final long LAUNCH_EXTRA_DEVICE = 15 * 60 * 1000L;
96      public static final long SETUP_REMOTE_DIR_TIMEOUT = 10 * 60 * 1000L;
97      public static final long NEW_USER_TIMEOUT = 5 * 60 * 1000L;
98      public static final long JOIN_CLEAN_TIMEOUT_MS = 2 * 60 * 1000L;
99  
100      public static final String REMOTE_USER_DIR = "/home/{$USER}/";
101      public static final String PROTO_RESULT_NAME = "output.pb";
102      public static final String STDOUT_FILE = "screen-VM_tradefed-stdout.txt";
103      public static final String STDERR_FILE = "screen-VM_tradefed-stderr.txt";
104      public static final String REMOTE_CONFIG = "configuration";
105      public static final String GLOBAL_REMOTE_CONFIG = "global-remote-configuration";
106  
107      private static final int MAX_CONNECTION_REFUSED_COUNT = 3;
108      private static final int MAX_PUSH_TF_ATTEMPTS = 3;
109      private static final String TRADEFED_EARLY_TERMINATION =
110              "Remote Tradefed might have terminated early.\nRemote Stderr:\n%s";
111      /**
112       * Pass these invocation context attributes to remote if they are not part of invocation data
113       */
114      private static final String[] INVOCATION_CONTEXT_ATTR_TO_DATA = {
115          "invocation_id", "work_unit_id"
116      };
117  
118      private String mRemoteTradefedDir = null;
119      private String mRemoteAdbPath = null;
120      private ProtoResultParser mProtoParser = null;
121      private String mRemoteConsoleStdErr = null;
122  
123      @Override
fetchBuild( TestInformation testInfo, IConfiguration config, IRescheduler rescheduler, ITestInvocationListener listener)124      public boolean fetchBuild(
125              TestInformation testInfo,
126              IConfiguration config,
127              IRescheduler rescheduler,
128              ITestInvocationListener listener)
129              throws DeviceNotAvailableException, BuildRetrievalError {
130          // TODO: handle multiple devices/build config
131          StubBuildProvider stubProvider = new StubBuildProvider();
132  
133          String deviceName = config.getDeviceConfig().get(0).getDeviceName();
134          OptionCopier.copyOptionsNoThrow(
135                  config.getDeviceConfig().get(0).getBuildProvider(), stubProvider);
136  
137          IBuildInfo info = stubProvider.getBuild();
138          if (info == null) {
139              return false;
140          }
141          testInfo.getContext().addDeviceBuildInfo(deviceName, info);
142          updateBuild(info, config);
143          return true;
144      }
145  
146      @Override
customizeDevicePreInvocation(IConfiguration config, IInvocationContext context)147      protected void customizeDevicePreInvocation(IConfiguration config, IInvocationContext context) {
148          super.customizeDevicePreInvocation(config, context);
149  
150          if (config.getCommandOptions().getShardCount() != null
151                  && config.getCommandOptions().getShardIndex() == null
152                  && !config.getCommandOptions().isRemoteInvocationDeviceless()) {
153              ITestDevice device = context.getDevices().get(0);
154              TestDeviceOptions options = device.getOptions();
155              // Trigger the multi-tenant start in the VM
156              options.addGceDriverParams("--num-avds-per-instance");
157              String count = config.getCommandOptions().getShardCount().toString();
158              options.addGceDriverParams(count);
159              InvocationMetricLogger.addInvocationMetrics(
160                      InvocationMetricKey.CF_INSTANCE_COUNT, count);
161          }
162      }
163  
164      @Override
runTests( TestInformation info, IConfiguration config, ITestInvocationListener listener)165      public void runTests(
166              TestInformation info, IConfiguration config, ITestInvocationListener listener)
167              throws Throwable {
168          ManagedRemoteDevice device = (ManagedRemoteDevice) info.getDevice();
169          GceAvdInfo gceInfo = ((AdbSshConnection) device.getConnection()).getAvdInfo();
170  
171          // Run remote TF (new tests?)
172          IRunUtil runUtil = new RunUtil();
173  
174          TestDeviceOptions options = device.getOptions();
175          String mainRemoteDir = getRemoteMainDir(options);
176          mRemoteAdbPath = String.format("/home/%s/bin/adb", options.getInstanceUser());
177          // Select the TF version that should be pushed to the remote VM
178          File tfToPush = getLocalTradefedPath(listener, options.getRemoteTf());
179          if (tfToPush == null) {
180              return;
181          }
182  
183          String invocationWorkDir =
184                  String.format("%stf-invocation-%s/", mainRemoteDir, UUID.randomUUID().toString());
185          CLog.d("Remote invocation work directory is at %s", invocationWorkDir);
186  
187          CommandResult cr =
188                  GceManager.remoteSshCommandExecution(
189                          gceInfo, options, runUtil, 120000L, "mkdir", "-p", invocationWorkDir);
190          if (!CommandStatus.SUCCESS.equals(cr.getStatus())) {
191              CLog.e("Creation of %s failed.", invocationWorkDir);
192              CLog.e("Command stdout: %s, stderr: %s", cr.getStdout(), cr.getStderr());
193              listener.invocationFailed(
194                      createInvocationFailure(
195                              "Failed to create remote tradefed dir.", FailureStatus.INFRA_FAILURE));
196              return;
197          }
198  
199          // Push Tradefed to the remote
200          int attempt = 0;
201          boolean result = false;
202          while (!result && attempt < MAX_PUSH_TF_ATTEMPTS) {
203              result =
204                      RemoteFileUtil.pushFileToRemote(
205                              gceInfo,
206                              options,
207                              Arrays.asList("-r"),
208                              runUtil,
209                              PUSH_TF_TIMEOUT,
210                              invocationWorkDir,
211                              tfToPush);
212              attempt++;
213          }
214          if (!result) {
215              CLog.e("Failed to push Tradefed.");
216              listener.invocationFailed(
217                      createInvocationFailure(
218                              "Failed to push Tradefed.", FailureStatus.INFRA_FAILURE));
219              return;
220          }
221  
222          mRemoteTradefedDir = invocationWorkDir + tfToPush.getName() + "/";
223          CommandResult listRemoteDir =
224                  GceManager.remoteSshCommandExecution(
225                          gceInfo, options, runUtil, 120000L, "ls", "-l", mRemoteTradefedDir);
226          CLog.d("stdout: %s", listRemoteDir.getStdout());
227          CLog.d("stderr: %s", listRemoteDir.getStderr());
228  
229          File configFile =
230                  createRemoteConfig(info.getContext(), config, listener, mRemoteTradefedDir);
231          File globalConfig = null;
232          try {
233              CLog.d("Pushing Tradefed XML configuration to remote.");
234              boolean resultPush =
235                      RemoteFileUtil.pushFileToRemote(
236                              gceInfo,
237                              options,
238                              null,
239                              runUtil,
240                              PUSH_TF_TIMEOUT,
241                              mRemoteTradefedDir,
242                              configFile);
243              if (!resultPush) {
244                  CLog.e("Failed to push Tradefed Configuration.");
245                  listener.invocationFailed(
246                          createInvocationFailure(
247                                  "Failed to push Tradefed Configuration.",
248                                  FailureStatus.INFRA_FAILURE));
249                  return;
250              }
251  
252              String[] allowListConfigs =
253                      new String[] {
254                          GlobalConfiguration.SANDBOX_FACTORY_TYPE_NAME,
255                          GlobalConfiguration.HOST_OPTIONS_TYPE_NAME,
256                          "android-build"
257                      };
258              // use an IConfigOptionValueTransformer to collect+rewrite options that are File type
259              FileOptionValueTransformer fileTransformer =
260                      new FileOptionValueTransformer(mRemoteTradefedDir);
261              try {
262                  globalConfig =
263                          GlobalConfiguration.getInstance()
264                                  .cloneConfigWithFilter(
265                                          new HashSet<>(), fileTransformer, true, allowListConfigs);
266              } catch (IOException e) {
267                  listener.invocationFailed(createInvocationFailure(e, FailureStatus.INFRA_FAILURE));
268                  return;
269              }
270              try (InputStreamSource source = new FileInputStreamSource(globalConfig)) {
271                  listener.testLog(GLOBAL_REMOTE_CONFIG, LogDataType.HARNESS_CONFIG, source);
272              }
273              // Push the global configuration
274              boolean resultPushGlobal =
275                      RemoteFileUtil.pushFileToRemote(
276                              gceInfo,
277                              options,
278                              null,
279                              runUtil,
280                              PUSH_TF_TIMEOUT,
281                              mRemoteTradefedDir,
282                              globalConfig);
283              if (!resultPushGlobal) {
284                  CLog.e("Failed to push Tradefed Global Configuration.");
285                  listener.invocationFailed(
286                          createInvocationFailure(
287                                  "Failed to push Tradefed Global Configuration.",
288                                  FailureStatus.INFRA_FAILURE));
289                  return;
290              }
291              // Push files referenced in global config over to remote
292              if (!fileTransformer.getRenamedFiles().isEmpty()) {
293                  List<String> pushErrors = new ArrayList<>();
294                  CLog.d("Pushing files referenced in global config to remote.");
295                  for (Map.Entry<String, String> entry :
296                          fileTransformer.getRenamedFiles().entrySet()) {
297                      boolean pushResult =
298                              RemoteFileUtil.pushFileToRemote(
299                                      gceInfo,
300                                      options,
301                                      null,
302                                      runUtil,
303                                      PUSH_TF_TIMEOUT,
304                                      entry.getValue(),
305                                      new File(entry.getKey()));
306                      if (!pushResult) {
307                          pushErrors.add(entry.getKey());
308                      }
309                  }
310                  CLog.d("Done pushing files.");
311                  if (!pushErrors.isEmpty()) {
312                      listener.invocationFailed(
313                              createInvocationFailure(
314                                      "Failed to push some files to remote: " + pushErrors,
315                                      FailureStatus.INFRA_FAILURE));
316                  }
317              }
318  
319              resetAdb(gceInfo, options, runUtil);
320              runRemote(
321                      listener,
322                      info.getContext(),
323                      configFile,
324                      gceInfo,
325                      options,
326                      runUtil,
327                      config,
328                      globalConfig);
329              collectAdbLogs(gceInfo, options, runUtil, listener);
330          } finally {
331              FileUtil.recursiveDelete(configFile);
332              FileUtil.recursiveDelete(globalConfig);
333              cr =
334                      GceManager.remoteSshCommandExecution(
335                              gceInfo, options, runUtil, 120000L, "rm", "-rf", invocationWorkDir);
336              if (!CommandStatus.SUCCESS.equals(cr.getStatus())) {
337                  CLog.w("Clean up of %s failed.", invocationWorkDir);
338                  CLog.w("Command stdout: %s, stderr: %s", cr.getStdout(), cr.getStderr());
339              }
340          }
341      }
342  
343      @Override
doSetup(TestInformation testInfo, IConfiguration config, ITestLogger logger)344      public void doSetup(TestInformation testInfo, IConfiguration config, ITestLogger logger)
345              throws TargetSetupError, BuildError, DeviceNotAvailableException {
346          // Skip
347      }
348  
349      @Override
doTeardown( TestInformation testInfo, IConfiguration config, ITestLogger logger, Throwable exception)350      public void doTeardown(
351              TestInformation testInfo,
352              IConfiguration config,
353              ITestLogger logger,
354              Throwable exception)
355              throws Throwable {
356          super.runDevicePostInvocationTearDown(testInfo.getContext(), config, exception);
357      }
358  
359      @Override
doCleanUp(IInvocationContext context, IConfiguration config, Throwable exception)360      public void doCleanUp(IInvocationContext context, IConfiguration config, Throwable exception) {
361          // Skip
362      }
363  
364      @Override
getAdbVersion()365      protected String getAdbVersion() {
366          // Do not report the adb version from the parent, the remote child will remote its own.
367          return null;
368      }
369  
runRemote( ITestInvocationListener currentInvocationListener, IInvocationContext context, File configFile, GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil, IConfiguration config, File globalConfig)370      private void runRemote(
371              ITestInvocationListener currentInvocationListener,
372              IInvocationContext context,
373              File configFile,
374              GceAvdInfo info,
375              TestDeviceOptions options,
376              IRunUtil runUtil,
377              IConfiguration config,
378              File globalConfig)
379              throws InvalidProtocolBufferException, IOException {
380          List<String> remoteTfCommand = new ArrayList<>();
381          remoteTfCommand.add("pushd");
382          remoteTfCommand.add(mRemoteTradefedDir + ";");
383          remoteTfCommand.add(String.format("PATH=%s:$PATH", new File(mRemoteAdbPath).getParent()));
384          remoteTfCommand.add("screen -dmSU tradefed sh -c");
385  
386          StringBuilder tfCmdBuilder =
387                  new StringBuilder("TF_GLOBAL_CONFIG=" + globalConfig.getName());
388          // Set an env variable to notify that this a remote environment.
389          tfCmdBuilder.append(" " + SystemUtil.REMOTE_VM_VARIABLE + "=1");
390          // Disable clearcut in the remote
391          tfCmdBuilder.append(" " + ClearcutClient.DISABLE_CLEARCUT_KEY + "=1");
392          tfCmdBuilder.append(" " + START_FEATURE_SERVER + "=1");
393          tfCmdBuilder.append(" ENTRY_CLASS=" + CommandRunner.class.getCanonicalName());
394          tfCmdBuilder.append(" ./tradefed.sh " + mRemoteTradefedDir + configFile.getName());
395          if (config.getCommandOptions().shouldUseRemoteSandboxMode()) {
396              tfCmdBuilder.append(" --" + CommandOptions.USE_SANDBOX);
397          }
398          tfCmdBuilder.append(" > " + STDOUT_FILE + " 2> " + STDERR_FILE);
399          remoteTfCommand.add("\"" + tfCmdBuilder.toString() + "\"");
400          // Kick off the actual remote run
401          CommandResult resultRemoteExecution =
402                  GceManager.remoteSshCommandExecution(
403                          info, options, runUtil, 0L, remoteTfCommand.toArray(new String[0]));
404          if (!CommandStatus.SUCCESS.equals(resultRemoteExecution.getStatus())) {
405              CLog.e("Error running the remote command: %s", resultRemoteExecution.getStdout());
406              currentInvocationListener.invocationFailed(
407                      createInvocationFailure(
408                              resultRemoteExecution.getStderr(), FailureStatus.INFRA_FAILURE));
409              return;
410          }
411          // Sleep a bit to let the process start
412          RunUtil.getDefault().sleep(10000L);
413  
414          mProtoParser = new ProtoResultParser(currentInvocationListener, context, false, "remote-");
415          // Print when parsing
416          mProtoParser.setQuiet(false);
417          // Do not report accounting again, it should have been done in the remote
418          mProtoParser.setSkipParsingAccounting(true);
419          // Monitor the remote invocation to ensure it's completing. Block until timeout or stops
420          // running.
421          boolean stillRunning = true;
422          try {
423              stillRunning =
424                      isStillRunning(
425                              currentInvocationListener, configFile, info, options, runUtil, config);
426          } finally {
427              // Fetch the logs for debugging
428              File stdout =
429                      fetchRemoteAndLogFile(
430                              currentInvocationListener,
431                              STDOUT_FILE,
432                              STDOUT_FILE,
433                              info,
434                              options,
435                              runUtil);
436              FileUtil.recursiveDelete(stdout);
437              File stderr =
438                      fetchRemoteAndLogFile(
439                              currentInvocationListener,
440                              STDERR_FILE,
441                              STDERR_FILE,
442                              info,
443                              options,
444                              runUtil);
445              if (stderr != null && stderr.exists()) {
446                  mRemoteConsoleStdErr = FileUtil.readStringFromFile(stderr);
447                  FileUtil.recursiveDelete(stderr);
448              } else {
449                  mRemoteConsoleStdErr = "Failed to fetch stderr from remote.";
450              }
451          }
452  
453          // If no result in progress are reported, parse the full results at the end.
454          if (!config.getCommandOptions().shouldReportModuleProgression()) {
455              fetchAndProcessResults(
456                      stillRunning,
457                      currentInvocationListener,
458                      info,
459                      options,
460                      runUtil,
461                      mRemoteTradefedDir);
462          } else if (!mProtoParser.invocationEndedReached()) {
463              String message =
464                      String.format(
465                              "Parsing of results protos might be incomplete: invocation ended "
466                                      + "of remote execution was not found. "
467                                      + TRADEFED_EARLY_TERMINATION,
468                              mRemoteConsoleStdErr);
469              String exceptionRemoteFilePath =
470                      SubprocessExceptionParser.getPathFromStderr(mRemoteConsoleStdErr);
471              FailureDescription failureDescription =
472                      createInvocationFailure(message, FailureStatus.INFRA_FAILURE);
473              if (exceptionRemoteFilePath != null) {
474                  File remoteSerialized =
475                          RemoteFileUtil.fetchRemoteFile(
476                                  info,
477                                  options,
478                                  runUtil,
479                                  PULL_RESULT_TIMEOUT,
480                                  exceptionRemoteFilePath);
481                  if (remoteSerialized != null) {
482                      try {
483                          Throwable obj =
484                                  (Throwable) SerializationUtil.deserialize(remoteSerialized, true);
485                          failureDescription =
486                                  createInvocationFailure(obj, FailureStatus.INFRA_FAILURE);
487                      } catch (IOException e) {
488                          // Ignored
489                          CLog.w("Could not parse the stderr as a particular exception.");
490                      }
491                  }
492              }
493              currentInvocationListener.invocationFailed(failureDescription);
494          }
495      }
496  
isStillRunning( ITestInvocationListener currentInvocationListener, File configFile, GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil, IConfiguration config)497      private boolean isStillRunning(
498              ITestInvocationListener currentInvocationListener,
499              File configFile,
500              GceAvdInfo info,
501              TestDeviceOptions options,
502              IRunUtil runUtil,
503              IConfiguration config)
504              throws IOException {
505          long maxTimeout = config.getCommandOptions().getInvocationTimeout();
506          Long endTime = null;
507          if (maxTimeout > 0L) {
508              endTime = System.currentTimeMillis() + maxTimeout;
509          }
510          boolean stillRunning = true;
511          int errorConnectCount = 0;
512          int currentIndex = 0;
513          long currentTimeOnProto = 0L;
514          while (stillRunning) {
515              if (config.getCommandOptions().shouldReportModuleProgression()) {
516                  String remoteFilePath = mRemoteTradefedDir + PROTO_RESULT_NAME + currentIndex;
517                  if (RemoteFileUtil.doesRemoteFileExist(
518                          info, options, runUtil, PULL_RESULT_TIMEOUT, remoteFilePath)) {
519                      File resultFile =
520                              RemoteFileUtil.fetchRemoteFile(
521                                      info, options, runUtil, PULL_RESULT_TIMEOUT, remoteFilePath);
522                      if (resultFile != null) {
523                          currentIndex++;
524                          currentTimeOnProto = System.currentTimeMillis();
525                          try {
526                              mProtoParser.processFileProto(resultFile);
527                          } finally {
528                              FileUtil.deleteFile(resultFile);
529                          }
530                          // Don't sleep in that case since we might have more file to process, this
531                          // will sleep next time we don't find a file to process on the remote.
532                          continue;
533                      }
534                  }
535              }
536              if (System.currentTimeMillis() - currentTimeOnProto > 7200000) { // 2 hours
537                  // If we are stuck on waiting the same proto for over 2 hours, collect some logs
538                  File stdout =
539                          fetchRemoteAndLogFile(
540                                  currentInvocationListener,
541                                  STDOUT_FILE,
542                                  STDOUT_FILE + "-early",
543                                  info,
544                                  options,
545                                  runUtil);
546                  FileUtil.recursiveDelete(stdout);
547                  currentTimeOnProto = System.currentTimeMillis();
548              }
549  
550              CommandResult psRes =
551                      GceManager.remoteSshCommandExecution(
552                              info,
553                              options,
554                              runUtil,
555                              120000L,
556                              "ps",
557                              "-ef",
558                              "| grep",
559                              CommandRunner.class.getCanonicalName());
560              if (!CommandStatus.SUCCESS.equals(psRes.getStatus())) {
561                  errorConnectCount++;
562                  // If we get several connection errors in a row, give up.
563                  if (errorConnectCount > MAX_CONNECTION_REFUSED_COUNT) {
564                      CLog.e("Failed to connect to the remote to check running status.");
565                      return false;
566                  }
567              } else {
568                  // Reset the error count
569                  errorConnectCount = 0;
570                  CLog.d("ps -ef: stdout: %s\nstderr: %s\n", psRes.getStdout(), psRes.getStderr());
571                  stillRunning = psRes.getStdout().contains(configFile.getName());
572                  CLog.d("still running: %s", stillRunning);
573                  if (endTime != null && System.currentTimeMillis() > endTime) {
574                      currentInvocationListener.invocationFailed(
575                              createInvocationFailure(
576                                      String.format(
577                                              "Remote invocation timeout after %s",
578                                              TimeUtil.formatElapsedTime(maxTimeout)),
579                                      FailureStatus.TIMED_OUT));
580                      break;
581                  }
582              }
583              if (stillRunning) {
584                  RunUtil.getDefault().sleep(REMOTE_PROCESS_RUNNING_WAIT);
585              }
586          }
587  
588          File resultFile = null;
589          if (config.getCommandOptions().shouldReportModuleProgression()) {
590              // Process all remaining proto files available
591              do {
592                  String remoteFilePath = mRemoteTradefedDir + PROTO_RESULT_NAME + currentIndex;
593                  if (RemoteFileUtil.doesRemoteFileExist(
594                          info, options, runUtil, PULL_RESULT_TIMEOUT, remoteFilePath)) {
595                      resultFile =
596                              RemoteFileUtil.fetchRemoteFile(
597                                      info, options, runUtil, PULL_RESULT_TIMEOUT, remoteFilePath);
598                      if (resultFile != null) {
599                          currentIndex++;
600                          try {
601                              mProtoParser.processFileProto(resultFile);
602                          } finally {
603                              FileUtil.deleteFile(resultFile);
604                          }
605                      }
606                  } else {
607                      break;
608                  }
609              } while (resultFile != null);
610          }
611          return stillRunning;
612      }
613  
614      /** Returns the main remote working directory. */
getRemoteMainDir(TestDeviceOptions options)615      private String getRemoteMainDir(TestDeviceOptions options) {
616          return REMOTE_USER_DIR.replace("{$USER}", options.getInstanceUser());
617      }
618  
619      /**
620       * Sometimes remote adb version is a bit weird and is not running properly the first time. Try
621       * it out once to ensure it starts.
622       */
resetAdb(GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil)623      private void resetAdb(GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil) {
624          CommandResult probAdb =
625                  GceManager.remoteSshCommandExecution(
626                          info, options, runUtil, 120000L, mRemoteAdbPath, "devices");
627          CLog.d("remote adb prob: %s", probAdb.getStdout());
628          CLog.d("%s", probAdb.getStderr());
629  
630          CommandResult versionAdb =
631                  GceManager.remoteSshCommandExecution(
632                          info, options, runUtil, 120000L, mRemoteAdbPath, "version");
633          CLog.d("version adb: %s", versionAdb.getStdout());
634          CLog.d("%s", versionAdb.getStderr());
635      }
636  
637      /**
638       * Remote invocation relies on the adb of the remote, so always collect its logs to make sure we
639       * can debug it appropriately.
640       */
collectAdbLogs( GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil, ITestLogger logger)641      private void collectAdbLogs(
642              GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil, ITestLogger logger) {
643          CommandResult tmpDirFolder =
644                  GceManager.remoteSshCommandExecution(
645                          info, options, runUtil, 120000L, "bash -c \"echo \\$TMPDIR\"");
646          String folder = tmpDirFolder.getStdout().trim();
647          CLog.d("Remote TMPDIR folder is: %s", folder);
648          if (Strings.isNullOrEmpty(folder)) {
649              // If TMPDIR is not set, default to /tmp/ location.
650              folder = "/tmp";
651          }
652          CommandResult uid =
653                  GceManager.remoteSshCommandExecution(
654                          info, options, new RunUtil(), 120000L, "bash -c \"echo \\$UID\"");
655          String uidString = uid.getStdout().trim();
656          CLog.d("Remote $UID for adb is: %s", uidString);
657  
658          if (Strings.isNullOrEmpty(uidString)) {
659              CLog.w("Could not determine adb log path.");
660              return;
661          }
662  
663          GceManager.logNestedRemoteFile(
664                  logger,
665                  info,
666                  options,
667                  runUtil,
668                  folder + "/adb." + uidString + ".log",
669                  LogDataType.TEXT,
670                  "full_adb.log");
671      }
672  
673      /**
674       * Create the configuration that will run in the remote VM.
675       *
676       * @param context the {@link IInvocationContext} for the current invocation
677       * @param config The main {@link IConfiguration}.
678       * @param logger A logger where to save the XML configuration for debugging.
679       * @param resultDirPath the remote result dir where results should be saved.
680       * @return A file containing the dumped remote XML configuration.
681       * @throws IOException
682       */
683      @VisibleForTesting
createRemoteConfig( IInvocationContext context, IConfiguration config, ITestLogger logger, String resultDirPath)684      File createRemoteConfig(
685              IInvocationContext context,
686              IConfiguration config,
687              ITestLogger logger,
688              String resultDirPath)
689              throws IOException, ConfigurationException {
690          // Setup the remote reporting to a proto file
691          List<ITestInvocationListener> reporters = new ArrayList<>();
692          FileProtoResultReporter protoReporter = new FileProtoResultReporter();
693          OptionSetter protoResSetter = new OptionSetter(protoReporter);
694          if (config.getCommandOptions().shouldReportModuleProgression()) {
695              protoResSetter.setOptionValue(
696                      FileProtoResultReporter.PERIODIC_PROTO_WRITING_OPTION, "true");
697          }
698          protoResSetter.setOptionValue(
699                  FileProtoResultReporter.PROTO_OUTPUT_FILE,
700                  new File(resultDirPath + PROTO_RESULT_NAME).getPath());
701          reporters.add(protoReporter);
702  
703          config.setTestInvocationListeners(reporters);
704  
705          for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
706              deviceConfig.getDeviceRequirements().setSerial();
707              if (deviceConfig.getDeviceRequirements() instanceof DeviceSelectionOptions) {
708                  ((DeviceSelectionOptions) deviceConfig.getDeviceRequirements())
709                          .setDeviceTypeRequested(null);
710                  if (config.getCommandOptions().isRemoteInvocationDeviceless()) {
711                      ((DeviceSelectionOptions) deviceConfig.getDeviceRequirements())
712                              .setDeviceTypeRequested(DeviceRequestedType.NULL_DEVICE);
713                  }
714              }
715              // For deviceless reset instance type so remote has right type
716              if (config.getCommandOptions().isRemoteInvocationDeviceless()) {
717                  deviceConfig.getDeviceOptions().setInstanceType(InstanceType.GCE);
718              }
719          }
720  
721          if (config.getCommandOptions().getShardCount() != null
722                  && config.getCommandOptions().getShardIndex() == null) {
723              config.getCommandOptions().setReplicateSetup(true);
724          }
725  
726          // Lower the remote invocation timeout to trigger an interrupt
727          long invocationTimeout =
728                  Math.max(config.getCommandOptions().getInvocationTimeout() - 120000L, 0L);
729          config.getCommandOptions().setInvocationTimeout(invocationTimeout);
730  
731          // Mark the remote invocation as subprocess
732          config.getCommandOptions()
733                  .getInvocationData()
734                  .put(SubprocessTfLauncher.SUBPROCESS_TAG_NAME, "true");
735  
736          // Pass invocation and work unit ids for local invocation since they are not provided as
737          // command line invocation-data options
738          for (String key : INVOCATION_CONTEXT_ATTR_TO_DATA) {
739              if (!config.getCommandOptions().getInvocationData().containsKey(key)) {
740                  String value = context.getAttribute(key);
741                  if (!Strings.isNullOrEmpty(value)) {
742                      config.getCommandOptions().getInvocationData().put(key, value);
743                  }
744              }
745          }
746  
747          // Clear the server reference as remote will run its own.
748          if (GlobalConfiguration.getInstance().getFeatureServer() != null) {
749              GlobalConfiguration.getInstance().getFeatureServer().unregisterInvocation(config);
750          }
751          config.getConfigurationDescription().removeMetadata(TradefedFeatureServer.SERVER_REFERENCE);
752  
753          // Unset remote-tf-version to avoid re-downloading from remote VM.
754          OptionSetter deviceOptions =
755                  new OptionSetter(config.getDeviceConfig().get(0).getDeviceOptions());
756          deviceOptions.setOptionValue(TestDeviceOptions.REMOTE_TF_VERSION_OPTION, "");
757  
758          // Dump and log the configuration
759          File configFile = FileUtil.createTempFile(config.getName(), ".xml");
760          config.dumpXml(
761                  new PrintWriter(configFile),
762                  new ArrayList<String>(),
763                  /* print deprecated */ true,
764                  /* print unchanged*/ false);
765          try (InputStreamSource source = new FileInputStreamSource(configFile)) {
766              logger.testLog(REMOTE_CONFIG, LogDataType.HARNESS_CONFIG, source);
767          }
768          return configFile;
769      }
770  
771      /** Returns the Tradefed version that should be pushed to the remote to drive the invocation. */
getLocalTradefedPath(ITestInvocationListener listener, File remoteTf)772      private File getLocalTradefedPath(ITestInvocationListener listener, File remoteTf) {
773          if (remoteTf != null && remoteTf.exists()) {
774              return remoteTf;
775          }
776  
777          String tfPath = System.getProperty("TF_JAR_DIR");
778          if (tfPath == null) {
779              listener.invocationFailed(
780                      createInvocationFailure(
781                              "Failed to find $TF_JAR_DIR.", FailureStatus.INFRA_FAILURE));
782              return null;
783          }
784          File currentTf = new File(tfPath).getAbsoluteFile();
785          if (tfPath.equals(".")) {
786              currentTf = new File("").getAbsoluteFile();
787          }
788          return currentTf;
789      }
790  
fetchAndProcessResults( boolean wasStillRunning, ITestInvocationListener invocationListener, GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil, String resultDirPath)791      private void fetchAndProcessResults(
792              boolean wasStillRunning,
793              ITestInvocationListener invocationListener,
794              GceAvdInfo info,
795              TestDeviceOptions options,
796              IRunUtil runUtil,
797              String resultDirPath)
798              throws InvalidProtocolBufferException, IOException {
799          File resultFile = null;
800          if (wasStillRunning) {
801              CLog.d("Remote invocation was still running. No result can be pulled.");
802              return;
803          }
804          resultFile =
805                  RemoteFileUtil.fetchRemoteFile(
806                          info,
807                          options,
808                          runUtil,
809                          PULL_RESULT_TIMEOUT,
810                          resultDirPath + PROTO_RESULT_NAME);
811          if (resultFile == null) {
812              invocationListener.invocationFailed(
813                      createInvocationFailure(
814                              String.format(
815                                      "Could not find remote result file at %s. "
816                                              + TRADEFED_EARLY_TERMINATION,
817                                      resultDirPath + PROTO_RESULT_NAME,
818                                      mRemoteConsoleStdErr),
819                              FailureStatus.INFRA_FAILURE));
820              return;
821          }
822          CLog.d("Fetched remote result file!");
823          // Report result to listener.
824          try {
825              mProtoParser.processFinalizedProto(TestRecordProtoUtil.readFromFile(resultFile));
826          } finally {
827              FileUtil.deleteFile(resultFile);
828          }
829      }
830  
fetchRemoteAndLogFile( ITestLogger logger, String fileName, String logName, GceAvdInfo info, TestDeviceOptions options, IRunUtil runUtil)831      private File fetchRemoteAndLogFile(
832              ITestLogger logger,
833              String fileName,
834              String logName,
835              GceAvdInfo info,
836              TestDeviceOptions options,
837              IRunUtil runUtil) {
838          File file =
839                  RemoteFileUtil.fetchRemoteFile(
840                          info, options, runUtil, PULL_RESULT_TIMEOUT, mRemoteTradefedDir + fileName);
841          if (file != null) {
842              try (InputStreamSource source = new FileInputStreamSource(file, false)) {
843                  logger.testLog(logName, LogDataType.HARNESS_STD_LOG, source);
844              }
845          }
846          return file;
847      }
848  
createInvocationFailure(String errorMessage, FailureStatus status)849      private FailureDescription createInvocationFailure(String errorMessage, FailureStatus status) {
850          FailureDescription failure = FailureDescription.create(errorMessage);
851          failure.setFailureStatus(status);
852          failure.setCause(new RuntimeException(errorMessage));
853          return failure;
854      }
855  
createInvocationFailure( Throwable exception, FailureStatus defaultStatus)856      private FailureDescription createInvocationFailure(
857              Throwable exception, FailureStatus defaultStatus) {
858          ErrorIdentifier id = null;
859          if (exception instanceof IHarnessException) {
860              id = ((IHarnessException) exception).getErrorId();
861          }
862          String message = exception.getMessage();
863          if (message == null) {
864              message = "No error message";
865          }
866          FailureDescription failure =
867                  CurrentInvocation.createFailure(message, id).setCause(exception);
868          if (id == null) {
869              // Use default status if none available
870              failure.setFailureStatus(defaultStatus);
871          }
872          return failure;
873      }
874  
875      protected static class FileOptionValueTransformer implements IConfigOptionValueTransformer {
876  
877          private Map<String, String> mRenamedFiles = new HashMap<>();
878          private String mBaseRemoteDir;
879  
FileOptionValueTransformer(String baseRemoteDir)880          public FileOptionValueTransformer(String baseRemoteDir) {
881              mBaseRemoteDir = baseRemoteDir;
882          }
883  
getRenamedFiles()884          public Map<String, String> getRenamedFiles() {
885              return mRenamedFiles;
886          }
887  
shouldTransformValueForOption(Option option)888          private boolean shouldTransformValueForOption(Option option) {
889              // seems sufficient for now, may need more configurable mechanism eventually
890              return option.name().contains("key-file");
891          }
892  
handleFileTypeValue(File file)893          private File handleFileTypeValue(File file) {
894              // use String here because it might contain unresolved value e.g. "gs://"
895              String filePath = file.toString();
896              if (filePath.startsWith("/")) {
897                  // strip the leading '/' of the file path and replace the rest with '_'
898                  // i.e. /path/to/file becomes path_to_file
899                  String remoteName = mBaseRemoteDir + filePath.substring(1).replace('/', '_');
900                  mRenamedFiles.put(filePath, remoteName);
901                  CLog.v("File to be transferred: local=%s -> remote=%s", filePath, remoteName);
902                  return new File(remoteName);
903              }
904              return file;
905          }
906  
907          @Override
transform(Object configObj, Option option, Object fieldValue)908          public Object transform(Object configObj, Option option, Object fieldValue) {
909              if (fieldValue instanceof File) {
910                  if (shouldTransformValueForOption(option)) {
911                      return handleFileTypeValue((File) fieldValue);
912                  }
913              }
914              return fieldValue;
915          }
916      }
917  }
918