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 }