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