You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/09/14 11:21:37 UTC
(groovy) 18/20: GROOVY-12410: groovysh `/!` can lose a shell command's output
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch GROOVY_6_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit c20e8d4c1f98515c1fdd540417eff1276683b02a
Author: Paul King <pa...@asert.com.au>
AuthorDate: Mon Sep 14 19:08:30 2026 +1000
GROOVY-12410: groovysh `/!` can lose a shell command's output
executeCommand started the two stream-reader threads and then waited for
the process, but nothing waited for the threads:
proc.consumeProcessOutput(sout, serr) // starts the readers
int exitCode = proc.waitFor() // waits for the process only
if (sout.size()) print sout // may run before they append
consumeProcessOutput starts the readers and returns; waitFor waits for
the process to exit, which can happen while output is still sitting in
the pipe unread. The command could therefore print nothing for a shell
command that produced output. waitForProcessOutput joins both readers
and then waits for the process, which is the guarantee this code needs;
its javadoc makes the distinction explicit. exitValue is safe after it,
since the process has finished.
This is the only occurrence of the shape in product code. The two in
`WorkingWithIOSpecTest` are the documented drain-to-avoid-blocking
idiom, where the output is discarded, so they are correct as they stand.
The failure seen in CI is `ExtraConsoleCommandsTest.bangRunsAPortableEcho`
asserting on an empty buffer, intermittently and on a different JDK each
time. That is consistent with this race, but it did not reproduce
locally, so the fix is made on the strength of the code being wrong
rather than on a demonstrated repro.
---
.../groovy/org/apache/groovy/groovysh/ExtraConsoleCommands.groovy | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/ExtraConsoleCommands.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/ExtraConsoleCommands.groovy
index f0acc3eead..56d1cc555a 100644
--- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/ExtraConsoleCommands.groovy
+++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/ExtraConsoleCommands.groovy
@@ -226,8 +226,11 @@ class ExtraConsoleCommands extends JlineCommandRegistry implements CommandRegist
def sout = new StringBuilder(), serr = new StringBuilder()
def command = OSUtils.IS_WINDOWS ? ['cmd.exe', '/c'] : ['sh', '-c']
def proc = new ProcessBuilder().command(command + args.join(' ')).start()
- proc.consumeProcessOutput(sout, serr)
- int exitCode = proc.waitFor()
+ // waitForProcessOutput joins the two reader threads before returning;
+ // consumeProcessOutput only starts them, and waitFor waits for the
+ // process rather than for them, so the output could still be unread
+ proc.waitForProcessOutput(sout, serr)
+ int exitCode = proc.exitValue()
if (sout.size()) print sout
if (exitCode != 0) {
if (serr.size()) print serr