• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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