| /* |
| * Copyright (C) 2020 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.invoker; |
| |
| import com.android.tradefed.command.CommandRunner; |
| import com.android.tradefed.config.ConfigurationException; |
| import com.android.tradefed.config.GlobalConfiguration; |
| import com.android.tradefed.config.IConfiguration; |
| import com.android.tradefed.config.proxy.AutomatedReporters; |
| import com.android.tradefed.config.proxy.TradefedDelegator; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.error.HarnessRuntimeException; |
| import com.android.tradefed.invoker.TestInvocation.Stage; |
| import com.android.tradefed.invoker.logger.CurrentInvocation; |
| 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.LogDataType; |
| import com.android.tradefed.result.error.InfraErrorIdentifier; |
| import com.android.tradefed.result.proto.StreamProtoReceiver; |
| import com.android.tradefed.service.TradefedFeatureServer; |
| import com.android.tradefed.targetprep.BuildError; |
| import com.android.tradefed.targetprep.TargetSetupError; |
| 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.IRunUtil.EnvPriority; |
| import com.android.tradefed.util.RunUtil; |
| import com.android.tradefed.util.StreamUtil; |
| import com.android.tradefed.util.SubprocessExceptionParser; |
| import com.android.tradefed.util.SystemUtil; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.net.ServerSocket; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** {@link InvocationExecution} which delegate the execution to another Tradefed binary. */ |
| public class DelegatedInvocationExecution extends InvocationExecution { |
| |
| /** If present the invocation is executing within a delegated mode */ |
| public static final String DELEGATED_MODE_VAR = "DELEGATED_MODE"; |
| |
| /** Timeout to wait for the events received from subprocess to finish being processed. */ |
| private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000; |
| |
| private File mTmpDelegatedDir = null; |
| private File mGlobalConfig = null; |
| // Output reporting |
| private File mStdoutFile = null; |
| private File mStderrFile = null; |
| private OutputStream mStderr = null; |
| private OutputStream mStdout = null; |
| |
| @Override |
| public void reportLogs(ITestDevice device, ITestLogger logger, Stage stage) { |
| // Do nothing |
| } |
| |
| @Override |
| public boolean shardConfig( |
| IConfiguration config, |
| TestInformation testInfo, |
| IRescheduler rescheduler, |
| ITestLogger logger) { |
| return false; |
| } |
| |
| @Override |
| public void doSetup(TestInformation testInfo, IConfiguration config, ITestLogger listener) |
| throws TargetSetupError, BuildError, DeviceNotAvailableException { |
| // Do nothing |
| } |
| |
| @Override |
| public void runDevicePreInvocationSetup( |
| IInvocationContext context, IConfiguration config, ITestLogger logger) |
| throws DeviceNotAvailableException, TargetSetupError { |
| // Do nothing |
| } |
| |
| @Override |
| public void runDevicePostInvocationTearDown( |
| IInvocationContext context, IConfiguration config, Throwable exception) { |
| // Do nothing |
| } |
| |
| @Override |
| public void doTeardown( |
| TestInformation testInfo, |
| IConfiguration config, |
| ITestLogger logger, |
| Throwable exception) |
| throws Throwable { |
| // Do nothing |
| } |
| |
| @Override |
| public void runTests( |
| TestInformation info, IConfiguration config, ITestInvocationListener listener) |
| throws Throwable { |
| // Dump the delegated config for debugging |
| File dumpConfig = FileUtil.createTempFile("delegated-config", ".xml"); |
| try (PrintWriter pw = new PrintWriter(dumpConfig)) { |
| config.dumpXml(pw); |
| } |
| logAndCleanFile(dumpConfig, LogDataType.HARNESS_CONFIG, listener); |
| |
| if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) == null) { |
| throw new ConfigurationException( |
| "Delegate object should not be null in DelegatedInvocation"); |
| } |
| TradefedDelegator delegator = |
| (TradefedDelegator) |
| config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT); |
| if (!delegator.getTfRootDir().exists() || !delegator.getTfRootDir().isDirectory()) { |
| throw new ConfigurationException( |
| String.format( |
| "delegated-tf was misconfigured and doesn't point to a valid" |
| + " location: %s", |
| delegator.getTfRootDir()), |
| InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); |
| } |
| List<String> commandLine = new ArrayList<>(); |
| commandLine.add(SystemUtil.getRunningJavaBinaryPath().getAbsolutePath()); |
| mTmpDelegatedDir = |
| FileUtil.createTempDir("delegated-invocation", CurrentInvocation.getWorkFolder()); |
| commandLine.add(String.format("-Djava.io.tmpdir=%s", mTmpDelegatedDir.getAbsolutePath())); |
| commandLine.add("-cp"); |
| // Add classpath |
| commandLine.add(delegator.createClasspath()); |
| // Carry the updated TF_JAR_DIR to delegate, this will simulate tradefed.sh environment. |
| commandLine.add( |
| String.format("-DTF_JAR_DIR=%s", delegator.getTfRootDir().getAbsolutePath())); |
| commandLine.add("com.android.tradefed.command.CommandRunner"); |
| // Add command line |
| commandLine.addAll(Arrays.asList(delegator.getCommandLine())); |
| |
| try (StreamProtoReceiver receiver = createReceiver(listener, info.getContext())) { |
| mStdoutFile = FileUtil.createTempFile("stdout_delegate_", ".log", mTmpDelegatedDir); |
| mStderrFile = FileUtil.createTempFile("stderr_delegate_", ".log", mTmpDelegatedDir); |
| mStderr = new FileOutputStream(mStderrFile); |
| mStdout = new FileOutputStream(mStdoutFile); |
| IRunUtil runUtil = createRunUtil(receiver.getSocketServerPort(), config); |
| CommandResult result = null; |
| RuntimeException runtimeException = null; |
| CLog.d("Command line: %s", commandLine); |
| try { |
| result = |
| runUtil.runTimedCmd( |
| config.getCommandOptions().getInvocationTimeout(), |
| mStdout, |
| mStderr, |
| commandLine.toArray(new String[0])); |
| } catch (RuntimeException e) { |
| CLog.e("Delegated runtimedCmd threw an exception"); |
| CLog.e(e); |
| runtimeException = e; |
| result = new CommandResult(CommandStatus.EXCEPTION); |
| result.setStdout(StreamUtil.getStackTrace(e)); |
| } |
| 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); |
| } |
| boolean joinResult = receiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS); |
| if (runtimeException != null) { |
| throw runtimeException; |
| } |
| if (!joinResult) { |
| if (!failedStatus) { |
| result.setStatus(CommandStatus.EXCEPTION); |
| } |
| result.setStderr( |
| String.format("Event receiver thread did not complete.:\n%s", stderrText)); |
| } |
| receiver.completeModuleEvents(); |
| if (result.getStatus().equals(CommandStatus.TIMED_OUT)) { |
| throw new HarnessRuntimeException( |
| "Delegated invocation timed out.", InfraErrorIdentifier.INVOCATION_TIMEOUT); |
| } |
| if (!CommandStatus.SUCCESS.equals(result.getStatus())) { |
| CLog.e( |
| "Sandbox finished with status: %s and exit code: %s", |
| result.getStatus(), result.getExitCode()); |
| SubprocessExceptionParser.handleStderrException(result); |
| } |
| } finally { |
| StreamUtil.close(mStderr); |
| StreamUtil.close(mStdout); |
| logAndCleanFile(mStdoutFile, LogDataType.HARNESS_STD_LOG, listener); |
| logAndCleanFile(mStderrFile, LogDataType.HARNESS_STD_LOG, listener); |
| logAndCleanFile(mGlobalConfig, LogDataType.HARNESS_CONFIG, listener); |
| } |
| } |
| |
| @Override |
| public void doCleanUp(IInvocationContext context, IConfiguration config, Throwable exception) { |
| super.doCleanUp(context, config, exception); |
| FileUtil.recursiveDelete(mTmpDelegatedDir); |
| FileUtil.deleteFile(mGlobalConfig); |
| } |
| |
| private IRunUtil createRunUtil(int port, IConfiguration config) throws IOException { |
| IRunUtil runUtil = new RunUtil(); |
| // Handle the global configs for the subprocess |
| runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE); |
| runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE); |
| runUtil.setEnvVariablePriority(EnvPriority.SET); |
| mGlobalConfig = createGlobalConfig(); |
| runUtil.setEnvVariable( |
| GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig.getAbsolutePath()); |
| runUtil.setEnvVariable(AutomatedReporters.PROTO_REPORTING_PORT, Integer.toString(port)); |
| // Set a variable to detect delegated mode |
| runUtil.setEnvVariable(DELEGATED_MODE_VAR, "1"); |
| // Trigger the feature server to be restarted in the delegate |
| // this ensures all the code is being delegated. |
| runUtil.setEnvVariable(CommandRunner.START_FEATURE_SERVER, "1"); |
| ServerSocket s = new ServerSocket(0); |
| s.setReuseAddress(true); |
| int servicePort = s.getLocalPort(); |
| s.close(); |
| runUtil.setEnvVariable( |
| TradefedFeatureServer.TF_SERVICE_PORT, Integer.toString(servicePort)); |
| return runUtil; |
| } |
| |
| private StreamProtoReceiver createReceiver( |
| ITestInvocationListener listener, IInvocationContext mainContext) throws IOException { |
| StreamProtoReceiver receiver = |
| new StreamProtoReceiver( |
| listener, mainContext, false, false, /* report logs */ false, ""); |
| return receiver; |
| } |
| |
| private File createGlobalConfig() throws IOException { |
| String[] configList = |
| new String[] { |
| GlobalConfiguration.DEVICE_MANAGER_TYPE_NAME, |
| GlobalConfiguration.KEY_STORE_TYPE_NAME, |
| GlobalConfiguration.HOST_OPTIONS_TYPE_NAME, |
| GlobalConfiguration.SANDBOX_FACTORY_TYPE_NAME, |
| "android-build" |
| }; |
| File filteredGlobalConfig = |
| GlobalConfiguration.getInstance().cloneConfigWithFilter(configList); |
| return filteredGlobalConfig; |
| } |
| |
| /** |
| * Log the content of given file to listener, then remove the file. |
| * |
| * @param fileToExport the {@link File} pointing to the file to log. |
| * @param type the {@link LogDataType} of the data |
| * @param listener the {@link ITestInvocationListener} where to report the test. |
| */ |
| private void logAndCleanFile( |
| File fileToExport, LogDataType type, ITestInvocationListener listener) { |
| if (fileToExport == null) { return; } |
| |
| try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport, true)) { |
| listener.testLog(fileToExport.getName(), type, inputStream); |
| } |
| } |
| } |