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