1 /*
2  * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 import static java.lang.Long.parseLong;
25 import static java.lang.System.getProperty;
26 import static java.nio.file.Files.readAllBytes;
27 import static java.util.Arrays.stream;
28 import static java.util.stream.Collectors.joining;
29 import static java.util.stream.Collectors.toList;
30 import static jdk.test.lib.process.ProcessTools.createJavaProcessBuilder;
31 import static jdk.test.lib.Platform.isWindows;
32 import jdk.test.lib.Utils;
33 import jdk.test.lib.Platform;
34 import jtreg.SkippedException;
35 
36 import java.io.BufferedReader;
37 import java.io.File;
38 import java.io.FileNotFoundException;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.io.InputStreamReader;
42 import java.util.Collection;
43 import java.util.Optional;
44 import java.util.stream.Stream;
45 
46 /*
47  * @test TestInheritFD
48  * @bug 8176717 8176809 8222500
49  * @summary a new process should not inherit open file descriptors
50  * @comment On Aix lsof requires root privileges.
51  * @requires os.family != "aix"
52  * @library /test/lib
53  * @modules java.base/jdk.internal.misc
54  *          java.management
55  * @run driver TestInheritFD
56  */
57 
58 /**
59  * Test that HotSpot does not leak logging file descriptors.
60  *
61  * This test is performed in three steps. The first VM starts a second VM with
62  * gc logging enabled. The second VM starts a third VM and redirects the third
63  * VMs output to the first VM, it then exits and hopefully closes its log file.
64  *
65  * The third VM waits for the second to exit and close its log file. After that,
66  * the third VM tries to rename the log file of the second VM. If it succeeds in
67  * doing so it means that the third VM did not inherit the open log file
68  * (windows can not rename opened files easily)
69  *
70  * The third VM communicates the success to rename the file by printing "CLOSED
71  * FD". The first VM checks that the string was printed by the third VM.
72  *
73  * On unix like systems "lsof" is used.
74  */
75 
76 public class TestInheritFD {
77 
78     public static final String LEAKS_FD = "VM RESULT => LEAKS FD";
79     public static final String RETAINS_FD = "VM RESULT => RETAINS FD";
80     public static final String EXIT = "VM RESULT => VM EXIT";
81     public static final String LOG_SUFFIX = ".strangelogsuffixthatcanbecheckedfor";
82 
83     // first VM
main(String[] args)84     public static void main(String[] args) throws Exception {
85         String logPath = Utils.createTempFile("logging", LOG_SUFFIX).toFile().getName();
86         File commFile = Utils.createTempFile("communication", ".txt").toFile();
87 
88         if (!isWindows() && !lsofCommand().isPresent()) {
89             throw new SkippedException("Could not find lsof like command");
90         }
91 
92         ProcessBuilder pb = createJavaProcessBuilder(
93             "-Xlog:gc:\"" + logPath + "\"",
94             "-Dtest.jdk=" + getProperty("test.jdk"),
95             VMStartedWithLogging.class.getName(),
96             logPath);
97 
98         pb.redirectOutput(commFile); // use temp file to communicate between processes
99         pb.start();
100 
101         String out = "";
102         do {
103             out = new String(readAllBytes(commFile.toPath()));
104             Thread.sleep(100);
105             System.out.println("SLEEP 100 millis");
106         } while (!out.contains(EXIT));
107 
108         System.out.println(out);
109         if (out.contains(RETAINS_FD)) {
110             System.out.println("Log file was not inherited by third VM");
111         } else {
112             throw new RuntimeException("could not match: " + RETAINS_FD);
113         }
114     }
115 
116     static class VMStartedWithLogging {
117         // second VM
main(String[] args)118         public static void main(String[] args) throws IOException, InterruptedException {
119             ProcessBuilder pb = createJavaProcessBuilder(
120                 "-Dtest.jdk=" + getProperty("test.jdk"),
121                 VMShouldNotInheritFileDescriptors.class.getName(),
122                 args[0],
123                 "" + ProcessHandle.current().pid());
124             pb.inheritIO(); // in future, redirect information from third VM to first VM
125             pb.start();
126 
127             if (!isWindows()) {
128                 System.out.println("(Second VM) Open file descriptors:\n" + outputContainingFilenames().stream().collect(joining("\n")));
129             }
130         }
131     }
132 
133     static class VMShouldNotInheritFileDescriptors {
134         // third VM
main(String[] args)135         public static void main(String[] args) throws InterruptedException {
136             try {
137                 File logFile = new File(args[0]);
138                 long parentPid = parseLong(args[1]);
139                 fakeLeakyJVM(false); // for debugging of test case
140 
141                 if (isWindows()) {
142                     windows(logFile, parentPid);
143                 } else {
144                     Collection<String> output = outputContainingFilenames();
145                     System.out.println("(Third VM) Open file descriptors:\n" + output.stream().collect(joining("\n")));
146                     System.out.println(findOpenLogFile(output) ? LEAKS_FD : RETAINS_FD);
147                 }
148             } catch (Exception e) {
149                 System.out.println(e.toString());
150             } finally {
151                 System.out.println(EXIT);
152             }
153         }
154     }
155 
156     // for debugging of test case
157     @SuppressWarnings("resource")
fakeLeakyJVM(boolean fake)158     static void fakeLeakyJVM(boolean fake) {
159         if (fake) {
160             try {
161                 new FileOutputStream("fakeLeakyJVM" + LOG_SUFFIX, false);
162             } catch (FileNotFoundException e) {
163             }
164         }
165     }
166 
run(String... args)167     static Stream<String> run(String... args){
168         try {
169             return new BufferedReader(new InputStreamReader(new ProcessBuilder(args).start().getInputStream())).lines();
170         } catch (IOException e) {
171             throw new RuntimeException(e);
172         }
173     }
174 
175     static Optional<String[]> lsofCommandCache = stream(new String[][]{
176             {"/usr/bin/lsof", "-p"},
177             {"/usr/sbin/lsof", "-p"},
178             {"/bin/lsof", "-p"},
179             {"/sbin/lsof", "-p"},
180             {"/usr/local/bin/lsof", "-p"}})
181         .filter(args -> new File(args[0]).exists())
182         .findFirst();
183 
lsofCommand()184     static Optional<String[]> lsofCommand() {
185         return lsofCommandCache;
186     }
187 
outputContainingFilenames()188     static Collection<String> outputContainingFilenames() {
189         long pid = ProcessHandle.current().pid();
190 
191         String[] command = lsofCommand().orElseThrow(() -> new RuntimeException("lsof like command not found"));
192         System.out.println("using command: " + command[0] + " " + command[1]);
193         return run(command[0], command[1], "" + pid).collect(toList());
194     }
195 
findOpenLogFile(Collection<String> fileNames)196     static boolean findOpenLogFile(Collection<String> fileNames) {
197         String pid = Long.toString(ProcessHandle.current().pid());
198         String[] command = lsofCommand().orElseThrow(() ->
199                 new RuntimeException("lsof like command not found"));
200         String lsof = command[0];
201         boolean isBusybox = Platform.isBusybox(lsof);
202         return fileNames.stream()
203             // lsof from busybox does not support "-p" option
204             .filter(fileName -> !isBusybox || fileName.contains(pid))
205             .filter(fileName -> fileName.contains(LOG_SUFFIX))
206             .findAny()
207             .isPresent();
208     }
209 
windows(File f, long parentPid)210     static void windows(File f, long parentPid) throws InterruptedException {
211         System.out.println("waiting for pid: " + parentPid);
212         ProcessHandle.of(parentPid).ifPresent(handle -> handle.onExit().join());
213         System.out.println("trying to rename file to the same name: " + f);
214         System.out.println(f.renameTo(f) ? RETAINS_FD : LEAKS_FD); // this parts communicates a closed file descriptor by printing "VM RESULT => RETAINS FD"
215     }
216 }
217 
218