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 8e6f0a8409a8..fc2f90233b36 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 @@ -104,6 +104,20 @@ public FastTerminal(Callable builder, Consumer consumer) { "fast-terminal-thread"); // a wedged builder must not keep the JVM alive; everything waits on the future, not the thread this.buildThread.setDaemon(true); + } + + /** + * Starts the build thread. Must be called after the caller has published this + * {@code FastTerminal} (e.g. assigned it to {@link MessageUtils#terminal}) so that code running + * on the build thread can obtain a non-null reference through {@link MessageUtils#getTerminal()}. + *

+ * {@link Thread#start()} establishes a happens-before edge, so the assignment made by + * the caller before this method is visible to the build thread without additional + * synchronization. + * + * @see #12912 + */ + public void start() { this.buildThread.start(); } diff --git a/impl/maven-jline/src/main/java/org/apache/maven/jline/MessageUtils.java b/impl/maven-jline/src/main/java/org/apache/maven/jline/MessageUtils.java index 572241e04a49..8f4800c8132d 100644 --- a/impl/maven-jline/src/main/java/org/apache/maven/jline/MessageUtils.java +++ b/impl/maven-jline/src/main/java/org/apache/maven/jline/MessageUtils.java @@ -47,7 +47,11 @@ public static void systemInstall() { } public static void systemInstall(Consumer builderConsumer, Consumer terminalConsumer) { - MessageUtils.terminal = new FastTerminal( + // Assign the FastTerminal to the field BEFORE starting the build thread so that code + // running on that thread (e.g. JLine's FFM provider init, logger calls) sees a non-null + // reference when it calls MessageUtils.getTerminal(). Thread.start() provides the + // happens-before edge that makes the assignment visible to the new thread. + FastTerminal ft = new FastTerminal( () -> { TerminalBuilder builder = TerminalBuilder.builder().name("Maven").dumb(true); @@ -64,6 +68,8 @@ public static void systemInstall(Consumer builderConsumer, Cons terminalConsumer.accept(terminal); } }); + MessageUtils.terminal = ft; + ft.start(); } private static LineReader createReader(Terminal terminal) { 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 f6d1e2e26a40..aee9ececb8ed 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 @@ -122,6 +122,26 @@ void arbitraryTerminalMethodsFromTheConsumerDoNotDeadlock() { }); } + /** + * Verifies that {@link MessageUtils#getTerminal()} is non-null when called from the builder + * callback. Before the fix, the {@code FastTerminal} constructor started its build thread + * before returning, so {@code MessageUtils.terminal} was still {@code null} when the build + * thread ran the builder callback — a race between the constructor returning and the + * thread scheduling. After the fix, {@code MessageUtils} assigns the field before calling + * {@link FastTerminal#start()}, and {@link Thread#start()} provides the happens-before edge. + * + * @see #12912 + */ + @Test + void terminalAssignmentIsVisibleFromBuilderCallback() { + assertTimeoutPreemptively(Duration.ofSeconds(30), () -> { + CompletableFuture observed = new CompletableFuture<>(); + installAndAwait(builder -> observed.complete(MessageUtils.getTerminal()), terminal -> {}); + assertNotNull(observed.get(), "MessageUtils.getTerminal() must not return null from the builder callback"); + assertTrue(observed.get() instanceof FastTerminal, "terminal should be the FastTerminal wrapper"); + }); + } + /** * Both terminal calls a single log statement makes, run on the terminal building thread. */