| /* | |
| * Copyright (c) 2003, 2019, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* @test | |
| @bug 4843136 4763384 8044841 | |
| @summary Various race conditions caused exec'ed processes to have | |
| extra unused file descriptors, which caused hard-to-reproduce hangs. | |
| @author Martin Buchholz | |
| */ | |
| import java.util.Timer; | |
| import java.util.TimerTask; | |
| import java.io.IOException; | |
| public class SleepyCat { | |
| private static void destroy (Process[] deathRow) { | |
| for (int i = 0; i < deathRow.length; ++i) | |
| if (deathRow[i] != null) | |
| deathRow[i].destroy(); | |
| } | |
| static class TimeoutTask extends TimerTask { | |
| private Process[] deathRow; | |
| private boolean timedOut; | |
| TimeoutTask (Process[] deathRow) { | |
| this.deathRow = deathRow; | |
| this.timedOut = false; | |
| } | |
| public void run() { | |
| dumpState(deathRow); // before killing the processes dump all the state | |
| timedOut = true; | |
| destroy(deathRow); | |
| } | |
| public boolean timedOut() { | |
| return timedOut; | |
| } | |
| } | |
| /** | |
| * Temporary debugging code for intermittent failures. | |
| * @param pids the processes to dump status for | |
| */ | |
| static void dumpState(Process... pids) { | |
| if (!System.getProperty("os.name").contains("SunOS")) { | |
| return; | |
| } | |
| try { | |
| String[] psArgs = {"ps", "-elf"}; | |
| Process ps = new ProcessBuilder(psArgs).inheritIO().start(); | |
| ps.waitFor(); | |
| String[] sfiles = {"pfiles", "self"}; | |
| Process fds = new ProcessBuilder(sfiles).inheritIO().start(); | |
| fds.waitFor(); | |
| for (Process p : pids) { | |
| if (p == null) | |
| continue; | |
| String[] pfiles = {"pfiles", Long.toString(p.pid())}; | |
| fds = new ProcessBuilder(pfiles).inheritIO().start(); | |
| fds.waitFor(); | |
| String[] pstack = {"pstack", Long.toString(p.pid())}; | |
| fds = new ProcessBuilder(pstack).inheritIO().start(); | |
| fds.waitFor(); | |
| } | |
| } catch (IOException | InterruptedException i) { | |
| i.printStackTrace(); | |
| } | |
| } | |
| private static boolean hang1() throws IOException, InterruptedException { | |
| // Time out was reproducible on Solaris 50% of the time; | |
| // on Linux 80% of the time. | |
| // | |
| // Scenario: After fork(), parent executes and closes write end of child's stdin. | |
| // This causes child to retain a write end of the same pipe. | |
| // Thus the child will never see an EOF on its stdin, and will hang. | |
| Runtime rt = Runtime.getRuntime(); | |
| // Increasing the iteration count makes the bug more | |
| // reproducible not only for the obvious reason, but also for | |
| // the subtle reason that it makes reading /proc/getppid()/fd | |
| // slower, making the child more likely to win the race! | |
| int iterations = 20; | |
| int timeout = 30; | |
| String[] catArgs = new String[] {UnixCommands.cat()}; | |
| String[] sleepArgs = new String[] {UnixCommands.sleep(), | |
| String.valueOf(timeout+1)}; | |
| Process[] cats = new Process[iterations]; | |
| Process[] sleeps = new Process[iterations]; | |
| Timer timer = new Timer(true); | |
| TimeoutTask catExecutioner = new TimeoutTask(cats); | |
| timer.schedule(catExecutioner, timeout * 1000); | |
| for (int i = 0; i < cats.length; ++i) { | |
| cats[i] = rt.exec(catArgs); | |
| java.io.OutputStream s = cats[i].getOutputStream(); | |
| Process sleep = rt.exec(sleepArgs); | |
| s.close(); // race condition here | |
| sleeps[i] = sleep; | |
| } | |
| for (int i = 0; i < cats.length; ++i) | |
| cats[i].waitFor(); // hangs? | |
| timer.cancel(); | |
| destroy(sleeps); | |
| if (catExecutioner.timedOut()) | |
| System.out.println("Child process has a hidden writable pipe fd for its stdin."); | |
| return catExecutioner.timedOut(); | |
| } | |
| private static boolean hang2() throws Exception { | |
| // Inspired by the imaginative test case for | |
| // 4850368 (process) getInputStream() attaches to forked background processes (Linux) | |
| // Time out was reproducible on Linux 80% of the time; | |
| // never on Solaris because of explicit close in Solaris-specific code. | |
| // Scenario: After fork(), the parent naturally closes the | |
| // child's stdout write end. The child dup2's the write end | |
| // of its stdout onto fd 1. On Linux, it fails to explicitly | |
| // close the original fd, and because of the parent's close() | |
| // of the fd, the child retains it. The child thus ends up | |
| // with two copies of its stdout. Thus closing one of those | |
| // write fds does not have the desired effect of causing an | |
| // EOF on the parent's read end of that pipe. | |
| Runtime rt = Runtime.getRuntime(); | |
| int iterations = 10; | |
| Timer timer = new Timer(true); | |
| int timeout = 30; | |
| Process[] backgroundSleepers = new Process[iterations]; | |
| TimeoutTask sleeperExecutioner = new TimeoutTask(backgroundSleepers); | |
| timer.schedule(sleeperExecutioner, timeout * 1000); | |
| byte[] buffer = new byte[10]; | |
| String[] args = | |
| new String[] {UnixCommands.sh(), "-c", | |
| "exec " + UnixCommands.sleep() + " " | |
| + (timeout+1) + " >/dev/null"}; | |
| for (int i = 0; | |
| i < backgroundSleepers.length && !sleeperExecutioner.timedOut(); | |
| ++i) { | |
| backgroundSleepers[i] = rt.exec(args); // race condition here | |
| try { | |
| // should get immediate EOF, but might hang | |
| if (backgroundSleepers[i].getInputStream().read() != -1) | |
| throw new Exception("Expected EOF, got a byte"); | |
| } catch (IOException e) { | |
| // Stream closed by sleeperExecutioner | |
| break; | |
| } | |
| } | |
| timer.cancel(); | |
| destroy(backgroundSleepers); | |
| if (sleeperExecutioner.timedOut()) | |
| System.out.println("Child process has two (should be one) writable pipe fds for its stdout."); | |
| return sleeperExecutioner.timedOut(); | |
| } | |
| public static void main (String[] args) throws Exception { | |
| if (! UnixCommands.isUnix) { | |
| System.out.println("For UNIX only"); | |
| return; | |
| } | |
| UnixCommands.ensureCommandsAvailable("sh", "cat", "sleep"); | |
| if (hang1() | hang2()) | |
| throw new Exception("Read from closed pipe hangs"); | |
| } | |
| } |