1 /*
   2  * Copyright (c) 2018, 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.io.File.createTempFile;
  25 import static java.lang.Long.parseLong;
  26 import static java.lang.System.getProperty;
  27 import static java.nio.file.Files.readAllBytes;
  28 import static java.util.Arrays.stream;
  29 import static java.util.stream.Collectors.joining;
  30 import static java.util.stream.Collectors.toList;
  31 import static jdk.test.lib.process.ProcessTools.createJavaProcessBuilder;
  32 import static jdk.test.lib.Platform.isWindows;
  33 
  34 import java.io.BufferedReader;
  35 import java.io.File;
  36 import java.io.FileNotFoundException;
  37 import java.io.FileOutputStream;
  38 import java.io.IOException;
  39 import java.io.InputStreamReader;
  40 import java.util.Collection;
  41 import java.util.Optional;
  42 import java.util.stream.Stream;
  43 
  44 /*
  45  * @test TestInheritFD
  46  * @bug 8176717 8176809
  47  * @summary a new process should not inherit open file descriptors
  48  * @library /test/lib
  49  * @modules java.base/jdk.internal.misc
  50  *          java.management
  51  */
  52 
  53 /**
  54  * Test that HotSpot does not leak logging file descriptors.
  55  *
  56  * This test is performed in three steps. The first VM starts a second VM with
  57  * gc logging enabled. The second VM starts a third VM and redirects the third
  58  * VMs output to the first VM, it then exits and hopefully closes its log file.
  59  *
  60  * The third VM waits for the second to exit and close its log file. After that,
  61  * the third VM tries to rename the log file of the second VM. If it succeeds in
  62  * doing so it means that the third VM did not inherit the open log file
  63  * (windows can not rename opened files easily)
  64  *
  65  * The third VM communicates the success to rename the file by printing "CLOSED
  66  * FD". The first VM checks that the string was printed by the third VM.
  67  *
  68  * On unix like systems "lsof" or "pfiles" is used.
  69  */
  70 
  71 public class TestInheritFD {
  72 
  73     public static final String LEAKS_FD = "VM RESULT => LEAKS FD";
  74     public static final String RETAINS_FD = "VM RESULT => RETAINS FD";
  75     public static final String EXIT = "VM RESULT => VM EXIT";
  76     public static final String LOG_SUFFIX = ".strangelogsuffixthatcanbecheckedfor";
  77 
  78     // first VM
  79     public static void main(String[] args) throws Exception {
  80         String logPath = createTempFile("logging", LOG_SUFFIX).getName();
  81         File commFile = createTempFile("communication", ".txt");
  82 
  83         if (!isWindows() && !lsofCommand().isPresent()) {
  84             System.out.println("Could not find lsof like command");
  85             System.out.println("Exit test case as successful though it could not verify anything");
  86             return;
  87         }
  88 
  89         ProcessBuilder pb = createJavaProcessBuilder(
  90             "-Xlog:gc:\"" + logPath + "\"",
  91             "-Dtest.jdk=" + getProperty("test.jdk"),
  92             VMStartedWithLogging.class.getName(),
  93             logPath);
  94 
  95         pb.redirectOutput(commFile); // use temp file to communicate between processes
  96         pb.start();
  97 
  98         String out = "";
  99         do {
 100             out = new String(readAllBytes(commFile.toPath()));
 101             Thread.sleep(100);
 102             System.out.println("SLEEP 100 millis");
 103         } while (!out.contains(EXIT));
 104 
 105         System.out.println(out);
 106         if (out.contains(RETAINS_FD)) {
 107             System.out.println("Log file was not inherited by third VM");
 108         } else {
 109             throw new RuntimeException("could not match: " + RETAINS_FD);
 110         }
 111     }
 112 
 113     static class VMStartedWithLogging {
 114         // second VM
 115         public static void main(String[] args) throws IOException, InterruptedException {
 116             ProcessBuilder pb = createJavaProcessBuilder(
 117                 "-Dtest.jdk=" + getProperty("test.jdk"),
 118                 VMShouldNotInheritFileDescriptors.class.getName(),
 119                 args[0],
 120                 "" + ProcessHandle.current().pid());
 121             pb.inheritIO(); // in future, redirect information from third VM to first VM
 122             pb.start();
 123 
 124             if (!isWindows()) {
 125                 System.out.println("(Second VM) Open file descriptors:\n" + outputContainingFilenames().stream().collect(joining("\n")));
 126             }
 127         }
 128     }
 129 
 130     static class VMShouldNotInheritFileDescriptors {
 131         // third VM
 132         public static void main(String[] args) throws InterruptedException {
 133             try {
 134                 File logFile = new File(args[0]);
 135                 long parentPid = parseLong(args[1]);
 136                 fakeLeakyJVM(false); // for debugging of test case
 137 
 138                 if (isWindows()) {
 139                     windows(logFile, parentPid);
 140                 } else {
 141                     Collection<String> output = outputContainingFilenames();
 142                     System.out.println("(Third VM) Open file descriptors:\n" + output.stream().collect(joining("\n")));
 143                     System.out.println(findOpenLogFile(output) ? LEAKS_FD : RETAINS_FD);
 144                 }
 145             } catch (Exception e) {
 146                 System.out.println(e.toString());
 147             } finally {
 148                 System.out.println(EXIT);
 149             }
 150         }
 151     }
 152 
 153     // for debugging of test case
 154     @SuppressWarnings("resource")
 155     static void fakeLeakyJVM(boolean fake) {
 156         if (fake) {
 157             try {
 158                 new FileOutputStream("fakeLeakyJVM" + LOG_SUFFIX, false);
 159             } catch (FileNotFoundException e) {
 160             }
 161         }
 162     }
 163 
 164     static Stream<String> run(String... args){
 165         try {
 166             return new BufferedReader(new InputStreamReader(new ProcessBuilder(args).start().getInputStream())).lines();
 167         } catch (IOException e) {
 168             throw new RuntimeException(e);
 169         }
 170     }
 171 
 172     static Optional<String[]> lsofCommandCache = stream(new String[][]{
 173             {"/usr/bin/lsof", "-p"},
 174             {"/usr/sbin/lsof", "-p"},
 175             {"/bin/lsof", "-p"},
 176             {"/sbin/lsof", "-p"},
 177             {"/usr/local/bin/lsof", "-p"},
 178             {"/usr/bin/pfiles", "-F"}}) // Solaris
 179         .filter(args -> new File(args[0]).exists())
 180         .findFirst();
 181 
 182     static Optional<String[]> lsofCommand() {
 183         return lsofCommandCache;
 184     }
 185 
 186     static Collection<String> outputContainingFilenames() {
 187         long pid = ProcessHandle.current().pid();
 188 
 189         String[] command = lsofCommand().orElseThrow(() -> new RuntimeException("lsof like command not found"));
 190         System.out.println("using command: " + command[0] + " " + command[1]);
 191         return run(command[0], command[1], "" + pid).collect(toList());
 192     }
 193 
 194     static boolean findOpenLogFile(Collection<String> fileNames) {
 195         return fileNames.stream()
 196             .filter(fileName -> fileName.contains(LOG_SUFFIX))
 197             .findAny()
 198             .isPresent();
 199     }
 200 
 201     static void windows(File f, long parentPid) throws InterruptedException {
 202         System.out.println("waiting for pid: " + parentPid);
 203         ProcessHandle.of(parentPid).ifPresent(handle -> handle.onExit().join());
 204         System.out.println("trying to rename file to the same name: " + f);
 205         System.out.println(f.renameTo(f) ? RETAINS_FD : LEAKS_FD); // this parts communicates a closed file descriptor by printing "VM RESULT => RETAINS FD"
 206     }
 207 }
 208