1 /*
2  * Copyright (C) 2017 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.sandbox;
17 
18 import com.android.annotations.VisibleForTesting;
19 import com.android.tradefed.command.CommandOptions;
20 import com.android.tradefed.config.ArgsOptionParser;
21 import com.android.tradefed.config.Configuration;
22 import com.android.tradefed.config.ConfigurationException;
23 import com.android.tradefed.config.ConfigurationFactory;
24 import com.android.tradefed.config.ConfigurationXmlParserSettings;
25 import com.android.tradefed.config.GlobalConfiguration;
26 import com.android.tradefed.config.IConfiguration;
27 import com.android.tradefed.config.IConfigurationFactory;
28 import com.android.tradefed.config.IDeviceConfiguration;
29 import com.android.tradefed.config.IGlobalConfiguration;
30 import com.android.tradefed.config.proxy.AutomatedReporters;
31 import com.android.tradefed.device.DeviceSelectionOptions;
32 import com.android.tradefed.device.IDeviceSelection.BaseDeviceType;
33 import com.android.tradefed.device.ManagedTestDeviceFactory;
34 import com.android.tradefed.invoker.IInvocationContext;
35 import com.android.tradefed.invoker.InvocationContext;
36 import com.android.tradefed.invoker.RemoteInvocationExecution;
37 import com.android.tradefed.invoker.TestInformation;
38 import com.android.tradefed.invoker.logger.CurrentInvocation;
39 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
40 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
41 import com.android.tradefed.invoker.proto.InvocationContext.Context;
42 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
43 import com.android.tradefed.log.ITestLogger;
44 import com.android.tradefed.log.LogUtil.CLog;
45 import com.android.tradefed.result.FileInputStreamSource;
46 import com.android.tradefed.result.ITestInvocationListener;
47 import com.android.tradefed.result.InputStreamSource;
48 import com.android.tradefed.result.LogDataType;
49 import com.android.tradefed.result.proto.StreamProtoReceiver;
50 import com.android.tradefed.result.proto.StreamProtoResultReporter;
51 import com.android.tradefed.sandbox.SandboxConfigDump.DumpCmd;
52 import com.android.tradefed.service.TradefedFeatureServer;
53 import com.android.tradefed.util.CommandResult;
54 import com.android.tradefed.util.CommandStatus;
55 import com.android.tradefed.util.FileUtil;
56 import com.android.tradefed.util.IRunUtil;
57 import com.android.tradefed.util.IRunUtil.EnvPriority;
58 import com.android.tradefed.util.PrettyPrintDelimiter;
59 import com.android.tradefed.util.QuotationAwareTokenizer;
60 import com.android.tradefed.util.RunUtil;
61 import com.android.tradefed.util.StreamUtil;
62 import com.android.tradefed.util.SubprocessExceptionParser;
63 import com.android.tradefed.util.SubprocessTestResultsParser;
64 import com.android.tradefed.util.SystemUtil;
65 import com.android.tradefed.util.keystore.IKeyStoreClient;
66 import com.android.tradefed.util.keystore.KeyStoreException;
67 
68 import com.google.common.base.Joiner;
69 
70 import java.io.File;
71 import java.io.FileOutputStream;
72 import java.io.IOException;
73 import java.io.PrintWriter;
74 import java.lang.reflect.InvocationTargetException;
75 import java.lang.reflect.Method;
76 import java.util.ArrayList;
77 import java.util.Arrays;
78 import java.util.HashSet;
79 import java.util.LinkedHashSet;
80 import java.util.List;
81 import java.util.Map;
82 import java.util.Map.Entry;
83 import java.util.Set;
84 
85 /**
86  * Sandbox container that can run a Trade Federation invocation. TODO: Allow Options to be passed to
87  * the sandbox.
88  */
89 public class TradefedSandbox implements ISandbox {
90 
91     private static final String SANDBOX_PREFIX = "sandbox-";
92     public static final String SANDBOX_ENABLED = "SANDBOX_ENABLED";
93 
94     private static final String SANDBOX_JVM_OPTIONS_ENV_VAR_KEY = "TF_SANDBOX_JVM_OPTIONS";
95 
96     private File mStdoutFile = null;
97     private File mStderrFile = null;
98     private File mHeapDump = null;
99 
100     private File mSandboxTmpFolder = null;
101     private File mRootFolder = null;
102     private File mGlobalConfig = null;
103     private File mSerializedContext = null;
104     private File mSerializedConfiguration = null;
105     private File mSerializedTestConfig = null;
106 
107     private SubprocessTestResultsParser mEventParser = null;
108     private StreamProtoReceiver mProtoReceiver = null;
109 
110     private IRunUtil mRunUtil;
111 
112     @VisibleForTesting
getJava()113     protected String getJava() {
114         return SystemUtil.getRunningJavaBinaryPath().getAbsolutePath();
115     }
116 
117     @Override
run(TestInformation info, IConfiguration config, ITestLogger logger)118     public CommandResult run(TestInformation info, IConfiguration config, ITestLogger logger)
119             throws Throwable {
120         List<String> mCmdArgs = new ArrayList<>();
121         mCmdArgs.add(getJava());
122         mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mSandboxTmpFolder.getAbsolutePath()));
123         mCmdArgs.add(String.format("-DTF_JAR_DIR=%s", mRootFolder.getAbsolutePath()));
124         // Setup heap dump collection
125         try {
126             mHeapDump = FileUtil.createTempDir("heap-dump", getWorkFolder());
127             mCmdArgs.add("-XX:+HeapDumpOnOutOfMemoryError");
128             mCmdArgs.add(String.format("-XX:HeapDumpPath=%s", mHeapDump.getAbsolutePath()));
129         } catch (IOException e) {
130             CLog.e(e);
131         }
132         SandboxOptions sandboxOptions = getSandboxOptions(config);
133         mCmdArgs.addAll(sandboxOptions.getJavaOptions());
134         if (System.getenv(SANDBOX_JVM_OPTIONS_ENV_VAR_KEY) != null) {
135             mCmdArgs.addAll(
136                     Arrays.asList(System.getenv(SANDBOX_JVM_OPTIONS_ENV_VAR_KEY).split(",")));
137         }
138         mCmdArgs.add("-cp");
139         mCmdArgs.add(createClasspath(mRootFolder));
140         mCmdArgs.add(TradefedSandboxRunner.class.getCanonicalName());
141         mCmdArgs.add(mSerializedContext.getAbsolutePath());
142         mCmdArgs.add(mSerializedConfiguration.getAbsolutePath());
143         if (mProtoReceiver != null) {
144             mCmdArgs.add("--" + StreamProtoResultReporter.PROTO_REPORT_PORT_OPTION);
145             mCmdArgs.add(Integer.toString(mProtoReceiver.getSocketServerPort()));
146         } else {
147             mCmdArgs.add("--subprocess-report-port");
148             mCmdArgs.add(Integer.toString(mEventParser.getSocketServerPort()));
149         }
150         if (config.getCommandOptions().shouldUseSandboxTestMode()) {
151             // In test mode, re-add the --use-sandbox to trigger a sandbox run again in the process
152             mCmdArgs.add("--" + CommandOptions.USE_SANDBOX);
153         }
154         if (sandboxOptions.startAvdInParent()) {
155             Set<String> notifyAsNative = new LinkedHashSet<String>();
156             for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
157                 if (deviceConfig.getDeviceRequirements().gceDeviceRequested()) {
158                     // Turn off the gce-device option and force the serial instead to use the
159                     // started virtual device.
160                     String deviceName =
161                             (config.getDeviceConfig().size() > 1)
162                                     ? String.format("{%s}", deviceConfig.getDeviceName())
163                                     : "";
164                     mCmdArgs.add(String.format("--%sno-gce-device", deviceName));
165                     mCmdArgs.add(String.format("--%sserial", deviceName));
166                     mCmdArgs.add(
167                             info.getContext()
168                                     .getDevice(deviceConfig.getDeviceName())
169                                     .getSerialNumber());
170                     // If we are using the device-type selector, override it
171                     if (DeviceSelectionOptions.DeviceRequestedType.GCE_DEVICE.equals(
172                             ((DeviceSelectionOptions) deviceConfig.getDeviceRequirements())
173                                     .getDeviceTypeRequested())) {
174                         mCmdArgs.add(String.format("--%sdevice-type", deviceName));
175                         mCmdArgs.add(
176                                 DeviceSelectionOptions.DeviceRequestedType.EXISTING_DEVICE.name());
177                     }
178                     if (BaseDeviceType.NATIVE_DEVICE.equals(
179                             deviceConfig.getDeviceRequirements().getBaseDeviceTypeRequested())) {
180                         notifyAsNative.add(
181                                 info.getContext()
182                                         .getDevice(deviceConfig.getDeviceName())
183                                         .getSerialNumber());
184                     }
185                 }
186             }
187             if (!notifyAsNative.isEmpty()) {
188                 mRunUtil.setEnvVariable(
189                         ManagedTestDeviceFactory.NOTIFY_AS_NATIVE,
190                         Joiner.on(",").join(notifyAsNative));
191             }
192         }
193 
194         // Remove a bit of timeout to account for parent overhead
195         long timeout = Math.max(config.getCommandOptions().getInvocationTimeout() - 120000L, 0);
196         // Allow interruption, subprocess should handle signals itself
197         mRunUtil.allowInterrupt(true);
198         CommandResult result = null;
199         RuntimeException interruptedException = null;
200         try {
201             result =
202                     mRunUtil.runTimedCmdWithInput(
203                             timeout, /*input*/
204                             null,
205                             mStdoutFile,
206                             mStderrFile,
207                             mCmdArgs.toArray(new String[0]));
208         } catch (RuntimeException interrupted) {
209             CLog.e("Sandbox runtimedCmd threw an exception");
210             CLog.e(interrupted);
211             interruptedException = interrupted;
212             result = new CommandResult(CommandStatus.EXCEPTION);
213             result.setStdout(StreamUtil.getStackTrace(interrupted));
214         }
215 
216         boolean failedStatus = false;
217         String stderrText;
218         try {
219             stderrText = FileUtil.readStringFromFile(mStderrFile);
220         } catch (IOException e) {
221             stderrText = "Could not read the stderr output from process.";
222         }
223         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
224             failedStatus = true;
225             result.setStderr(stderrText);
226         }
227 
228         try {
229             boolean joinResult = false;
230             long waitTime = getSandboxOptions(config).getWaitForEventsTimeout();
231             try (CloseableTraceScope ignored =
232                     new CloseableTraceScope(
233                             InvocationMetricKey.invocation_events_processing.toString())) {
234                 if (mProtoReceiver != null) {
235                     joinResult = mProtoReceiver.joinReceiver(waitTime);
236                 } else {
237                     joinResult = mEventParser.joinReceiver(waitTime);
238                 }
239             }
240             if (interruptedException != null) {
241                 throw interruptedException;
242             }
243             if (!joinResult) {
244                 if (!failedStatus) {
245                     result.setStatus(CommandStatus.EXCEPTION);
246                 }
247                 result.setStderr(
248                         String.format(
249                                 "%s:\n%s",
250                                 SubprocessExceptionParser.EVENT_THREAD_JOIN, stderrText));
251             }
252             PrettyPrintDelimiter.printStageDelimiter(
253                     String.format(
254                             "Execution of the tests occurred in the sandbox, you can find its logs "
255                                     + "under the name pattern '%s*'",
256                             SANDBOX_PREFIX));
257         } finally {
258             if (mProtoReceiver != null) {
259                 mProtoReceiver.completeModuleEvents();
260             }
261             try (InputStreamSource contextFile = new FileInputStreamSource(mSerializedContext)) {
262                 logger.testLog("sandbox-context", LogDataType.PB, contextFile);
263             }
264             // Log stdout and stderr
265             if (mStdoutFile != null) {
266                 try (InputStreamSource sourceStdOut = new FileInputStreamSource(mStdoutFile)) {
267                     logger.testLog("sandbox-stdout", LogDataType.HARNESS_STD_LOG, sourceStdOut);
268                 }
269             }
270             try (InputStreamSource sourceStdErr = new FileInputStreamSource(mStderrFile)) {
271                 logger.testLog("sandbox-stderr", LogDataType.HARNESS_STD_LOG, sourceStdErr);
272             }
273             // Collect heap dump if any
274             logAndCleanHeapDump(mHeapDump, logger);
275             mHeapDump = null;
276         }
277 
278         if (result.getExitCode() != null) {
279             // Log the exit code
280             InvocationMetricLogger.addInvocationMetrics(
281                     InvocationMetricKey.SANDBOX_EXIT_CODE, result.getExitCode());
282         }
283         if (mProtoReceiver != null && mProtoReceiver.hasInvocationFailed()) {
284             // If an invocation failed has already been reported, skip the logic below to report it
285             // again.
286             return result;
287         }
288         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
289             CLog.e(
290                     "Sandbox finished with status: %s and exit code: %s",
291                     result.getStatus(), result.getExitCode());
292             SubprocessExceptionParser.handleStderrException(result);
293         }
294         return result;
295     }
296 
297     @Override
prepareEnvironment( IInvocationContext context, IConfiguration config, ITestInvocationListener listener)298     public Exception prepareEnvironment(
299             IInvocationContext context, IConfiguration config, ITestInvocationListener listener) {
300         long startTime = System.currentTimeMillis();
301         try {
302             // Create our temp directories.
303             try {
304                 mStdoutFile =
305                         FileUtil.createTempFile("stdout_subprocess_", ".log", getWorkFolder());
306                 mStderrFile =
307                         FileUtil.createTempFile("stderr_subprocess_", ".log", getWorkFolder());
308                 mSandboxTmpFolder = FileUtil.createTempDir("tf-container", getWorkFolder());
309             } catch (IOException e) {
310                 return e;
311             }
312             // Unset the current global environment
313             mRunUtil = createRunUtil();
314             mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
315             mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
316             mRunUtil.unsetEnvVariable(AutomatedReporters.PROTO_REPORTING_PORT);
317             // Handle feature server
318             mRunUtil.unsetEnvVariable(RemoteInvocationExecution.START_FEATURE_SERVER);
319             mRunUtil.unsetEnvVariable(TradefedFeatureServer.TF_SERVICE_PORT);
320             mRunUtil.setEnvVariablePriority(EnvPriority.SET);
321             mRunUtil.setEnvVariable(
322                     TradefedFeatureServer.TF_SERVICE_PORT,
323                     Integer.toString(TradefedFeatureServer.getPort()));
324             // Mark subprocess for sandbox
325             mRunUtil.setEnvVariable(SANDBOX_ENABLED, "true");
326 
327             if (getSandboxOptions(config).shouldEnableDebugThread()) {
328                 mRunUtil.setEnvVariable(TradefedSandboxRunner.DEBUG_THREAD_KEY, "true");
329             }
330             for (Entry<String, String> envEntry :
331                     getSandboxOptions(config).getEnvVariables().entrySet()) {
332                 mRunUtil.setEnvVariable(envEntry.getKey(), envEntry.getValue());
333             }
334             if (config.getConfigurationDescription()
335                             .getMetaData(TradefedFeatureServer.SERVER_REFERENCE)
336                     != null) {
337                 mRunUtil.setEnvVariable(
338                         TradefedFeatureServer.SERVER_REFERENCE,
339                         config.getConfigurationDescription()
340                                 .getAllMetaData()
341                                 .getUniqueMap()
342                                 .get(TradefedFeatureServer.SERVER_REFERENCE));
343             }
344 
345             try {
346                 mRootFolder =
347                         getTradefedSandboxEnvironment(
348                                 context,
349                                 config,
350                                 listener,
351                                 QuotationAwareTokenizer.tokenizeLine(
352                                         config.getCommandLine(),
353                                         /** no logging */
354                                         false));
355             } catch (Exception e) {
356                 return e;
357             }
358 
359             PrettyPrintDelimiter.printStageDelimiter("Sandbox Configuration Preparation");
360             // Prepare the configuration
361             Exception res = prepareConfiguration(context, config, listener);
362             if (res != null) {
363                 return res;
364             }
365             // Prepare the context
366             try (CloseableTraceScope ignored = new CloseableTraceScope("prepareContext")) {
367                 mSerializedContext = prepareContext(context, config);
368             } catch (IOException e) {
369                 return e;
370             }
371         } finally {
372             if (!getSandboxOptions(config).shouldParallelSetup()) {
373                 InvocationMetricLogger.addInvocationPairMetrics(
374                         InvocationMetricKey.DYNAMIC_FILE_RESOLVER_PAIR,
375                         startTime,
376                         System.currentTimeMillis());
377             }
378         }
379         return null;
380     }
381 
382     @Override
tearDown()383     public void tearDown() {
384         StreamUtil.close(mEventParser);
385         StreamUtil.close(mProtoReceiver);
386         FileUtil.deleteFile(mStdoutFile);
387         FileUtil.deleteFile(mStderrFile);
388         FileUtil.recursiveDelete(mSandboxTmpFolder);
389         FileUtil.deleteFile(mSerializedContext);
390         FileUtil.deleteFile(mSerializedConfiguration);
391         FileUtil.deleteFile(mGlobalConfig);
392         FileUtil.deleteFile(mSerializedTestConfig);
393     }
394 
395     @Override
getTradefedSandboxEnvironment( IInvocationContext context, IConfiguration nonVersionedConfig, ITestLogger logger, String[] args)396     public File getTradefedSandboxEnvironment(
397             IInvocationContext context,
398             IConfiguration nonVersionedConfig,
399             ITestLogger logger,
400             String[] args)
401             throws Exception {
402         SandboxOptions options = getSandboxOptions(nonVersionedConfig);
403         // Check that we have no args conflicts.
404         if (options.getSandboxTfDirectory() != null && options.getSandboxBuildId() != null) {
405             throw new ConfigurationException(
406                     String.format(
407                             "Sandbox options %s and %s cannot be set at the same time",
408                             SandboxOptions.TF_LOCATION, SandboxOptions.SANDBOX_BUILD_ID));
409         }
410 
411         if (options.getSandboxTfDirectory() != null) {
412             return options.getSandboxTfDirectory();
413         }
414         String tfDir = System.getProperty("TF_JAR_DIR");
415         if (tfDir == null || tfDir.isEmpty()) {
416             throw new ConfigurationException(
417                     "Could not read TF_JAR_DIR to get current Tradefed instance.");
418         }
419         return new File(tfDir);
420     }
421 
422     /**
423      * Create a classpath based on the environment and the working directory returned by {@link
424      * #getTradefedSandboxEnvironment(IInvocationContext, IConfiguration, String[])}.
425      *
426      * @param workingDir the current working directory for the sandbox.
427      * @return The classpath to be use.
428      */
429     @Override
createClasspath(File workingDir)430     public String createClasspath(File workingDir) throws ConfigurationException {
431         // Get the classpath property.
432         String classpathStr = System.getProperty("java.class.path");
433         if (classpathStr == null) {
434             throw new ConfigurationException(
435                     "Could not find the classpath property: java.class.path");
436         }
437         return classpathStr;
438     }
439 
440     /**
441      * Prepare the {@link IConfiguration} that will be passed to the subprocess and will drive the
442      * container execution.
443      *
444      * @param context The current {@link IInvocationContext}.
445      * @param config the {@link IConfiguration} to be prepared.
446      * @param listener The current invocation {@link ITestInvocationListener}.
447      * @return an Exception if anything went wrong, null otherwise.
448      */
prepareConfiguration( IInvocationContext context, IConfiguration config, ITestInvocationListener listener)449     protected Exception prepareConfiguration(
450             IInvocationContext context, IConfiguration config, ITestInvocationListener listener) {
451         try {
452             String commandLine = config.getCommandLine();
453             if (getSandboxOptions(config).shouldUseProtoReporter()) {
454                 mProtoReceiver =
455                         new StreamProtoReceiver(listener, context, false, false, SANDBOX_PREFIX);
456                 // Force the child to the same mode as the parent.
457                 commandLine = commandLine + " --" + SandboxOptions.USE_PROTO_REPORTER;
458             } else {
459                 mEventParser = new SubprocessTestResultsParser(listener, true, context);
460                 commandLine = commandLine + " --no-" + SandboxOptions.USE_PROTO_REPORTER;
461             }
462             String[] args =
463                     QuotationAwareTokenizer.tokenizeLine(commandLine, /* No Logging */ false);
464 
465             mGlobalConfig = dumpGlobalConfig(config, new HashSet<>());
466             try (InputStreamSource source = new FileInputStreamSource(mGlobalConfig)) {
467                 listener.testLog("sandbox-global-config", LogDataType.HARNESS_CONFIG, source);
468             }
469             DumpCmd mode = DumpCmd.RUN_CONFIG;
470             if (config.getCommandOptions().shouldUseSandboxTestMode()) {
471                 mode = DumpCmd.TEST_MODE;
472             }
473             try (CloseableTraceScope ignored = new CloseableTraceScope("serialize_test_config")) {
474                 mSerializedConfiguration =
475                         SandboxConfigUtil.dumpConfigForVersion(
476                                 createClasspath(mRootFolder),
477                                 mRunUtil,
478                                 args,
479                                 mode,
480                                 mGlobalConfig,
481                                 false);
482             } catch (SandboxConfigurationException e) {
483                 // TODO: Improve our detection of that scenario
484                 CLog.e(e);
485                 CLog.e("%s", args[0]);
486                 if (e.getMessage().contains(String.format("Can not find local config %s", args[0]))
487                         || e.getMessage()
488                                 .contains(
489                                         String.format(
490                                                 "Could not find configuration '%s'", args[0]))) {
491                     CLog.w(
492                             "Child version doesn't contains '%s'. Attempting to backfill missing"
493                                     + " parent configuration.",
494                             args[0]);
495                     File parentConfig = handleChildMissingConfig(getSandboxOptions(config), args);
496                     if (parentConfig != null) {
497                         try (InputStreamSource source = new FileInputStreamSource(parentConfig)) {
498                             listener.testLog(
499                                     "sandbox-parent-config", LogDataType.HARNESS_CONFIG, source);
500                         }
501                         if (mSerializedTestConfig != null) {
502                             try (InputStreamSource source =
503                                     new FileInputStreamSource(mSerializedTestConfig)) {
504                                 listener.testLog(
505                                         "sandbox-test-config", LogDataType.HARNESS_CONFIG, source);
506                             }
507                         }
508                         try {
509                             mSerializedConfiguration =
510                                     SandboxConfigUtil.dumpConfigForVersion(
511                                             createClasspath(mRootFolder),
512                                             mRunUtil,
513                                             new String[] {parentConfig.getAbsolutePath()},
514                                             mode,
515                                             mGlobalConfig,
516                                             false);
517                         } finally {
518                             FileUtil.deleteFile(parentConfig);
519                         }
520                         return null;
521                     }
522                 }
523                 throw e;
524             } finally {
525                 // Turn off some of the invocation level options that would be duplicated in the
526                 // child sandbox subprocess.
527                 config.getCommandOptions().setBugreportOnInvocationEnded(false);
528                 config.getCommandOptions().setBugreportzOnInvocationEnded(false);
529             }
530         } catch (IOException | ConfigurationException e) {
531             StreamUtil.close(mEventParser);
532             StreamUtil.close(mProtoReceiver);
533             return e;
534         }
535         return null;
536     }
537 
538     @VisibleForTesting
createRunUtil()539     IRunUtil createRunUtil() {
540         return new RunUtil();
541     }
542 
543     /**
544      * Prepare and serialize the {@link IInvocationContext}.
545      *
546      * @param context the {@link IInvocationContext} to be prepared.
547      * @param config The {@link IConfiguration} of the sandbox.
548      * @return the serialized {@link IInvocationContext}.
549      * @throws IOException
550      */
prepareContext(IInvocationContext context, IConfiguration config)551     protected File prepareContext(IInvocationContext context, IConfiguration config)
552             throws IOException {
553         // In test mode we need to keep the context unlocked for the next layer.
554         if (config.getCommandOptions().shouldUseSandboxTestMode()) {
555             try {
556                 Method unlock = InvocationContext.class.getDeclaredMethod("unlock");
557                 unlock.setAccessible(true);
558                 unlock.invoke(context);
559                 unlock.setAccessible(false);
560             } catch (NoSuchMethodException
561                     | SecurityException
562                     | IllegalAccessException
563                     | IllegalArgumentException
564                     | InvocationTargetException e) {
565                 throw new IOException("Couldn't unlock the context.", e);
566             }
567         }
568         File protoFile =
569                 FileUtil.createTempFile(
570                         "context-proto", "." + LogDataType.PB.getFileExt(), mSandboxTmpFolder);
571         Context contextProto = context.toProto();
572         contextProto.writeDelimitedTo(new FileOutputStream(protoFile));
573         return protoFile;
574     }
575 
576     /** Dump the global configuration filtered from some objects. */
dumpGlobalConfig(IConfiguration config, Set<String> exclusionPatterns)577     protected File dumpGlobalConfig(IConfiguration config, Set<String> exclusionPatterns)
578             throws IOException, ConfigurationException {
579         SandboxOptions options = getSandboxOptions(config);
580         if (options.getChildGlobalConfig() != null) {
581             IConfigurationFactory factory = ConfigurationFactory.getInstance();
582             IGlobalConfiguration globalConfig =
583                     factory.createGlobalConfigurationFromArgs(
584                             new String[] {options.getChildGlobalConfig()}, new ArrayList<>());
585             CLog.d(
586                     "Using %s directly as global config without filtering",
587                     options.getChildGlobalConfig());
588             return globalConfig.cloneConfigWithFilter();
589         }
590         return SandboxConfigUtil.dumpFilteredGlobalConfig(exclusionPatterns);
591     }
592 
593     /** {@inheritDoc} */
594     @Override
createThinLauncherConfig( String[] args, IKeyStoreClient keyStoreClient, IRunUtil runUtil, File globalConfig)595     public IConfiguration createThinLauncherConfig(
596             String[] args, IKeyStoreClient keyStoreClient, IRunUtil runUtil, File globalConfig) {
597         // Default thin launcher cannot do anything, since this sandbox uses the same version as
598         // the parent version.
599         return null;
600     }
601 
getSandboxOptions(IConfiguration config)602     private SandboxOptions getSandboxOptions(IConfiguration config) {
603         return (SandboxOptions)
604                 config.getConfigurationObject(Configuration.SANBOX_OPTIONS_TYPE_NAME);
605     }
606 
handleChildMissingConfig(SandboxOptions options, String[] args)607     private File handleChildMissingConfig(SandboxOptions options, String[] args) {
608         IConfiguration parentConfig = null;
609         File tmpParentConfig = null;
610         PrintWriter pw = null;
611 
612         try {
613             if (options.dumpTestTemplate()) {
614                 args = extractTestTemplate(args);
615             }
616             tmpParentConfig = FileUtil.createTempFile("parent-config", ".xml", mSandboxTmpFolder);
617             pw = new PrintWriter(tmpParentConfig);
618             IKeyStoreClient keyStoreClient =
619                     GlobalConfiguration.getInstance().getKeyStoreFactory().createKeyStoreClient();
620             parentConfig =
621                     ConfigurationFactory.getInstance()
622                             .createConfigurationFromArgs(args, null, keyStoreClient);
623             // Do not print deprecated options to avoid compatibility issues, and do not print
624             // unchanged options.
625             parentConfig.dumpXml(pw, new ArrayList<>(), false, false);
626             return tmpParentConfig;
627         } catch (ConfigurationException | IOException | KeyStoreException e) {
628             CLog.e("Parent doesn't understand the command either:");
629             CLog.e(e);
630             FileUtil.deleteFile(tmpParentConfig);
631             return null;
632         } finally {
633             StreamUtil.close(pw);
634         }
635     }
636 
extractTestTemplate(String[] args)637     private String[] extractTestTemplate(String[] args) throws ConfigurationException, IOException {
638         ConfigurationXmlParserSettings parserSettings = new ConfigurationXmlParserSettings();
639         final ArgsOptionParser templateArgParser = new ArgsOptionParser(parserSettings);
640         List<String> listArgs = new ArrayList<>(Arrays.asList(args));
641         String configArg = listArgs.remove(0);
642         List<String> leftOverCommandLine = new ArrayList<>();
643         leftOverCommandLine.addAll(templateArgParser.parseBestEffort(listArgs, true));
644         Map<String, String> uniqueTemplates = parserSettings.templateMap.getUniqueMap();
645         CLog.d("Templates: %s", uniqueTemplates);
646         // We look at the "test" template since it's the usual main part of the versioned object
647         // configs. This will be improved in the future.
648         if (!uniqueTemplates.containsKey("test")) {
649             return args;
650         }
651         for (Entry<String, String> template : uniqueTemplates.entrySet()) {
652             if (!"test".equals(template.getKey())) {
653                 leftOverCommandLine.add("--template:map");
654                 leftOverCommandLine.add(
655                         String.format("%s=%s", template.getKey(), template.getValue()));
656             }
657         }
658         mSerializedTestConfig =
659                 SandboxConfigUtil.dumpConfigForVersion(
660                         createClasspath(mRootFolder),
661                         mRunUtil,
662                         new String[] {uniqueTemplates.get("test")},
663                         DumpCmd.STRICT_TEST,
664                         mGlobalConfig,
665                         false);
666         leftOverCommandLine.add("--template:map");
667         leftOverCommandLine.add("test=" + mSerializedTestConfig.getAbsolutePath());
668         leftOverCommandLine.add(0, configArg);
669         CLog.d("New Command line: %s", leftOverCommandLine);
670         return leftOverCommandLine.toArray(new String[0]);
671     }
672 
logAndCleanHeapDump(File heapDumpDir, ITestLogger logger)673     private void logAndCleanHeapDump(File heapDumpDir, ITestLogger logger) {
674         try {
675             if (heapDumpDir == null) {
676                 return;
677             }
678             if (!heapDumpDir.isDirectory()) {
679                 return;
680             }
681             if (heapDumpDir.listFiles().length == 0) {
682                 return;
683             }
684             for (File f : heapDumpDir.listFiles()) {
685                 FileInputStreamSource fileInput = new FileInputStreamSource(f);
686                 logger.testLog(f.getName(), LogDataType.HPROF, fileInput);
687                 StreamUtil.cancel(fileInput);
688             }
689         } finally {
690             FileUtil.recursiveDelete(heapDumpDir);
691         }
692     }
693 
getWorkFolder()694     private File getWorkFolder() {
695         return CurrentInvocation.getWorkFolder();
696     }
697 }
698