1 /*
2  * Copyright (C) 2022 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 package com.android.compatibility.common.util;
17 
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.fail;
20 
21 import android.app.Instrumentation;
22 import android.os.ParcelFileDescriptor;
23 import android.os.SystemClock;
24 import android.util.Log;
25 
26 import java.io.BufferedReader;
27 import java.io.FileInputStream;
28 import java.io.InputStreamReader;
29 import java.util.ArrayDeque;
30 import java.util.Queue;
31 
32 import javax.annotation.concurrent.GuardedBy;
33 
34 /**
35  * Detects an ANR using `am monitor`.
36  *
37  * Use {@link #start} to create an instance and start monitoring.
38  * Use {@link #waitForAnrAndReturnUptime} to wait for an "early ANR", and get the uptime of it.
39  * ANR monitoring is single-shot, so once an ANR is detected,
40  * the target process will be killed and the AnrMonitor instance will be unusable.
41  */
42 public class AnrMonitor implements AutoCloseable {
43     private static final String TAG = "AnrMonitor";
44 
45     public static final long NO_ANR = -1L;
46 
47     private final Instrumentation mInstrumentation;
48     private final String mTargetProcess;
49     private final Thread mThread;
50     private volatile boolean mStop = false;
51 
52     /** Queue of detected "early ANR" uptime */
53     @GuardedBy("mEventQueue")
54     private final Queue<Long> mEventQueue = new ArrayDeque<>(0);
55 
AnrMonitor(Instrumentation instrumentation, String targetProcess)56     private AnrMonitor(Instrumentation instrumentation, String targetProcess) {
57         mInstrumentation = instrumentation;
58         mTargetProcess = targetProcess;
59         mThread = new Thread(this::threadMain, "AnrMonitor");
60         mThread.setDaemon(true);
61     }
62 
63     /**
64      * Start monitoring a process for an "early ANR".
65      */
start(Instrumentation instrumentation, String targetProcess)66     public static AnrMonitor start(Instrumentation instrumentation, String targetProcess) {
67         final AnrMonitor instance =  new AnrMonitor(instrumentation, targetProcess);
68 
69         instance.run();
70 
71         return instance;
72     }
73 
run()74     private void run() {
75         mThread.start();
76     }
77 
78     @Override
close()79     public void close() {
80         mStop = true;
81         synchronized (mEventQueue) {
82             mEventQueue.notifyAll();
83         }
84         if (mThread.isAlive()) {
85             mThread.interrupt();
86         }
87     }
88 
89     /**
90      * Return for an early-ANR event from the target process, and return the uptime of it.
91      * Fails if an ANR doesn't happen before the timeout.
92      */
waitForAnrAndReturnUptime(long timeoutMillis)93     public long waitForAnrAndReturnUptime(long timeoutMillis) {
94         return waitForAnrAndReturnUptime(true, timeoutMillis);
95     }
96 
97     /**
98      * Asserts that no ANR occurs within the specified timeframe.
99      */
assertNoAnr(long timeoutMillis)100     public void assertNoAnr(long timeoutMillis) {
101         assertEquals(NO_ANR, waitForAnrAndReturnUptime(false, timeoutMillis));
102     }
103 
104     /**
105      * Waits for an ANR for the target process.
106      * If an ANR is expected, the uptime when an ANR event occurred will be returned, or the
107      * method will fail.
108      * If no ANR is expected, the method will fail if an ANR is seen, or it will return
109      * {@link #NO_ANR} after the timeout.
110      */
waitForAnrAndReturnUptime(boolean expectAnr, long timeoutMillis)111     private long waitForAnrAndReturnUptime(boolean expectAnr, long timeoutMillis) {
112         if (timeoutMillis <= 0) {
113             fail("Timeout must be positive");
114         }
115         if (mStop) {
116             fail("Monitor has been closed");
117         }
118         try {
119             final long timeoutUptime = SystemClock.uptimeMillis() + timeoutMillis;
120             synchronized (mEventQueue) {
121                 for (;;) {
122                     if (mEventQueue.size() == 0) {
123                         final long waitTime = timeoutUptime - SystemClock.uptimeMillis();
124                         if (waitTime <= 0) {
125                             // Timed out
126                             if (expectAnr) {
127                                 fail("Timeout waiting for an ANR event from `am monitor`");
128                             } else {
129                                 return NO_ANR;
130                             }
131                         }
132                         try {
133                             mEventQueue.wait(waitTime);
134                         } catch (InterruptedException e) {
135                             continue;
136                         }
137                     }
138                     final Long uptime = mEventQueue.poll();
139                     if (uptime == null) {
140                         // Empty
141                         continue;
142                     }
143                     if (!expectAnr) {
144                         fail("App had unexpected ANR");
145                     }
146                     return uptime;
147                 }
148             }
149         } finally {
150             close();
151         }
152     }
153 
threadMain()154     private void threadMain() {
155         ParcelFileDescriptor[] pfds = null;
156         try {
157             pfds = mInstrumentation.getUiAutomation()
158                     .executeShellCommandRw("am monitor -s -k -p " + mTargetProcess);
159             final ParcelFileDescriptor rfd = pfds[0];
160             final FileInputStream rfs = new ParcelFileDescriptor.AutoCloseInputStream(rfd);
161             final BufferedReader reader = new BufferedReader(new InputStreamReader(rfs));
162 
163             Log.i(TAG, "am monitor started");
164 
165             // We don't use the writer.
166             // We don't need to send "q" for finish. Closing the FD will finish the command.
167 
168             // Read from the `am monitor` command output...
169             for (;;) {
170                 if (mStop) {
171                     return;
172                 }
173                 final String line = reader.readLine();
174                 if (line == null || mStop) {
175                     // command finished, or stopping
176                     return;
177                 }
178                 if (!line.startsWith("** EARLY PROCESS NOT RESPONDING:")) {
179                     Log.i(TAG, "Ignoring unrelated am monitor output: " + line);
180                     // ignore
181                     continue;
182                 }
183 
184                 Log.i(TAG, "Early ANR detected: " + line);
185                 synchronized (mEventQueue) {
186                     mEventQueue.add(SystemClock.uptimeMillis());
187                     mEventQueue.notifyAll();
188                 }
189             }
190         } catch (Throwable th) {
191             if (!mStop) {
192                 Log.e(TAG, "BG thread dying unexpectedly", th);
193                 fail("Unexpected exception detected: " + th.getMessage() + "\n"
194                         + Log.getStackTraceString(th));
195             }
196         } finally {
197             if (pfds != null) {
198                 closeQuietly(pfds[0]);
199                 closeQuietly(pfds[1]);
200             }
201             Log.i(TAG, "am monitor finished");
202         }
203     }
204 
closeQuietly(AutoCloseable c)205     private static void closeQuietly(AutoCloseable c) {
206         if (c != null) {
207             try {
208                 c.close();
209             } catch (Exception e) {
210             }
211         }
212     }
213 }
214