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