From c1c9704268e9e651cd82c8550913d8ac60aa494a Mon Sep 17 00:00:00 2001
From: Naoto Sato <naoto@openjdk.org>
Date: Mon, 22 Jul 2024 17:17:46 +0000
Subject: [PATCH] 8336479: Provide Process.waitFor(Duration)

Reviewed-by: liach, jpai, rriggs
---
 .../share/classes/java/lang/Process.java      | 34 ++++++++-
 .../java/lang/Process/WaitForDuration.java    | 73 +++++++++++++++++++
 .../jdk/test/lib/process/ProcessTools.java    | 10 +++
 3 files changed, 115 insertions(+), 2 deletions(-)
 create mode 100644 test/jdk/java/lang/Process/WaitForDuration.java

diff --git a/src/java.base/share/classes/java/lang/Process.java b/src/java.base/share/classes/java/lang/Process.java
index 756705285d9..a01a1f93fd2 100644
--- a/src/java.base/share/classes/java/lang/Process.java
+++ b/src/java.base/share/classes/java/lang/Process.java
@@ -32,6 +32,7 @@ import java.io.*;
 import java.lang.ProcessBuilder.Redirect;
 import java.nio.charset.Charset;
 import java.nio.charset.UnsupportedCharsetException;
+import java.time.Duration;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ForkJoinPool;
@@ -475,6 +476,35 @@ public abstract class Process {
         return false;
     }
 
+    /**
+     * Causes the current thread to wait, if necessary, until the
+     * process represented by this {@code Process} object has
+     * terminated, or the specified waiting duration elapses.
+     *
+     * <p>If the process has already terminated then this method returns
+     * immediately with the value {@code true}.  If the process has not
+     * terminated and the duration is not positive, then
+     * this method returns immediately with the value {@code false}.
+     *
+     * <p>The default implementation of this method polls the {@code exitValue}
+     * to check if the process has terminated. Concrete implementations of this
+     * class are strongly encouraged to override this method with a more
+     * efficient implementation.
+     *
+     * @param duration the maximum duration to wait; if not positive,
+     *                this method returns immediately.
+     * @return {@code true} if the process has exited and {@code false} if
+     *         the waiting duration elapsed before the process has exited.
+     * @throws InterruptedException if the current thread is interrupted
+     *         while waiting.
+     * @throws NullPointerException if duration is null
+     * @since 24
+     */
+    public boolean waitFor(Duration duration) throws InterruptedException {
+        Objects.requireNonNull(duration, "duration");
+        return waitFor(TimeUnit.NANOSECONDS.convert(duration), TimeUnit.NANOSECONDS);
+    }
+
     /**
      * Returns the exit value for the process.
      *
@@ -577,8 +607,8 @@ public abstract class Process {
 
     /**
      * This is called from the default implementation of
-     * {@code waitFor(long, TimeUnit)}, which is specified to poll
-     * {@code exitValue()}.
+     * {@code waitFor(long, TimeUnit)} and {@code waitFor(Duration)},
+     * which are specified to poll {@code exitValue()}.
      */
     private boolean hasExited() {
         try {
diff --git a/test/jdk/java/lang/Process/WaitForDuration.java b/test/jdk/java/lang/Process/WaitForDuration.java
new file mode 100644
index 00000000000..d9172b305a0
--- /dev/null
+++ b/test/jdk/java/lang/Process/WaitForDuration.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2024, 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 8336479
+ * @summary Tests for Process.waitFor(Duration)
+ * @library /test/lib
+ * @run junit WaitForDuration
+ */
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.stream.Stream;
+import jdk.test.lib.process.ProcessTools;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class WaitForDuration {
+    static Stream<Arguments> durations() {
+        return Stream.of(
+            Arguments.of(Duration.ZERO, 3_600_000, false),
+            Arguments.of(Duration.ofSeconds(-100), 3_600_000, false),
+            Arguments.of(Duration.ofSeconds(100), 0, true),
+            Arguments.of(Duration.ofSeconds(Long.MAX_VALUE), 0, true), // nano overflow
+            Arguments.of(Duration.ofSeconds(Long.MIN_VALUE), 3_600_000, false) // nano underflow
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("durations")
+    void testEdgeDurations(Duration d, int sleepMillis, boolean expected)
+            throws IOException, InterruptedException {
+        var pb = ProcessTools.createTestJavaProcessBuilder(
+            WaitForDuration.class.getSimpleName(), Integer.toString(sleepMillis));
+        assertEquals(expected, pb.start().waitFor(d));
+    }
+
+    @Test
+    void testNullDuration() throws IOException, InterruptedException {
+        var pb = ProcessTools.createTestJavaProcessBuilder(
+            WaitForDuration.class.getSimpleName(), "0");
+        assertThrows(NullPointerException.class, () -> pb.start().waitFor(null));
+    }
+
+    public static void main(String... args) throws InterruptedException {
+        Thread.sleep(Integer.parseInt(args[0]));
+    }
+}
diff --git a/test/lib/jdk/test/lib/process/ProcessTools.java b/test/lib/jdk/test/lib/process/ProcessTools.java
index aafb6fe3f4c..6ab36c8bfd3 100644
--- a/test/lib/jdk/test/lib/process/ProcessTools.java
+++ b/test/lib/jdk/test/lib/process/ProcessTools.java
@@ -42,6 +42,7 @@ import java.nio.file.Paths;
 import java.security.AccessController;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -965,6 +966,15 @@ public final class ProcessTools {
             return rslt;
         }
 
+        @Override
+        public boolean waitFor(Duration duration) throws InterruptedException {
+            boolean rslt = p.waitFor(duration);
+            if (rslt) {
+                waitForStreams();
+            }
+            return rslt;
+        }
+
         private void waitForStreams() throws InterruptedException {
             try {
                 stdoutTask.get();