1 /*
2  * Copyright (C) 2020 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.modules.utils.testing;
18 
19 import android.system.ErrnoException;
20 import android.system.Os;
21 import android.util.Log;
22 
23 import androidx.test.internal.runner.listener.InstrumentationRunListener;
24 
25 import org.junit.runner.Result;
26 
27 import java.io.BufferedReader;
28 import java.io.FileReader;
29 import java.io.IOException;
30 import java.util.HashMap;
31 import java.util.Map;
32 
33 /**
34  * Instrumentation listener that dumps Clang coverage when the test run finishes.
35  *
36  * This is necessary as native coverage is not dumped automatically after Java/JNI tests
37  * (b/185074329) and should be replaced by more generic solutions (b/185822084).
38  */
39 public class NativeCoverageHackInstrumentationListener extends InstrumentationRunListener {
40     private static final String LOG_TAG =
41             NativeCoverageHackInstrumentationListener.class.getSimpleName();
42     // Signal used to trigger a dump of Clang coverage information.
43     // See {@code maybeDumpNativeCoverage} below.
44     private static final int COVERAGE_SIGNAL = 37;
45 
46     @Override
testRunFinished(Result result)47     public void testRunFinished(Result result) throws Exception {
48         maybeDumpNativeCoverage();
49         super.testRunFinished(result);
50     }
51 
52     /**
53      * If this test process is instrumented for native coverage, then trigger a dump
54      * of the coverage data and wait until either we detect the dumping has finished or 60 seconds,
55      * whichever is shorter.
56      *
57      * Background: Coverage builds install a signal handler for signal 37 which flushes coverage
58      * data to disk, which may take a few seconds.  Tests running as an app process will get
59      * killed with SIGKILL once the app code exits, even if the coverage handler is still running.
60      *
61      * Method: If a handler is installed for signal 37, then assume this is a coverage run and
62      * send signal 37.  The handler is non-reentrant and so signal 37 will then be blocked until
63      * the handler completes. So after we send the signal, we loop checking the blocked status
64      * for signal 37 until we hit the 60 second deadline.  If the signal is blocked then sleep for
65      * 2 seconds, and if it becomes unblocked then the handler exitted so we can return early.
66      * If the signal is not blocked at the start of the loop then most likely the handler has
67      * not yet been invoked.  This should almost never happen as it should get blocked on delivery
68      * when we call {@code Os.kill()}, so sleep for a shorter duration (100ms) and try again.  There
69      * is a race condition here where the handler is delayed but then runs for less than 100ms and
70      * gets missed, in which case this method will loop with 100ms sleeps until the deadline.
71      *
72      * In the case where the handler runs for more than 60 seconds, the test process will be allowed
73      * to exit so coverage information may be incomplete.
74      *
75      * There is no API for determining signal dispositions, so this method uses the
76      * {@link SignalMaskInfo} class to read the data from /proc.  If there is an error parsing
77      * the /proc data then this method will also loop until the 60s deadline passes.
78      */
maybeDumpNativeCoverage()79     private void maybeDumpNativeCoverage() {
80         SignalMaskInfo siginfo = new SignalMaskInfo();
81         if (!siginfo.isValid()) {
82             Log.e(LOG_TAG, "Invalid signal info");
83             return;
84         }
85 
86         if (!siginfo.isCaught(COVERAGE_SIGNAL)) {
87             // Process is not instrumented for coverage
88             Log.i(LOG_TAG, "Not dumping coverage, no handler installed");
89             return;
90         }
91 
92         Log.i(LOG_TAG,
93                 String.format("Sending coverage dump signal %d to pid %d uid %d", COVERAGE_SIGNAL,
94                         Os.getpid(), Os.getuid()));
95         try {
96             Os.kill(Os.getpid(), COVERAGE_SIGNAL);
97         } catch (ErrnoException e) {
98             Log.e(LOG_TAG, "Unable to send coverage signal", e);
99             return;
100         }
101 
102         long start = System.currentTimeMillis();
103         long deadline = start + 60 * 1000L;
104         while (System.currentTimeMillis() < deadline) {
105             siginfo.refresh();
106             try {
107                 if (siginfo.isValid() && siginfo.isBlocked(COVERAGE_SIGNAL)) {
108                     // Signal is currently blocked so assume a handler is running
109                     Thread.sleep(2000L);
110                     siginfo.refresh();
111                     if (siginfo.isValid() && !siginfo.isBlocked(COVERAGE_SIGNAL)) {
112                         // Coverage handler exited while we were asleep
113                         Log.i(LOG_TAG,
114                                 String.format("Coverage dump detected finished after %dms",
115                                         System.currentTimeMillis() - start));
116                         break;
117                     }
118                 } else {
119                     // Coverage signal handler not yet started or invalid siginfo
120                     Thread.sleep(100L);
121                 }
122             } catch (InterruptedException e) {
123                 // ignored
124             }
125         }
126     }
127 
128     /**
129      * Class for reading a process' signal masks from the /proc filesystem.  Looks for the
130      * BLOCKED, CAUGHT, IGNORED and PENDING masks from /proc/self/status, each of which is a
131      * 64 bit bitmask with one bit per signal.
132      *
133      * Maintains a map from SignalMaskInfo.Type to the bitmask.  The {@code isValid} method
134      * will only return true if all 4 masks were successfully parsed. Provides lookup
135      * methods per signal, e.g. {@code isPending(signum)} which will throw
136      * {@code IllegalStateException} if the current data is not valid.
137      */
138     private static class SignalMaskInfo {
139         private enum Type {
140             BLOCKED("SigBlk"),
141             CAUGHT("SigCgt"),
142             IGNORED("SigIgn"),
143             PENDING("SigPnd");
144             // The tag for this mask in /proc/self/status
145             private final String tag;
146 
Type(String tag)147             Type(String tag) {
148                 this.tag = tag + ":\t";
149             }
150 
getTag()151             public String getTag() {
152                 return tag;
153             }
154 
parseProcinfo(String path)155             public static Map<Type, Long> parseProcinfo(String path) {
156                 Map<Type, Long> map = new HashMap<>();
157                 try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
158                     String line;
159                     while ((line = reader.readLine()) != null) {
160                         for (Type mask : Type.values()) {
161                             long value = mask.tryToParse(line);
162                             if (value >= 0) {
163                                 map.put(mask, value);
164                             }
165                         }
166                     }
167                 } catch (NumberFormatException | IOException e) {
168                     // Ignored - the map will end up being invalid instead.
169                 }
170                 return map;
171             }
172 
tryToParse(String line)173             private long tryToParse(String line) {
174                 if (line.startsWith(tag)) {
175                     return Long.valueOf(line.substring(tag.length()), 16);
176                 } else {
177                     return -1;
178                 }
179             }
180         }
181 
182         private static final String PROCFS_PATH = "/proc/self/status";
183         private Map<Type, Long> maskMap = null;
184 
SignalMaskInfo()185         SignalMaskInfo() {
186             refresh();
187         }
188 
refresh()189         public void refresh() {
190             maskMap = Type.parseProcinfo(PROCFS_PATH);
191         }
192 
isValid()193         public boolean isValid() {
194             return (maskMap != null && maskMap.size() == Type.values().length);
195         }
196 
isCaught(int signal)197         public boolean isCaught(int signal) {
198             return isSignalInMask(signal, Type.CAUGHT);
199         }
200 
isBlocked(int signal)201         public boolean isBlocked(int signal) {
202             return isSignalInMask(signal, Type.BLOCKED);
203         }
204 
isPending(int signal)205         public boolean isPending(int signal) {
206             return isSignalInMask(signal, Type.PENDING);
207         }
208 
isIgnored(int signal)209         public boolean isIgnored(int signal) {
210             return isSignalInMask(signal, Type.IGNORED);
211         }
212 
checkValid()213         private void checkValid() {
214             if (!isValid()) {
215                 throw new IllegalStateException();
216             }
217         }
218 
isSignalInMask(int signal, Type mask)219         private boolean isSignalInMask(int signal, Type mask) {
220             long bit = 1L << (signal - 1);
221             return (getSignalMask(mask) & bit) != 0;
222         }
223 
getSignalMask(Type mask)224         private long getSignalMask(Type mask) {
225             checkValid();
226             Long value = maskMap.get(mask);
227             if (value == null) {
228                 throw new IllegalStateException();
229             }
230             return value;
231         }
232     }
233 }