FastTerminal.java
/*
* 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.apache.maven.jline;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
import java.util.function.IntSupplier;
import org.apache.maven.api.services.MavenException;
import org.jline.terminal.Attributes;
import org.jline.terminal.Cursor;
import org.jline.terminal.MouseEvent;
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;
import org.jline.utils.ColorPalette;
import org.jline.utils.InfoCmp;
import org.jline.utils.NonBlockingReader;
public class FastTerminal implements TerminalExt {
private final CompletableFuture<Terminal> terminal;
/**
* The thread running the builder and the consumer. Every method of this class delegates through
* {@link #getTerminal()}, so code running on this thread before the terminal is published would
* wait on the very future it is itself computing.
*/
private final Thread buildThread;
/**
* Writer handed to {@link #buildThread} while the real terminal is still being built. Created on
* demand, and only ever by {@link #buildThread}.
*/
private PrintWriter fallbackWriter;
/**
* Captured before {@link #buildThread} starts, hence before the consumer swaps the system streams
* for logging-backed ones. Writing to the live {@code System.err} instead would feed the fallback
* output back into the logger it came from.
*/
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 {
Terminal term = builder.call();
consumer.accept(term);
terminal.complete(term);
} catch (Exception e) {
terminal.completeExceptionally(new MavenException(e));
}
},
"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 <em>after</em> 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()}.
* <p>
* {@link Thread#start()} establishes a <em>happens-before</em> edge, so the assignment made by
* the caller before this method is visible to the build thread without additional
* synchronization.
*
* @see <a href="https://github.com/apache/maven/issues/12912">#12912</a>
*/
public void start() {
this.buildThread.start();
}
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) {
throw new RuntimeException(e);
}
}
/**
* True when the caller is the thread building the terminal and the terminal is not published yet.
* Waiting for the future here would never return, since this thread is the one that completes it.
*/
private boolean isBuildThreadWaitingOnItself() {
return Thread.currentThread() == buildThread && !isBuilt();
}
/**
* Whether the terminal has been published. {@link MessageUtils#systemUninstall()} waits for the
* build to finish, so a caller that must not block has to check this first.
*/
boolean isBuilt() {
return terminal.isDone();
}
private PrintWriter fallbackWriter() {
// only buildThread reaches this, so no synchronization is needed
if (fallbackWriter == null) {
fallbackWriter = new PrintWriter(new OutputStreamWriter(fallbackOutput, Charset.defaultCharset()), true);
}
return fallbackWriter;
}
@Override
public String getName() {
return getTerminal().getName();
}
@Override
public SignalHandler handle(Signal signal, SignalHandler signalHandler) {
return getTerminal().handle(signal, signalHandler);
}
@Override
public void raise(Signal signal) {
getTerminal().raise(signal);
}
@Override
public NonBlockingReader reader() {
return getTerminal().reader();
}
@Override
public PrintWriter writer() {
// the log sink writes here, and must not wait for the terminal this thread is building
return isBuildThreadWaitingOnItself() ? fallbackWriter() : getTerminal().writer();
}
@Override
public Charset encoding() {
return getTerminal().encoding();
}
@Override
public InputStream input() {
return getTerminal().input();
}
@Override
public OutputStream output() {
return getTerminal().output();
}
@Override
public boolean canPauseResume() {
return getTerminal().canPauseResume();
}
@Override
public void pause() {
getTerminal().pause();
}
@Override
public void pause(boolean b) throws InterruptedException {
getTerminal().pause(b);
}
@Override
public void resume() {
getTerminal().resume();
}
@Override
public boolean paused() {
return getTerminal().paused();
}
@Override
public Attributes enterRawMode() {
return getTerminal().enterRawMode();
}
@Override
public boolean echo() {
return getTerminal().echo();
}
@Override
public boolean echo(boolean b) {
return getTerminal().echo(b);
}
@Override
public Attributes getAttributes() {
return getTerminal().getAttributes();
}
@Override
public void setAttributes(Attributes attributes) {
getTerminal().setAttributes(attributes);
}
@Override
public Size getSize() {
return getTerminal().getSize();
}
@Override
public void setSize(Size size) {
getTerminal().setSize(size);
}
@Override
public void setSize(Sized sized) {
getTerminal().setSize(sized);
}
@Override
public int getWidth() {
return getTerminal().getWidth();
}
@Override
public int getHeight() {
return getTerminal().getHeight();
}
@Override
public Size getBufferSize() {
return getTerminal().getBufferSize();
}
@Override
public void flush() {
getTerminal().flush();
}
@Override
public String getType() {
// AttributedCharSequence.toAnsi asks for the type first and renders plain for a dumb one,
// so answering here keeps message rendering off the terminal this thread is building
return isBuildThreadWaitingOnItself()
? Terminal.TYPE_DUMB
: getTerminal().getType();
}
@Override
public boolean puts(InfoCmp.Capability capability, Object... objects) {
return getTerminal().puts(capability, objects);
}
@Override
public boolean getBooleanCapability(InfoCmp.Capability capability) {
return getTerminal().getBooleanCapability(capability);
}
@Override
public Integer getNumericCapability(InfoCmp.Capability capability) {
return getTerminal().getNumericCapability(capability);
}
@Override
public String getStringCapability(InfoCmp.Capability capability) {
return getTerminal().getStringCapability(capability);
}
@Override
public Cursor getCursorPosition(IntConsumer intConsumer) {
return getTerminal().getCursorPosition(intConsumer);
}
@Override
public boolean hasMouseSupport() {
return getTerminal().hasMouseSupport();
}
@Override
public MouseTracking getCurrentMouseTracking() {
return getTerminal().getCurrentMouseTracking();
}
@Override
public boolean trackMouse(MouseTracking mouseTracking) {
return getTerminal().trackMouse(mouseTracking);
}
@Override
public MouseEvent readMouseEvent() {
return getTerminal().readMouseEvent();
}
@Override
public MouseEvent readMouseEvent(IntSupplier intSupplier) {
return getTerminal().readMouseEvent(intSupplier);
}
@Override
public MouseEvent readMouseEvent(String prefix) {
return getTerminal().readMouseEvent(prefix);
}
@Override
public MouseEvent readMouseEvent(IntSupplier reader, String prefix) {
return getTerminal().readMouseEvent(reader, prefix);
}
@Override
public boolean hasFocusSupport() {
return getTerminal().hasFocusSupport();
}
@Override
public boolean trackFocus(boolean b) {
return getTerminal().trackFocus(b);
}
@Override
public ColorPalette getPalette() {
return getTerminal().getPalette();
}
@Override
public void close() throws IOException {
getTerminal().close();
}
@Override
public TerminalProvider getProvider() {
return getTerminal().getProvider();
}
@Override
public SystemStream getSystemStream() {
return getTerminal().getSystemStream();
}
}