1 /* 2 * Copyright 2022 Google LLC 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.google.android.libraries.mobiledatadownload.file.common.testing; 17 18 import static com.google.common.truth.Truth.assertWithMessage; 19 20 import android.os.Build; 21 import android.os.Process; 22 import android.os.SystemClock; 23 import android.system.Os; 24 import android.util.Log; 25 import com.google.common.collect.ImmutableList; 26 import com.google.common.collect.ImmutableMap; 27 import com.google.common.collect.Maps; 28 import com.google.errorprone.annotations.CanIgnoreReturnValue; 29 import java.io.BufferedReader; 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.InputStreamReader; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Set; 36 import org.junit.rules.MethodRule; 37 import org.junit.runners.model.FrameworkMethod; 38 import org.junit.runners.model.Statement; 39 40 /** 41 * Rule to ensure that tests do not leak file descriptors. This does not currently work with 42 * robolectric tests (b/121325017). 43 * 44 * <p>Usage: <code>@Rule FileDescriptorLeakChecker leakChecker = new FileDescriptorLeakChecker(); 45 * </code> 46 */ 47 public class FileDescriptorLeakChecker implements MethodRule { 48 private static final String TAG = "FileDescriptorLeakChecker"; 49 private static final int MAX_FDS = 1024; 50 51 private List<String> processesToMonitor = null; 52 private List<String> filesToMonitor = null; 53 private long msToWait = 0; 54 55 /** 56 * Processes whose FDs the FileDescriptorLeakChecker needs to monitor. 57 * 58 * @param processesToMonitor The names of the processes to monitor. 59 */ 60 @CanIgnoreReturnValue withProcessesToMonitor(List<String> processesToMonitor)61 public FileDescriptorLeakChecker withProcessesToMonitor(List<String> processesToMonitor) { 62 this.processesToMonitor = processesToMonitor; 63 return this; 64 } 65 66 @CanIgnoreReturnValue withFilesToMonitor(List<String> filesToMonitor)67 public FileDescriptorLeakChecker withFilesToMonitor(List<String> filesToMonitor) { 68 this.filesToMonitor = filesToMonitor; 69 return this; 70 } 71 72 /** 73 * If the files are closed asynchronously, the evaluation could fail. This option allows to 74 * perform the evaluation one more time. 75 * 76 * @param msToWait Milliseconds the FileDescriptorLeakChecker needs to wait before retrying. 77 */ 78 @CanIgnoreReturnValue withWaitIfFails(long msToWait)79 public FileDescriptorLeakChecker withWaitIfFails(long msToWait) { 80 this.msToWait = msToWait; 81 return this; 82 } 83 generateMap(List<Integer> pids)84 private ImmutableMap<String, Integer> generateMap(List<Integer> pids) { 85 ImmutableMap.Builder<String, Integer> names = ImmutableMap.builder(); 86 for (int pid : pids) { 87 String fdDir = "/proc/" + pid + "/fd/"; 88 for (int i = 0; i < MAX_FDS; i++) { 89 try { 90 File fdFile = new File(fdDir + i); 91 if (!fdFile.exists()) { 92 continue; 93 } 94 String filePath; 95 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 96 // Directly reading the symlink allows us to see special files (e.g., pipes), 97 // which are sanitized by getCanonicalPath() 98 filePath = Os.readlink(fdFile.getAbsolutePath()); 99 } else { 100 filePath = fdFile.getCanonicalPath(); 101 } 102 String key = fdFile + "=" + filePath; 103 names.put(key, pid); 104 } catch (Exception e) { 105 Log.w(TAG, i + " -> " + e); 106 } 107 } 108 } 109 return names.buildOrThrow(); 110 } 111 getProcessIds()112 private ImmutableList<Integer> getProcessIds() { 113 if (processesToMonitor == null) { 114 int myPid = Process.myPid(); 115 assertWithMessage("My process ID unavailable - are you using robolectric? b/121325017") 116 .that(myPid) 117 .isGreaterThan(0); 118 return ImmutableList.of(myPid); 119 } 120 ImmutableList.Builder<Integer> pids = ImmutableList.builder(); 121 try { 122 String line; 123 java.lang.Process p = Runtime.getRuntime().exec("ps"); 124 BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); 125 while ((line = input.readLine()) != null) { 126 for (String name : processesToMonitor) { 127 if (line.endsWith(name)) { 128 pids.add(Integer.parseInt(line.split("[ \t]+", -1)[1])); 129 break; 130 } 131 } 132 } 133 input.close(); 134 } catch (IOException e) { 135 Log.e(TAG, e.getMessage()); 136 } 137 return pids.build(); 138 } 139 filesToMonitorButOpen(Set<String> openFilesAfter)140 private int filesToMonitorButOpen(Set<String> openFilesAfter) { 141 int res = 0; 142 for (String file : filesToMonitor) { 143 res = openFilesAfter.contains(file) ? res + 1 : res; 144 } 145 return res; 146 } 147 numberOfInterestingOpenFiles(Map<String, Integer> diffMap)148 private int numberOfInterestingOpenFiles(Map<String, Integer> diffMap) { 149 Set<String> newOpenFiles = diffMap.keySet(); 150 return filesToMonitor == null ? newOpenFiles.size() : filesToMonitorButOpen(newOpenFiles); 151 } 152 difference( ImmutableMap<String, Integer> before, ImmutableMap<String, Integer> after)153 private Map<String, Integer> difference( 154 ImmutableMap<String, Integer> before, ImmutableMap<String, Integer> after) { 155 return Maps.difference(before, after).entriesOnlyOnRight(); 156 } 157 buildMessage(Map<String, Integer> openFiles)158 private String buildMessage(Map<String, Integer> openFiles) { 159 StringBuilder builder = new StringBuilder(); 160 builder.append("Your test is leaking file descriptors!\n"); 161 for (String key : openFiles.keySet()) { 162 builder.append(String.format("%s is still open in process %d\n", key, openFiles.get(key))); 163 } 164 builder.append('\n'); 165 return builder.toString(); 166 } 167 168 @Override apply(Statement base, FrameworkMethod method, Object target)169 public Statement apply(Statement base, FrameworkMethod method, Object target) { 170 return new Statement() { 171 @Override 172 public void evaluate() throws Throwable { 173 ImmutableList<Integer> pids = getProcessIds(); 174 ImmutableMap<String, Integer> beforeMap = generateMap(pids); 175 176 base.evaluate(); 177 178 Map<String, Integer> diffMap = difference(beforeMap, generateMap(pids)); 179 int diff = numberOfInterestingOpenFiles(diffMap); 180 if (diff > 0 && msToWait > 0) { 181 SystemClock.sleep(msToWait); 182 diffMap = difference(beforeMap, generateMap(pids)); 183 diff = numberOfInterestingOpenFiles(diffMap); 184 } 185 assertWithMessage(buildMessage(diffMap)).that(diff).isEqualTo(0); 186 } 187 }; 188 } 189 } 190