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