You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/10/14 17:46:51 UTC

(maven-mvnd) 01/01: [#1834] Pump forked process output when only one stream is inherited

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch agent/agent-single-inherit-1x
in repository https://gitbox.apache.org/repos/asf/maven-mvnd.git

commit 7cbc61357dda6f021eaffe2b92b93cd07731bf4f
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Wed Oct 14 17:03:43 2026 +0200

    [#1834] Pump forked process output when only one stream is inherited
    
    The agent only switched INHERIT to PIPE when both stdout and stderr
    inherited, so a child inheriting a single stream wrote to the daemon log.
    It now works on a copy of the redirects, so the ProcessBuilder keeps
    reporting what the caller set.
    
    (cherry picked from commit 3279a4b715d2cec4e781ca7491925486e3ec1683)
---
 .../main/java/org/mvndaemon/mvnd/agent/Agent.java  | 17 +++++---
 .../java/org/mvndaemon/mvnd/pump/AgentHelper.java  |  9 +++--
 .../java/org/mvndaemon/mvnd/it/ExecOutputTest.java | 12 ++++++
 .../src/test/projects/exec-output/pom.xml          | 10 +++++
 .../org/mvndaemon/mvnd/test/InheritStderrOnly.java | 46 ++++++++++++++++++++++
 .../java/org/mvndaemon/mvnd/test/StderrChild.java  | 16 ++------
 6 files changed, 88 insertions(+), 22 deletions(-)

diff --git a/agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java b/agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java
index 2109f5b3..bad0b037 100644
--- a/agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java
+++ b/agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java
@@ -28,12 +28,17 @@ import javassist.CtClass;
 public class Agent {
 
     public static final String START_WITH_PIPES = "if (redirects != null\n"
-            + "         && redirects[1] == ProcessBuilder$Redirect.INHERIT\n"
-            + "         && redirects[2] == ProcessBuilder$Redirect.INHERIT) {\n"
-            + "   redirects[1] = redirects[2] = ProcessBuilder$Redirect.PIPE;"
-            + "   Process p = start(redirects);\n"
-            + "   AgentHelper.pump(p.getInputStream(), System.out);\n"
-            + "   AgentHelper.pump(p.getErrorStream(), System.err);\n"
+            + "         && (redirects[1] == ProcessBuilder$Redirect.INHERIT\n"
+            + "             || redirects[2] == ProcessBuilder$Redirect.INHERIT)) {\n"
+            + "   ProcessBuilder$Redirect[] piped = (ProcessBuilder$Redirect[])\n"
+            + "           java.util.Arrays.copyOf(redirects, redirects.length);\n"
+            + "   boolean pumpOut = piped[1] == ProcessBuilder$Redirect.INHERIT;\n"
+            + "   boolean pumpErr = piped[2] == ProcessBuilder$Redirect.INHERIT;\n"
+            + "   if (pumpOut) piped[1] = ProcessBuilder$Redirect.PIPE;\n"
+            + "   if (pumpErr) piped[2] = ProcessBuilder$Redirect.PIPE;\n"
+            + "   Process p = start(piped);\n"
+            + "   if (pumpOut) AgentHelper.pump(p.getInputStream(), System.out);\n"
+            + "   if (pumpErr) AgentHelper.pump(p.getErrorStream(), System.err);\n"
             + "   return p;\n"
             + "}";
 
diff --git a/helper/src/main/java/org/mvndaemon/mvnd/pump/AgentHelper.java b/helper/src/main/java/org/mvndaemon/mvnd/pump/AgentHelper.java
index 95cf5534..d1a61d2f 100644
--- a/helper/src/main/java/org/mvndaemon/mvnd/pump/AgentHelper.java
+++ b/helper/src/main/java/org/mvndaemon/mvnd/pump/AgentHelper.java
@@ -26,9 +26,10 @@ import java.io.PrintStream;
 public class AgentHelper {
 
     public static void pump(InputStream stream, PrintStream out) {
-        new Thread(() -> new BufferedReader(new InputStreamReader(stream))
-                        .lines()
-                        .forEach(out::println))
-                .start();
+        Thread pump = new Thread(
+                () -> new BufferedReader(new InputStreamReader(stream)).lines().forEach(out::println),
+                "mvnd-process-pump");
+        pump.setDaemon(true);
+        pump.start();
     }
 }
diff --git a/integration-tests/src/test/java/org/mvndaemon/mvnd/it/ExecOutputTest.java b/integration-tests/src/test/java/org/mvndaemon/mvnd/it/ExecOutputTest.java
index 0bc94bd0..07e8d86b 100644
--- a/integration-tests/src/test/java/org/mvndaemon/mvnd/it/ExecOutputTest.java
+++ b/integration-tests/src/test/java/org/mvndaemon/mvnd/it/ExecOutputTest.java
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import org.mvndaemon.mvnd.assertj.TestClientOutput;
 import org.mvndaemon.mvnd.client.Client;
 import org.mvndaemon.mvnd.client.DaemonParameters;
+import org.mvndaemon.mvnd.common.Message;
 import org.mvndaemon.mvnd.junit.MvndTest;
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -48,4 +49,15 @@ class ExecOutputTest {
         assertTrue(output.messagesToString()
                 .contains("ProjectLogMessage{projectId='exec-output', message='[INFO] [stdout] Hello world!'}"));
     }
+
+    @Test
+    void cleanTestInheritStderrOnly() throws InterruptedException {
+
+        final TestClientOutput output = new TestClientOutput();
+        client.execute(output, "clean", "test", "-e", "-Dmvnd.log.level=DEBUG").assertSuccess();
+        assertTrue(output.getMessages().stream()
+                .filter(Message.ProjectEvent.class::isInstance)
+                .map(Message.ProjectEvent.class::cast)
+                .anyMatch(it -> it.getMessage().contains("From child stderr")));
+    }
 }
diff --git a/integration-tests/src/test/projects/exec-output/pom.xml b/integration-tests/src/test/projects/exec-output/pom.xml
index 393768fa..e243345c 100644
--- a/integration-tests/src/test/projects/exec-output/pom.xml
+++ b/integration-tests/src/test/projects/exec-output/pom.xml
@@ -93,6 +93,16 @@
                             </arguments>
                         </configuration>
                     </execution>
+                    <execution>
+                        <id>run-inherit-stderr-only</id>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>java</goal>
+                        </goals>
+                        <configuration>
+                            <mainClass>org.mvndaemon.mvnd.test.InheritStderrOnly</mainClass>
+                        </configuration>
+                    </execution>
                 </executions>
             </plugin>
         </plugins>
diff --git a/integration-tests/src/test/projects/exec-output/src/main/java/org/mvndaemon/mvnd/test/InheritStderrOnly.java b/integration-tests/src/test/projects/exec-output/src/main/java/org/mvndaemon/mvnd/test/InheritStderrOnly.java
new file mode 100644
index 00000000..cd375cb3
--- /dev/null
+++ b/integration-tests/src/test/projects/exec-output/src/main/java/org/mvndaemon/mvnd/test/InheritStderrOnly.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.mvndaemon.mvnd.test;
+
+import java.io.File;
+
+/**
+ * Forks a JVM that inherits only stderr, so the daemon must pump that one stream to the client.
+ */
+public class InheritStderrOnly {
+
+    public static void main(String[] args) throws Exception {
+        String java = new File(new File(System.getProperty("java.home"), "bin"), "java").getPath();
+        String classes = new File(InheritStderrOnly.class
+                        .getProtectionDomain()
+                        .getCodeSource()
+                        .getLocation()
+                        .toURI())
+                .getPath();
+        Process child = new ProcessBuilder(java, "-cp", classes, StderrChild.class.getName())
+                .redirectError(ProcessBuilder.Redirect.INHERIT)
+                .start();
+        int exit = child.waitFor();
+        if (exit != 0) {
+            throw new IllegalStateException("Child exited with " + exit);
+        }
+        // the pump thread forwards the child's output asynchronously
+        Thread.sleep(1000);
+    }
+}
diff --git a/helper/src/main/java/org/mvndaemon/mvnd/pump/AgentHelper.java b/integration-tests/src/test/projects/exec-output/src/main/java/org/mvndaemon/mvnd/test/StderrChild.java
similarity index 65%
copy from helper/src/main/java/org/mvndaemon/mvnd/pump/AgentHelper.java
copy to integration-tests/src/test/projects/exec-output/src/main/java/org/mvndaemon/mvnd/test/StderrChild.java
index 95cf5534..290a291a 100644
--- a/helper/src/main/java/org/mvndaemon/mvnd/pump/AgentHelper.java
+++ b/integration-tests/src/test/projects/exec-output/src/main/java/org/mvndaemon/mvnd/test/StderrChild.java
@@ -16,19 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.mvndaemon.mvnd.pump;
+package org.mvndaemon.mvnd.test;
 
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.PrintStream;
+public class StderrChild {
 
-public class AgentHelper {
-
-    public static void pump(InputStream stream, PrintStream out) {
-        new Thread(() -> new BufferedReader(new InputStreamReader(stream))
-                        .lines()
-                        .forEach(out::println))
-                .start();
+    public static void main(String[] args) {
+        System.err.println("From child stderr");
     }
 }