blob: e45b91eadace0568d363cefa06d37e5dd0a958c3 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.sandbox;
import com.android.annotations.VisibleForTesting;
import com.android.tradefed.command.CommandOptions;
import com.android.tradefed.config.Configuration;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationFactory;
import com.android.tradefed.config.IGlobalConfiguration;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.proto.InvocationContext.Context;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.proto.StreamProtoReceiver;
import com.android.tradefed.result.proto.StreamProtoResultReporter;
import com.android.tradefed.sandbox.SandboxConfigDump.DumpCmd;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.PrettyPrintDelimiter;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.SubprocessTestResultsParser;
import com.android.tradefed.util.SystemUtil;
import com.android.tradefed.util.keystore.IKeyStoreClient;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
/**
* Sandbox container that can run a Trade Federation invocation. TODO: Allow Options to be passed to
* the sandbox.
*/
public class TradefedSandbox implements ISandbox {
private static final String SANDBOX_PREFIX = "sandbox-";
private File mStdoutFile = null;
private File mStderrFile = null;
private OutputStream mStdout = null;
private FileOutputStream mStderr = null;
private File mHeapDump = null;
private File mSandboxTmpFolder = null;
private File mRootFolder = null;
private File mGlobalConfig = null;
private File mSerializedContext = null;
private File mSerializedConfiguration = null;
private SubprocessTestResultsParser mEventParser = null;
private StreamProtoReceiver mProtoReceiver = null;
private IRunUtil mRunUtil;
private boolean mCollectStdout = true;
@Override
public CommandResult run(IConfiguration config, ITestLogger logger) throws Throwable {
List<String> mCmdArgs = new ArrayList<>();
mCmdArgs.add(SystemUtil.getRunningJavaBinaryPath().getAbsolutePath());
mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mSandboxTmpFolder.getAbsolutePath()));
mCmdArgs.add(String.format("-DTF_JAR_DIR=%s", mRootFolder.getAbsolutePath()));
// Setup heap dump collection
try {
mHeapDump = FileUtil.createTempDir("heap-dump");
mCmdArgs.add("-XX:+HeapDumpOnOutOfMemoryError");
mCmdArgs.add(String.format("-XX:HeapDumpPath=%s", mHeapDump.getAbsolutePath()));
} catch (IOException e) {
CLog.e(e);
}
mCmdArgs.addAll(getSandboxOptions(config).getJavaOptions());
mCmdArgs.add("-cp");
mCmdArgs.add(createClasspath(mRootFolder));
mCmdArgs.add(TradefedSandboxRunner.class.getCanonicalName());
mCmdArgs.add(mSerializedContext.getAbsolutePath());
mCmdArgs.add(mSerializedConfiguration.getAbsolutePath());
if (mProtoReceiver != null) {
mCmdArgs.add("--" + StreamProtoResultReporter.PROTO_REPORT_PORT_OPTION);
mCmdArgs.add(Integer.toString(mProtoReceiver.getSocketServerPort()));
} else {
mCmdArgs.add("--subprocess-report-port");
mCmdArgs.add(Integer.toString(mEventParser.getSocketServerPort()));
}
if (config.getCommandOptions().shouldUseSandboxTestMode()) {
// In test mode, re-add the --use-sandbox to trigger a sandbox run again in the process
mCmdArgs.add("--" + CommandOptions.USE_SANDBOX);
}
long timeout = config.getCommandOptions().getInvocationTimeout();
// Allow interruption, subprocess should handle signals itself
mRunUtil.allowInterrupt(true);
CommandResult result =
mRunUtil.runTimedCmd(timeout, mStdout, mStderr, mCmdArgs.toArray(new String[0]));
// Log stdout and stderr
if (mStdoutFile != null) {
try (InputStreamSource sourceStdOut = new FileInputStreamSource(mStdoutFile)) {
logger.testLog("sandbox-stdout", LogDataType.TEXT, sourceStdOut);
}
}
try (InputStreamSource sourceStdErr = new FileInputStreamSource(mStderrFile)) {
logger.testLog("sandbox-stderr", LogDataType.TEXT, sourceStdErr);
}
// Collect heap dump if any
logAndCleanHeapDump(mHeapDump, logger);
boolean failedStatus = false;
String stderrText;
try {
stderrText = FileUtil.readStringFromFile(mStderrFile);
} catch (IOException e) {
stderrText = "Could not read the stderr output from process.";
}
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
failedStatus = true;
result.setStderr(stderrText);
}
// Log the configuration used to run
try (InputStreamSource configFile = new FileInputStreamSource(mSerializedConfiguration)) {
logger.testLog("sandbox-config", LogDataType.XML, configFile);
}
try (InputStreamSource contextFile = new FileInputStreamSource(mSerializedContext)) {
logger.testLog("sandbox-context", LogDataType.PB, contextFile);
}
boolean joinResult = false;
long waitTime = getSandboxOptions(config).getWaitForEventsTimeout();
if (mProtoReceiver != null) {
joinResult = mProtoReceiver.joinReceiver(waitTime);
} else {
joinResult = mEventParser.joinReceiver(waitTime);
}
if (!joinResult) {
if (!failedStatus) {
result.setStatus(CommandStatus.EXCEPTION);
}
result.setStderr(
String.format("Event receiver thread did not complete.:\n%s", stderrText));
}
if (mProtoReceiver != null) {
mProtoReceiver.completeModuleEvents();
}
PrettyPrintDelimiter.printStageDelimiter(
String.format(
"Execution of the tests occurred in the sandbox, you can find its logs "
+ "under the name pattern '%s*'",
SANDBOX_PREFIX));
return result;
}
@Override
public Exception prepareEnvironment(
IInvocationContext context, IConfiguration config, ITestInvocationListener listener) {
// Check for local sharding, avoid redirecting several stdout (from each shards) to the
// sandbox stdout as it creates a lot of I/O to the same output.
if (config.getCommandOptions().getShardCount() != null
&& config.getCommandOptions().getShardIndex() == null) {
mCollectStdout = false;
}
// Create our temp directories.
try {
if (mCollectStdout) {
mStdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log");
mStdout = new FileOutputStream(mStdoutFile);
} else {
mStdout =
new OutputStream() {
@Override
public void write(int b) throws IOException {
// Ignore stdout
}
};
}
mStderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log");
mStderr = new FileOutputStream(mStderrFile);
mSandboxTmpFolder = FileUtil.createTempDir("tradefed-container");
} catch (IOException e) {
return e;
}
// Unset the current global environment
mRunUtil = createRunUtil();
mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
// TODO: add handling of setting and creating the subprocess global configuration
if (getSandboxOptions(config).shouldEnableDebugThread()) {
mRunUtil.setEnvVariable(TradefedSandboxRunner.DEBUG_THREAD_KEY, "true");
}
for (Entry<String, String> envEntry :
getSandboxOptions(config).getEnvVariables().entrySet()) {
mRunUtil.setEnvVariable(envEntry.getKey(), envEntry.getValue());
}
try {
mRootFolder =
getTradefedSandboxEnvironment(
context,
config,
QuotationAwareTokenizer.tokenizeLine(
config.getCommandLine(),
/** no logging */
false));
} catch (ConfigurationException e) {
return e;
}
PrettyPrintDelimiter.printStageDelimiter("Sandbox Configuration Preparation");
// Prepare the configuration
Exception res = prepareConfiguration(context, config, listener);
if (res != null) {
return res;
}
// Prepare the context
try {
mSerializedContext = prepareContext(context, config);
} catch (IOException e) {
return e;
}
return null;
}
@Override
public void tearDown() {
StreamUtil.close(mEventParser);
StreamUtil.close(mProtoReceiver);
StreamUtil.close(mStdout);
StreamUtil.close(mStderr);
FileUtil.deleteFile(mStdoutFile);
FileUtil.deleteFile(mStderrFile);
FileUtil.recursiveDelete(mSandboxTmpFolder);
FileUtil.deleteFile(mSerializedContext);
FileUtil.deleteFile(mSerializedConfiguration);
FileUtil.deleteFile(mGlobalConfig);
}
@Override
public File getTradefedSandboxEnvironment(
IInvocationContext context, IConfiguration nonVersionedConfig, String[] args)
throws ConfigurationException {
SandboxOptions options = getSandboxOptions(nonVersionedConfig);
// Check that we have no args conflicts.
if (options.getSandboxTfDirectory() != null && options.getSandboxBuildId() != null) {
throw new ConfigurationException(
String.format(
"Sandbox options %s and %s cannot be set at the same time",
SandboxOptions.TF_LOCATION, SandboxOptions.SANDBOX_BUILD_ID));
}
if (options.getSandboxTfDirectory() != null) {
return options.getSandboxTfDirectory();
}
String tfDir = System.getProperty("TF_JAR_DIR");
if (tfDir == null || tfDir.isEmpty()) {
throw new ConfigurationException(
"Could not read TF_JAR_DIR to get current Tradefed instance.");
}
return new File(tfDir);
}
/**
* Create a classpath based on the environment and the working directory returned by {@link
* #getTradefedSandboxEnvironment(IInvocationContext, IConfiguration, String[])}.
*
* @param workingDir the current working directory for the sandbox.
* @return The classpath to be use.
*/
@Override
public String createClasspath(File workingDir) throws ConfigurationException {
// Get the classpath property.
String classpathStr = System.getProperty("java.class.path");
if (classpathStr == null) {
throw new ConfigurationException(
"Could not find the classpath property: java.class.path");
}
return classpathStr;
}
/**
* Prepare the {@link IConfiguration} that will be passed to the subprocess and will drive the
* container execution.
*
* @param context The current {@link IInvocationContext}.
* @param config the {@link IConfiguration} to be prepared.
* @param listener The current invocation {@link ITestInvocationListener}.
* @return an Exception if anything went wrong, null otherwise.
*/
protected Exception prepareConfiguration(
IInvocationContext context, IConfiguration config, ITestInvocationListener listener) {
try {
// TODO: switch reporting of parent and subprocess to proto
String commandLine = config.getCommandLine();
if (getSandboxOptions(config).shouldUseProtoReporter()) {
mProtoReceiver =
new StreamProtoReceiver(listener, context, false, false, SANDBOX_PREFIX);
// Force the child to the same mode as the parent.
commandLine = commandLine + " --" + SandboxOptions.USE_PROTO_REPORTER;
} else {
mEventParser = new SubprocessTestResultsParser(listener, true, context);
commandLine = commandLine + " --no-" + SandboxOptions.USE_PROTO_REPORTER;
}
String[] args =
QuotationAwareTokenizer.tokenizeLine(commandLine, /* No Logging */ false);
mGlobalConfig = dumpGlobalConfig(config, new HashSet<>());
try (InputStreamSource source = new FileInputStreamSource(mGlobalConfig)) {
listener.testLog("sandbox-global-config", LogDataType.XML, source);
}
DumpCmd mode = DumpCmd.RUN_CONFIG;
if (config.getCommandOptions().shouldUseSandboxTestMode()) {
mode = DumpCmd.TEST_MODE;
}
try {
mSerializedConfiguration =
SandboxConfigUtil.dumpConfigForVersion(
createClasspath(mRootFolder), mRunUtil, args, mode, mGlobalConfig);
} catch (SandboxConfigurationException e) {
// TODO: Improve our detection of that scenario
CLog.e(e);
CLog.e("%s", args[0]);
if (e.getMessage().contains(String.format("Can not find local config %s", args[0]))
|| e.getMessage()
.contains(
String.format(
"Could not find configuration '%s'", args[0]))) {
CLog.w(
"Child version doesn't contains '%s'. Attempting to backfill missing parent configuration.",
args[0]);
File parentConfig = handleChildMissingConfig(args);
if (parentConfig != null) {
try (InputStreamSource source = new FileInputStreamSource(parentConfig)) {
listener.testLog("sandbox-parent-config", LogDataType.XML, source);
}
try {
mSerializedConfiguration =
SandboxConfigUtil.dumpConfigForVersion(
createClasspath(mRootFolder),
mRunUtil,
new String[] {parentConfig.getAbsolutePath()},
mode,
mGlobalConfig);
} finally {
FileUtil.deleteFile(parentConfig);
}
return null;
}
}
throw e;
}
// Turn off some of the invocation level options that would be duplicated in the
// child sandbox subprocess.
config.getCommandOptions().setBugreportOnInvocationEnded(false);
config.getCommandOptions().setBugreportzOnInvocationEnded(false);
} catch (IOException | ConfigurationException e) {
StreamUtil.close(mEventParser);
StreamUtil.close(mProtoReceiver);
return e;
}
return null;
}
@VisibleForTesting
IRunUtil createRunUtil() {
return new RunUtil();
}
/**
* Prepare and serialize the {@link IInvocationContext}.
*
* @param context the {@link IInvocationContext} to be prepared.
* @param config The {@link IConfiguration} of the sandbox.
* @return the serialized {@link IInvocationContext}.
* @throws IOException
*/
protected File prepareContext(IInvocationContext context, IConfiguration config)
throws IOException {
// In test mode we need to keep the context unlocked for the next layer.
if (config.getCommandOptions().shouldUseSandboxTestMode()) {
try {
Method unlock = InvocationContext.class.getDeclaredMethod("unlock");
unlock.setAccessible(true);
unlock.invoke(context);
unlock.setAccessible(false);
} catch (NoSuchMethodException
| SecurityException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException e) {
throw new IOException("Couldn't unlock the context.", e);
}
}
File protoFile =
FileUtil.createTempFile(
"context-proto", "." + LogDataType.PB.getFileExt(), mSandboxTmpFolder);
Context contextProto = context.toProto();
contextProto.writeDelimitedTo(new FileOutputStream(protoFile));
return protoFile;
}
/** Dump the global configuration filtered from some objects. */
protected File dumpGlobalConfig(IConfiguration config, Set<String> exclusionPatterns)
throws IOException, ConfigurationException {
SandboxOptions options = getSandboxOptions(config);
if (options.getChildGlobalConfig() != null) {
IConfigurationFactory factory = ConfigurationFactory.getInstance();
IGlobalConfiguration globalConfig =
factory.createGlobalConfigurationFromArgs(
new String[] {options.getChildGlobalConfig()}, new ArrayList<>());
CLog.d(
"Using %s directly as global config without filtering",
options.getChildGlobalConfig());
return globalConfig.cloneConfigWithFilter();
}
return SandboxConfigUtil.dumpFilteredGlobalConfig(exclusionPatterns);
}
/** {@inheritDoc} */
@Override
public IConfiguration createThinLauncherConfig(
String[] args, IKeyStoreClient keyStoreClient, IRunUtil runUtil, File globalConfig) {
// Default thin launcher cannot do anything, since this sandbox uses the same version as
// the parent version.
return null;
}
private SandboxOptions getSandboxOptions(IConfiguration config) {
return (SandboxOptions)
config.getConfigurationObject(Configuration.SANBOX_OPTIONS_TYPE_NAME);
}
private File handleChildMissingConfig(String[] args) {
IConfiguration parentConfig = null;
File tmpParentConfig = null;
PrintWriter pw = null;
try {
tmpParentConfig = FileUtil.createTempFile("parent-config", ".xml", mSandboxTmpFolder);
pw = new PrintWriter(tmpParentConfig);
parentConfig = ConfigurationFactory.getInstance().createConfigurationFromArgs(args);
// Do not print deprecated options to avoid compatibility issues, and do not print
// unchanged options.
parentConfig.dumpXml(pw, new ArrayList<>(), false, false);
return tmpParentConfig;
} catch (ConfigurationException | IOException e) {
CLog.e("Parent doesn't understand the command either:");
CLog.e(e);
FileUtil.deleteFile(tmpParentConfig);
return null;
} finally {
StreamUtil.close(pw);
}
}
private void logAndCleanHeapDump(File heapDumpDir, ITestLogger logger) {
try {
if (heapDumpDir != null && heapDumpDir.listFiles().length != 0) {
for (File f : heapDumpDir.listFiles()) {
FileInputStreamSource fileInput = new FileInputStreamSource(f);
logger.testLog(f.getName(), LogDataType.HPROF, fileInput);
StreamUtil.cancel(fileInput);
}
}
} finally {
FileUtil.recursiveDelete(heapDumpDir);
}
}
}