1 /*
2  * Copyright (C) 2007 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.commands.am;
18 
19 import static android.app.ActivityManager.INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS;
20 import static android.app.ActivityManager.INSTR_FLAG_DISABLE_TEST_API_CHECKS;
21 import static android.app.ActivityManager.INSTR_FLAG_MOUNT_EXTERNAL_STORAGE_FULL;
22 
23 import android.app.IActivityManager;
24 import android.app.IInstrumentationWatcher;
25 import android.app.Instrumentation;
26 import android.app.UiAutomationConnection;
27 import android.content.ComponentName;
28 import android.content.pm.IPackageManager;
29 import android.content.pm.InstrumentationInfo;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.Environment;
33 import android.os.ServiceManager;
34 import android.os.UserHandle;
35 import android.util.AndroidException;
36 import android.util.proto.ProtoOutputStream;
37 import android.view.IWindowManager;
38 
39 import java.io.File;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStreamReader;
43 import java.io.OutputStream;
44 import java.text.SimpleDateFormat;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.Date;
49 import java.util.List;
50 import java.util.Locale;
51 
52 
53 /**
54  * Runs the am instrument command
55  *
56  * Test Result Code:
57  * 1 - Test running
58  * 0 - Test passed
59  * -2 - assertion failure
60  * -1 - other exceptions
61  *
62  * Session Result Code:
63  * -1: Success
64  * other: Failure
65  */
66 public class Instrument {
67     private static final String TAG = "am";
68 
69     public static final String DEFAULT_LOG_DIR = "instrument-logs";
70 
71     private static final int STATUS_TEST_PASSED = 0;
72     private static final int STATUS_TEST_STARTED = 1;
73     private static final int STATUS_TEST_FAILED_ASSERTION = -1;
74     private static final int STATUS_TEST_FAILED_OTHER = -2;
75 
76     private final IActivityManager mAm;
77     private final IPackageManager mPm;
78     private final IWindowManager mWm;
79 
80     // Command line arguments
81     public String profileFile = null;
82     public boolean wait = false;
83     public boolean rawMode = false;
84     boolean protoStd = false;  // write proto to stdout
85     boolean protoFile = false;  // write proto to a file
86     String logPath = null;
87     public boolean noWindowAnimation = false;
88     public boolean disableHiddenApiChecks = false;
89     public boolean disableTestApiChecks = true;
90     public boolean disableIsolatedStorage = false;
91     public String abi = null;
92     public int userId = UserHandle.USER_CURRENT;
93     public Bundle args = new Bundle();
94     // Required
95     public String componentNameArg;
96 
97     /**
98      * Construct the instrument command runner.
99      */
Instrument(IActivityManager am, IPackageManager pm)100     public Instrument(IActivityManager am, IPackageManager pm) {
101         mAm = am;
102         mPm = pm;
103         mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
104     }
105 
106     /**
107      * Base class for status reporting.
108      *
109      * All the methods on this interface are called within the synchronized block
110      * of the InstrumentationWatcher, so calls are in order.  However, that means
111      * you must be careful not to do blocking operations because you don't know
112      * exactly the locking dependencies.
113      */
114     private interface StatusReporter {
115         /**
116          * Status update for tests.
117          */
onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)118         public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
119                 Bundle results);
120 
121         /**
122          * The tests finished.
123          */
onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)124         public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
125                 Bundle results);
126 
127         /**
128          * @param errorText a description of the error
129          * @param commandError True if the error is related to the commandline, as opposed
130          *      to a test failing.
131          */
onError(String errorText, boolean commandError)132         public void onError(String errorText, boolean commandError);
133     }
134 
sorted(Collection<String> list)135     private static Collection<String> sorted(Collection<String> list) {
136         final ArrayList<String> copy = new ArrayList<>(list);
137         Collections.sort(copy);
138         return copy;
139     }
140 
141     /**
142      * Printer for the 'classic' text based status reporting.
143      */
144     private class TextStatusReporter implements StatusReporter {
145         private boolean mRawMode;
146 
147         /**
148          * Human-ish readable output.
149          *
150          * @param rawMode   In "raw mode" (true), all bundles are dumped.
151          *                  In "pretty mode" (false), if a bundle includes
152          *                  Instrumentation.REPORT_KEY_STREAMRESULT, just print that.
153          */
TextStatusReporter(boolean rawMode)154         public TextStatusReporter(boolean rawMode) {
155             mRawMode = rawMode;
156         }
157 
158         @Override
onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)159         public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
160                 Bundle results) {
161             // pretty printer mode?
162             String pretty = null;
163             if (!mRawMode && results != null) {
164                 pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
165             }
166             if (pretty != null) {
167                 System.out.print(pretty);
168             } else {
169                 if (results != null) {
170                     for (String key : sorted(results.keySet())) {
171                         System.out.println(
172                                 "INSTRUMENTATION_STATUS: " + key + "=" + results.get(key));
173                     }
174                 }
175                 System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode);
176             }
177         }
178 
179         @Override
onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)180         public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
181                 Bundle results) {
182             // pretty printer mode?
183             String pretty = null;
184             if (!mRawMode && results != null) {
185                 pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
186             }
187             if (pretty != null) {
188                 System.out.println(pretty);
189             } else {
190                 if (results != null) {
191                     for (String key : sorted(results.keySet())) {
192                         System.out.println(
193                                 "INSTRUMENTATION_RESULT: " + key + "=" + results.get(key));
194                     }
195                 }
196                 System.out.println("INSTRUMENTATION_CODE: " + resultCode);
197             }
198         }
199 
200         @Override
onError(String errorText, boolean commandError)201         public void onError(String errorText, boolean commandError) {
202             if (mRawMode) {
203                 System.out.println("onError: commandError=" + commandError + " message="
204                         + errorText);
205             }
206             // The regular BaseCommand error printing will print the commandErrors.
207             if (!commandError) {
208                 System.out.println(errorText);
209             }
210         }
211     }
212 
213     /**
214      * Printer for the protobuf based status reporting.
215      */
216     private class ProtoStatusReporter implements StatusReporter {
217 
218         private File mLog;
219 
220         private long mTestStartMs;
221 
ProtoStatusReporter()222         ProtoStatusReporter() {
223             if (protoFile) {
224                 if (logPath == null) {
225                     File logDir = new File(Environment.getLegacyExternalStorageDirectory(),
226                             DEFAULT_LOG_DIR);
227                     if (!logDir.exists() && !logDir.mkdirs()) {
228                         System.err.format("Unable to create log directory: %s\n",
229                                 logDir.getAbsolutePath());
230                         protoFile = false;
231                         return;
232                     }
233                     SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-hhmmss-SSS", Locale.US);
234                     String fileName = String.format("log-%s.instrumentation_data_proto",
235                             format.format(new Date()));
236                     mLog = new File(logDir, fileName);
237                 } else {
238                     mLog = new File(Environment.getLegacyExternalStorageDirectory(), logPath);
239                     File logDir = mLog.getParentFile();
240                     if (!logDir.exists() && !logDir.mkdirs()) {
241                         System.err.format("Unable to create log directory: %s\n",
242                                 logDir.getAbsolutePath());
243                         protoFile = false;
244                         return;
245                     }
246                 }
247                 if (mLog.exists()) mLog.delete();
248             }
249         }
250 
251         @Override
onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)252         public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
253                 Bundle results) {
254             final ProtoOutputStream proto = new ProtoOutputStream();
255 
256             final long testStatusToken = proto.start(InstrumentationData.Session.TEST_STATUS);
257 
258             proto.write(InstrumentationData.TestStatus.RESULT_CODE, resultCode);
259             writeBundle(proto, InstrumentationData.TestStatus.RESULTS, results);
260 
261             if (resultCode == STATUS_TEST_STARTED) {
262                 // Logcat -T takes wall clock time (!?)
263                 mTestStartMs = System.currentTimeMillis();
264             } else {
265                 if (mTestStartMs > 0) {
266                     proto.write(InstrumentationData.TestStatus.LOGCAT, readLogcat(mTestStartMs));
267                 }
268                 mTestStartMs = 0;
269             }
270 
271             proto.end(testStatusToken);
272 
273             outputProto(proto);
274         }
275 
276         @Override
onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)277         public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
278                 Bundle results) {
279             final ProtoOutputStream proto = new ProtoOutputStream();
280 
281             final long sessionStatusToken = proto.start(InstrumentationData.Session.SESSION_STATUS);
282             proto.write(InstrumentationData.SessionStatus.STATUS_CODE,
283                     InstrumentationData.SESSION_FINISHED);
284             proto.write(InstrumentationData.SessionStatus.RESULT_CODE, resultCode);
285             writeBundle(proto, InstrumentationData.SessionStatus.RESULTS, results);
286             proto.end(sessionStatusToken);
287 
288             outputProto(proto);
289         }
290 
291         @Override
onError(String errorText, boolean commandError)292         public void onError(String errorText, boolean commandError) {
293             final ProtoOutputStream proto = new ProtoOutputStream();
294 
295             final long sessionStatusToken = proto.start(InstrumentationData.Session.SESSION_STATUS);
296             proto.write(InstrumentationData.SessionStatus.STATUS_CODE,
297                     InstrumentationData.SESSION_ABORTED);
298             proto.write(InstrumentationData.SessionStatus.ERROR_TEXT, errorText);
299             proto.end(sessionStatusToken);
300 
301             outputProto(proto);
302         }
303 
writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle)304         private void writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle) {
305             final long bundleToken = proto.start(fieldId);
306 
307             for (final String key: sorted(bundle.keySet())) {
308                 final long entryToken = proto.startRepeatedObject(
309                         InstrumentationData.ResultsBundle.ENTRIES);
310 
311                 proto.write(InstrumentationData.ResultsBundleEntry.KEY, key);
312 
313                 final Object val = bundle.get(key);
314                 if (val instanceof String) {
315                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_STRING,
316                             (String)val);
317                 } else if (val instanceof Byte) {
318                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT,
319                             ((Byte)val).intValue());
320                 } else if (val instanceof Double) {
321                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_DOUBLE, (double)val);
322                 } else if (val instanceof Float) {
323                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_FLOAT, (float)val);
324                 } else if (val instanceof Integer) {
325                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (int)val);
326                 } else if (val instanceof Long) {
327                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_LONG, (long)val);
328                 } else if (val instanceof Short) {
329                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (short)val);
330                 } else if (val instanceof Bundle) {
331                     writeBundle(proto, InstrumentationData.ResultsBundleEntry.VALUE_BUNDLE,
332                             (Bundle)val);
333                 } else if (val instanceof byte[]) {
334                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_BYTES, (byte[])val);
335                 }
336 
337                 proto.end(entryToken);
338             }
339 
340             proto.end(bundleToken);
341         }
342 
outputProto(ProtoOutputStream proto)343         private void outputProto(ProtoOutputStream proto) {
344             byte[] out = proto.getBytes();
345             if (protoStd) {
346                 try {
347                     System.out.write(out);
348                     System.out.flush();
349                 } catch (IOException ex) {
350                     System.err.println("Error writing finished response: ");
351                     ex.printStackTrace(System.err);
352                 }
353             }
354             if (protoFile) {
355                 try (OutputStream os = new FileOutputStream(mLog, true)) {
356                     os.write(proto.getBytes());
357                     os.flush();
358                 } catch (IOException ex) {
359                     System.err.format("Cannot write to %s:\n", mLog.getAbsolutePath());
360                     ex.printStackTrace();
361                 }
362             }
363         }
364     }
365 
366 
367     /**
368      * Callbacks from the remote instrumentation instance.
369      */
370     private class InstrumentationWatcher extends IInstrumentationWatcher.Stub {
371         private final StatusReporter mReporter;
372 
373         private boolean mFinished = false;
374 
InstrumentationWatcher(StatusReporter reporter)375         public InstrumentationWatcher(StatusReporter reporter) {
376             mReporter = reporter;
377         }
378 
379         @Override
instrumentationStatus(ComponentName name, int resultCode, Bundle results)380         public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) {
381             synchronized (this) {
382                 mReporter.onInstrumentationStatusLocked(name, resultCode, results);
383                 notifyAll();
384             }
385         }
386 
387         @Override
instrumentationFinished(ComponentName name, int resultCode, Bundle results)388         public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) {
389             synchronized (this) {
390                 mReporter.onInstrumentationFinishedLocked(name, resultCode, results);
391                 mFinished = true;
392                 notifyAll();
393             }
394         }
395 
waitForFinish()396         public boolean waitForFinish() {
397             synchronized (this) {
398                 while (!mFinished) {
399                     try {
400                         if (!mAm.asBinder().pingBinder()) {
401                             return false;
402                         }
403                         wait(1000);
404                     } catch (InterruptedException e) {
405                         throw new IllegalStateException(e);
406                     }
407                 }
408             }
409             return true;
410         }
411     }
412 
413     /**
414      * Figure out which component they really meant.
415      */
parseComponentName(String cnArg)416     private ComponentName parseComponentName(String cnArg) throws Exception {
417         if (cnArg.contains("/")) {
418             ComponentName cn = ComponentName.unflattenFromString(cnArg);
419             if (cn == null) throw new IllegalArgumentException("Bad component name: " + cnArg);
420             return cn;
421         } else {
422             List<InstrumentationInfo> infos = mPm.queryInstrumentation(null, 0).getList();
423 
424             final int numInfos = infos == null ? 0: infos.size();
425             ArrayList<ComponentName> cns = new ArrayList<>();
426             for (int i = 0; i < numInfos; i++) {
427                 InstrumentationInfo info = infos.get(i);
428 
429                 ComponentName c = new ComponentName(info.packageName, info.name);
430                 if (cnArg.equals(info.packageName)) {
431                     cns.add(c);
432                 }
433             }
434 
435             if (cns.size() == 0) {
436                 throw new IllegalArgumentException("No instrumentation found for: " + cnArg);
437             } else if (cns.size() == 1) {
438                 return cns.get(0);
439             } else {
440                 StringBuilder cnsStr = new StringBuilder();
441                 final int numCns = cns.size();
442                 for (int i = 0; i < numCns; i++) {
443                     cnsStr.append(cns.get(i).flattenToString());
444                     cnsStr.append(", ");
445                 }
446 
447                 // Remove last ", "
448                 cnsStr.setLength(cnsStr.length() - 2);
449 
450                 throw new IllegalArgumentException("Found multiple instrumentations: "
451                         + cnsStr.toString());
452             }
453         }
454     }
455 
456     /**
457      * Run the instrumentation.
458      */
run()459     public void run() throws Exception {
460         StatusReporter reporter = null;
461         float[] oldAnims = null;
462 
463         try {
464             // Choose which output we will do.
465             if (protoFile || protoStd) {
466                 reporter = new ProtoStatusReporter();
467             } else if (wait) {
468                 reporter = new TextStatusReporter(rawMode);
469             }
470 
471             // Choose whether we have to wait for the results.
472             InstrumentationWatcher watcher = null;
473             UiAutomationConnection connection = null;
474             if (reporter != null) {
475                 watcher = new InstrumentationWatcher(reporter);
476                 connection = new UiAutomationConnection();
477             }
478 
479             // Set the window animation if necessary
480             if (noWindowAnimation) {
481                 oldAnims = mWm.getAnimationScales();
482                 mWm.setAnimationScale(0, 0.0f);
483                 mWm.setAnimationScale(1, 0.0f);
484                 mWm.setAnimationScale(2, 0.0f);
485             }
486 
487             // Figure out which component we are trying to do.
488             final ComponentName cn = parseComponentName(componentNameArg);
489 
490             // Choose an ABI if necessary
491             if (abi != null) {
492                 final String[] supportedAbis = Build.SUPPORTED_ABIS;
493                 boolean matched = false;
494                 for (String supportedAbi : supportedAbis) {
495                     if (supportedAbi.equals(abi)) {
496                         matched = true;
497                         break;
498                     }
499                 }
500                 if (!matched) {
501                     throw new AndroidException(
502                             "INSTRUMENTATION_FAILED: Unsupported instruction set " + abi);
503                 }
504             }
505 
506             // Start the instrumentation
507             int flags = 0;
508             if (disableHiddenApiChecks) {
509                 flags |= INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS;
510             }
511             if (disableTestApiChecks) {
512                 flags |= INSTR_FLAG_DISABLE_TEST_API_CHECKS;
513             }
514             if (disableIsolatedStorage) {
515                 flags |= INSTR_FLAG_MOUNT_EXTERNAL_STORAGE_FULL;
516             }
517             if (!mAm.startInstrumentation(cn, profileFile, flags, args, watcher, connection, userId,
518                         abi)) {
519                 throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString());
520             }
521 
522             // If we have been requested to wait, do so until the instrumentation is finished.
523             if (watcher != null) {
524                 if (!watcher.waitForFinish()) {
525                     reporter.onError("INSTRUMENTATION_ABORTED: System has crashed.", false);
526                     return;
527                 }
528             }
529         } catch (Exception ex) {
530             // Report failures
531             if (reporter != null) {
532                 reporter.onError(ex.getMessage(), true);
533             }
534 
535             // And re-throw the exception
536             throw ex;
537         } finally {
538             // Clean up
539             if (oldAnims != null) {
540                 mWm.setAnimationScales(oldAnims);
541             }
542         }
543     }
544 
readLogcat(long startTimeMs)545     private static String readLogcat(long startTimeMs) {
546         try {
547             // Figure out the timestamp arg for logcat.
548             final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
549             final String timestamp = format.format(new Date(startTimeMs));
550 
551             // Start the process
552             final Process process = new ProcessBuilder()
553                     .command("logcat", "-d", "-v threadtime,uid", "-T", timestamp)
554                     .start();
555 
556             // Nothing to write. Don't let the command accidentally block.
557             process.getOutputStream().close();
558 
559             // Read the output
560             final StringBuilder str = new StringBuilder();
561             final InputStreamReader reader = new InputStreamReader(process.getInputStream());
562             char[] buffer = new char[4096];
563             int amt;
564             while ((amt = reader.read(buffer, 0, buffer.length)) >= 0) {
565                 if (amt > 0) {
566                     str.append(buffer, 0, amt);
567                 }
568             }
569 
570             try {
571                 process.waitFor();
572             } catch (InterruptedException ex) {
573                 // We already have the text, drop the exception.
574             }
575 
576             return str.toString();
577 
578         } catch (IOException ex) {
579             return "Error reading logcat command:\n" + ex.toString();
580         }
581     }
582 }
583 
584