You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/30 11:46:33 UTC
(maven) branch forward-port/12912 created (now a9eefe3512)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a change to branch forward-port/12912
in repository https://gitbox.apache.org/repos/asf/maven.git
at a9eefe3512 Fix #12912: use a dumb fallback terminal for all build-thread reentrant calls (#12921)
This branch includes the following new commits:
new a9eefe3512 Fix #12912: use a dumb fallback terminal for all build-thread reentrant calls (#12921)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven) 01/01: Fix #12912: use a dumb fallback terminal for all build-thread reentrant calls (#12921)
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch forward-port/12912
in repository https://gitbox.apache.org/repos/asf/maven.git
commit a9eefe351217986619f07390ad30704c8297faa1
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Sun Aug 30 13:45:26 2026 +0200
Fix #12912: use a dumb fallback terminal for all build-thread reentrant calls (#12921)
The previous fix for #12761 guarded only writer() and getType() against
reentrant calls from the build thread, because those were the two methods
a log statement exercises. JLine 4.4.0's FFM provider initialization
(CLibrary.<clinit>) can reach back through other terminal methods
(getSize, getName, encoding, etc.) on the build thread, causing the same
deadlock on JDK 25 in container environments.
Construct a DumbTerminal before the build thread starts and return it from
getTerminal() when the build thread would otherwise wait on its own future.
This covers every delegate method at once, regardless of which call path
JLine's provider probing takes. The DumbTerminal is constructed directly
rather than via TerminalBuilder, which probes for grapheme-cluster support
and can block on a null input stream in JLine 4.4.0.
The existing writer() and getType() guards remain for explicitness: they
take priority (checked before getTerminal()) and guarantee the fallback
writer uses the pre-captured System.err.
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
.../java/org/apache/maven/jline/FastTerminal.java | 26 ++++++++++++
.../maven/jline/FastTerminalReentrancyTest.java | 47 +++++++++++++++++++++-
2 files changed, 72 insertions(+), 1 deletion(-)
diff --git a/impl/maven-jline/src/main/java/org/apache/maven/jline/FastTerminal.java b/impl/maven-jline/src/main/java/org/apache/maven/jline/FastTerminal.java
index 2cd81f9289..8e6f0a8409 100644
--- a/impl/maven-jline/src/main/java/org/apache/maven/jline/FastTerminal.java
+++ b/impl/maven-jline/src/main/java/org/apache/maven/jline/FastTerminal.java
@@ -37,6 +37,7 @@
import org.jline.terminal.Size;
import org.jline.terminal.Sized;
import org.jline.terminal.Terminal;
+import org.jline.terminal.impl.DumbTerminal;
import org.jline.terminal.spi.SystemStream;
import org.jline.terminal.spi.TerminalExt;
import org.jline.terminal.spi.TerminalProvider;
@@ -68,9 +69,28 @@ public class FastTerminal implements TerminalExt {
*/
private final OutputStream fallbackOutput;
+ /**
+ * A dumb terminal constructed <em>before</em> the build thread starts, used as a stand-in for
+ * every delegate method when the build thread would otherwise wait on its own future. Guarding
+ * only {@code writer()} and {@code getType()} was sufficient for JLine 4.3.x, but JLine 4.4.0's
+ * FFM provider initialization ({@code CLibrary.<clinit>}) reaches back through other terminal
+ * methods, so the fallback must cover all of them. See
+ * <a href="https://github.com/apache/maven/issues/12912">#12912</a>.
+ */
+ private final TerminalExt fallbackTerminal;
+
public FastTerminal(Callable<Terminal> builder, Consumer<Terminal> consumer) {
this.terminal = new CompletableFuture<>();
this.fallbackOutput = System.err;
+ TerminalExt dumbFallback;
+ try {
+ // Construct a DumbTerminal directly instead of going through TerminalBuilder, which
+ // probes for grapheme-cluster support and can block on a null input stream in JLine 4.4.0.
+ dumbFallback = new DumbTerminal(InputStream.nullInputStream(), fallbackOutput);
+ } catch (IOException e) {
+ dumbFallback = null;
+ }
+ this.fallbackTerminal = dumbFallback;
this.buildThread = new Thread(
() -> {
try {
@@ -88,6 +108,12 @@ public FastTerminal(Callable<Terminal> builder, Consumer<Terminal> consumer) {
}
public TerminalExt getTerminal() {
+ if (isBuildThreadWaitingOnItself()) {
+ if (fallbackTerminal != null) {
+ return fallbackTerminal;
+ }
+ throw new IllegalStateException("Terminal not yet available (build in progress on this thread)");
+ }
try {
return (TerminalExt) terminal.get();
} catch (Exception e) {
diff --git a/impl/maven-jline/src/test/java/org/apache/maven/jline/FastTerminalReentrancyTest.java b/impl/maven-jline/src/test/java/org/apache/maven/jline/FastTerminalReentrancyTest.java
index 0fa35e8815..f6d1e2e26a 100644
--- a/impl/maven-jline/src/test/java/org/apache/maven/jline/FastTerminalReentrancyTest.java
+++ b/impl/maven-jline/src/test/java/org/apache/maven/jline/FastTerminalReentrancyTest.java
@@ -32,16 +32,23 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link MessageUtils#systemInstall} publishes the terminal before the background thread has built
* it, so anything that thread logs is rendered through a terminal that same thread is still
- * producing. See <a href="https://github.com/apache/maven/issues/12761">#12761</a>.
+ * producing. See <a href="https://github.com/apache/maven/issues/12761">#12761</a> and
+ * <a href="https://github.com/apache/maven/issues/12912">#12912</a>.
* <p>
* A single log statement asks the terminal for two things, its type while rendering the message and
* its writer while emitting the line, so both are exercised from both halves of the window: the
* builder callable, and the consumer that runs before the terminal is published.
* <p>
+ * JLine 4.4.0's FFM provider initialization ({@code CLibrary.<clinit>}) can reach back through
+ * other terminal methods (e.g. {@code getName()}, {@code getWidth()}, {@code encoding()}) on the
+ * build thread, so the fallback must cover all delegate methods, not just {@code writer()} and
+ * {@code getType()}.
+ * <p>
* The timeouts are preemptive on purpose: a regression parks the build thread forever, and only an
* abandoning timeout turns that into a red test rather than a hung fork.
*/
@@ -77,6 +84,44 @@ void usingTheTerminalFromTheConsumerDoesNotDeadlock() {
});
}
+ /**
+ * Exercises terminal methods beyond {@code writer()} and {@code getType()} that JLine's FFM
+ * provider initialization can reach on the build thread. Before the fallback terminal was added,
+ * these would deadlock. See <a href="https://github.com/apache/maven/issues/12912">#12912</a>.
+ */
+ @Test
+ void arbitraryTerminalMethodsFromTheBuilderDoNotDeadlock() {
+ assertTimeoutPreemptively(Duration.ofSeconds(30), () -> {
+ CompletableFuture<int[]> probed = new CompletableFuture<>();
+ installAndAwait(
+ builder -> {
+ Terminal t = MessageUtils.getTerminal();
+ probed.complete(
+ new int[] {t.getSize().getColumns(), t.getSize().getRows()});
+ },
+ terminal -> {});
+ int[] dims = probed.get();
+ assertTrue(dims[0] >= 0, "width should be non-negative");
+ assertTrue(dims[1] >= 0, "height should be non-negative");
+ });
+ }
+
+ /**
+ * Exercises terminal methods beyond {@code writer()} and {@code getType()} from the consumer
+ * callback. See <a href="https://github.com/apache/maven/issues/12912">#12912</a>.
+ */
+ @Test
+ void arbitraryTerminalMethodsFromTheConsumerDoNotDeadlock() {
+ assertTimeoutPreemptively(Duration.ofSeconds(30), () -> {
+ CompletableFuture<String> probed = new CompletableFuture<>();
+ installAndAwait(builder -> {}, terminal -> {
+ Terminal t = MessageUtils.getTerminal();
+ probed.complete(t.getName());
+ });
+ assertNotNull(probed.get());
+ });
+ }
+
/**
* Both terminal calls a single log statement makes, run on the terminal building thread.
*/