1 /*
2  * Copyright (C) 2011 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 
17 package com.android.tradefed.command;
18 
19 import com.android.ddmlib.Log.LogLevel;
20 import com.android.tradefed.clearcut.ClearcutClient;
21 import com.android.tradefed.clearcut.TerminateClearcutClient;
22 import com.android.tradefed.config.ConfigurationException;
23 import com.android.tradefed.config.GlobalConfiguration;
24 import com.android.tradefed.device.NoDeviceException;
25 import com.android.tradefed.invoker.tracing.ActiveTrace;
26 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
27 import com.android.tradefed.invoker.tracing.TracingLogger;
28 import com.android.tradefed.log.LogUtil.CLog;
29 import com.android.tradefed.result.error.InfraErrorIdentifier;
30 import com.android.tradefed.service.TradefedFeatureServer;
31 import com.android.tradefed.testtype.suite.TestSuiteInfo;
32 import com.android.tradefed.util.FileUtil;
33 import com.android.tradefed.util.SerializationUtil;
34 import com.android.tradefed.util.SystemUtil;
35 
36 import com.google.common.annotations.VisibleForTesting;
37 
38 import org.json.JSONException;
39 import org.json.JSONObject;
40 
41 import java.io.File;
42 import java.io.IOException;
43 import java.util.concurrent.CompletableFuture;
44 import java.util.concurrent.ExecutionException;
45 
46 import sun.misc.Signal;
47 import sun.misc.SignalHandler;
48 
49 /**
50  * An alternate TradeFederation entry point that will run command specified in command
51  * line arguments and then quit.
52  * <p/>
53  * Intended for use with a debugger and other non-interactive modes of operation.
54  * <p/>
55  * Expected arguments: [commands options] (config to run)
56  */
57 public class CommandRunner {
58     private ICommandScheduler mScheduler;
59     private ExitCode mErrorCode = ExitCode.NO_ERROR;
60 
61     public static final String EXCEPTION_KEY = "serialized_exception";
62     public static final String START_FEATURE_SERVER = "START_FEATURE_SERVER";
63     private static final long CHECK_DEVICE_TIMEOUT = 60000;
64 
CommandRunner()65     public CommandRunner() {}
66 
getErrorCode()67     public ExitCode getErrorCode() {
68         return mErrorCode;
69     }
70 
71     /**
72      * Initialize the required global configuration.
73      */
74     @VisibleForTesting
initGlobalConfig(String[] args)75     void initGlobalConfig(String[] args) throws ConfigurationException {
76         GlobalConfiguration.createGlobalConfiguration(args);
77         GlobalConfiguration.getInstance().setup();
78     }
79 
80     /** Get the {@link ICommandScheduler} instance from the global configuration. */
81     @VisibleForTesting
getCommandScheduler()82     ICommandScheduler getCommandScheduler() {
83         return GlobalConfiguration.getInstance().getCommandScheduler();
84     }
85 
86     /** Prints the exception stack to stderr. */
87     @VisibleForTesting
printStackTrace(Throwable e)88     void printStackTrace(Throwable e) {
89         e.printStackTrace();
90         File serializedException = null;
91         try {
92             serializedException = SerializationUtil.serialize(e);
93             JSONObject json = new JSONObject();
94             json.put(EXCEPTION_KEY, serializedException.getAbsolutePath());
95             System.err.println(json.toString());
96             System.err.flush();
97         } catch (IOException | JSONException io) {
98             io.printStackTrace();
99             FileUtil.deleteFile(serializedException);
100         }
101     }
102 
103     /** Returns the timeout after which to check for the command. */
104     @VisibleForTesting
getCheckDeviceTimeout()105     long getCheckDeviceTimeout() {
106         return CHECK_DEVICE_TIMEOUT;
107     }
108 
109     /**
110      * The main method to run the command.
111      *
112      * @param args the config name to run and its options
113      */
run(String[] args)114     public void run(String[] args) {
115         try {
116             CompletableFuture<ClearcutClient> futureClient =
117                     CompletableFuture.supplyAsync(() -> createClient());
118             try (CloseableTraceScope ignored = new CloseableTraceScope("initGlobalConfig")) {
119                 initGlobalConfig(args);
120             }
121 
122             ClearcutClient client = futureClient.get();
123             Runtime.getRuntime().addShutdownHook(new TerminateClearcutClient(client));
124             client.notifyTradefedStartEvent();
125             if (System.getenv(START_FEATURE_SERVER) != null) {
126                 // Starting the server takes 100ms so do it in parallel
127                 CompletableFuture.supplyAsync(() -> startFeatureSever());
128             }
129 
130             mScheduler = getCommandScheduler();
131             mScheduler.setClearcutClient(client);
132             mScheduler.start();
133             SignalHandler handler =
134                     new SignalHandler() {
135                         @Override
136                         public void handle(Signal sig) {
137                             CLog.logAndDisplay(
138                                     LogLevel.INFO,
139                                     String.format(
140                                             "Received signal %s. Shutting down.", sig.getName()));
141                             mScheduler.shutdownHard(false);
142                         }
143                     };
144             Signal.handle(new Signal("TERM"), handler);
145 
146             mScheduler.addCommand(args);
147         } catch (ConfigurationException
148                 | RuntimeException
149                 | InterruptedException
150                 | ExecutionException e) {
151             printStackTrace(e);
152             mErrorCode = ExitCode.CONFIG_EXCEPTION;
153             return;
154         } finally {
155             if (mScheduler != null) {
156                 mScheduler.shutdownOnEmpty();
157             }
158         }
159         try {
160             mScheduler.join(getCheckDeviceTimeout());
161             // FIXME: if possible make the no_device allocated check deterministic.
162             // After 1 min we check if the command was executed.
163             if (mScheduler.getReadyCommandCount() > 0
164                     && mScheduler.getExecutingCommandCount() == 0) {
165                 printStackTrace(
166                         new NoDeviceException(
167                                 "No device was allocated for the command.",
168                                 InfraErrorIdentifier.RUNNER_ALLOCATION_ERROR));
169                 mErrorCode = ExitCode.NO_DEVICE_ALLOCATED;
170                 mScheduler.removeAllCommands();
171                 mScheduler.shutdown();
172                 return;
173             }
174             mScheduler.join();
175             // If no error code has been raised yet, we checked the invocation error code.
176             if (ExitCode.NO_ERROR.equals(mErrorCode)) {
177                 mErrorCode = mScheduler.getLastInvocationExitCode();
178             }
179         } catch (InterruptedException e) {
180             e.printStackTrace();
181             mErrorCode = ExitCode.THROWABLE_EXCEPTION;
182         } finally {
183             GlobalConfiguration.getInstance().cleanup();
184         }
185         if (!ExitCode.NO_ERROR.equals(mErrorCode)
186                 && mScheduler.getLastInvocationThrowable() != null) {
187             // Print error to the stderr so that it can be recovered.
188             printStackTrace(mScheduler.getLastInvocationThrowable());
189         }
190     }
191 
createClient()192     protected ClearcutClient createClient() {
193         try (CloseableTraceScope ignored = new CloseableTraceScope("createClient")) {
194             return new ClearcutClient(
195                     TestSuiteInfo.getInstance().didLoadFromProperties()
196                             ? TestSuiteInfo.getInstance().getName()
197                             : "");
198         }
199     }
200 
main(final String[] mainArgs)201     public static void main(final String[] mainArgs) {
202         long pid = ProcessHandle.current().pid();
203         long tid = Thread.currentThread().getId();
204         ActiveTrace trace = null;
205         // Enable Full Tradefed tracing in local mode only for now
206         if (SystemUtil.isLocalMode()) {
207             trace = TracingLogger.createActiveTrace(pid, tid, true);
208             trace.startTracing(false);
209         }
210         CommandRunner console = new CommandRunner();
211         try (CloseableTraceScope ignored = new CloseableTraceScope("end_to_end_command")) {
212             console.run(mainArgs);
213         } finally {
214             if (trace != null) {
215                 trace.finalizeTracing();
216             }
217         }
218         System.exit(console.getErrorCode().getCodeValue());
219     }
220 
221     /**
222      * Error codes that are possible to exit with.
223      */
224     public static enum ExitCode {
225         NO_ERROR(0),
226         CONFIG_EXCEPTION(1),
227         NO_BUILD(2),
228         DEVICE_UNRESPONSIVE(3),
229         DEVICE_UNAVAILABLE(4),
230         FATAL_HOST_ERROR(5),
231         THROWABLE_EXCEPTION(6),
232         NO_DEVICE_ALLOCATED(7),
233         WRONG_JAVA_VERSION(8);
234 
235         private final int mCodeValue;
236 
ExitCode(int codeValue)237         ExitCode(int codeValue) {
238             mCodeValue = codeValue;
239         }
240 
getCodeValue()241         public int getCodeValue() {
242             return mCodeValue;
243         }
244     }
245 
startFeatureSever()246     private boolean startFeatureSever() {
247         try (CloseableTraceScope f = new CloseableTraceScope("start_fr_client")) {
248             TradefedFeatureServer server = new TradefedFeatureServer();
249             server.start();
250             GlobalConfiguration.getInstance().setTradefedFeatureServer(server);
251         } catch (RuntimeException e) {
252             System.out.println(String.format("Error starting feature server: %s", e));
253         }
254         return true;
255     }
256 }
257