Support redirecting input from client to daemon, #541 (#581)

The implementation currently switches on the redirection when the daemon actually starts reading the System.in stream using InputStream.read() or InputStream.available().
This commit is contained in:
Guillaume Nodet
2022-12-13 23:59:35 +01:00
committed by GitHub
parent 2a8d18d119
commit 2b8076d1ed
3 changed files with 178 additions and 3 deletions

View File

@@ -62,6 +62,8 @@ public abstract class Message {
public static final int EXECUTION_FAILURE = 24;
public static final int PRINT_OUT = 25;
public static final int PRINT_ERR = 26;
public static final int REQUEST_INPUT = 27;
public static final int INPUT_DATA = 28;
final int type;
@@ -115,6 +117,10 @@ public abstract class Message {
case PRINT_OUT:
case PRINT_ERR:
return StringMessage.read(type, input);
case REQUEST_INPUT:
return RequestInput.read(input);
case INPUT_DATA:
return InputData.read(input);
}
throw new IllegalStateException("Unexpected message type: " + type);
}
@@ -137,6 +143,8 @@ public abstract class Message {
case DISPLAY:
case PRINT_OUT:
case PRINT_ERR:
case REQUEST_INPUT:
case INPUT_DATA:
return 2;
case PROJECT_STARTED:
return 3;
@@ -1036,6 +1044,66 @@ public abstract class Message {
}
}
public static class RequestInput extends Message {
private String projectId;
public static RequestInput read(DataInputStream input) throws IOException {
String projectId = readUTF(input);
return new RequestInput(projectId);
}
public RequestInput(String projectId) {
super(REQUEST_INPUT);
this.projectId = projectId;
}
public String getProjectId() {
return projectId;
}
@Override
public String toString() {
return "RequestInput{" + "projectId='" + projectId + '\'' + '}';
}
@Override
public void write(DataOutputStream output) throws IOException {
super.write(output);
writeUTF(output, projectId);
}
}
public static class InputData extends Message {
final String data;
public static Message read(DataInputStream input) throws IOException {
String data = readUTF(input);
return new InputData(data);
}
private InputData(String data) {
super(INPUT_DATA);
this.data = data;
}
public String getData() {
return data;
}
@Override
public String toString() {
return "InputResponse{" + "data='" + data + "\'" + '}';
}
@Override
public void write(DataOutputStream output) throws IOException {
super.write(output);
writeUTF(output, data);
}
}
public int getType() {
return type;
}
@@ -1048,6 +1116,14 @@ public abstract class Message {
return new StringMessage(DISPLAY, message);
}
public static RequestInput requestInput(String projectId) {
return new RequestInput(projectId);
}
public static InputData inputResponse(String data) {
return new InputData(data);
}
public static StringMessage out(String message) {
return new StringMessage(PRINT_OUT, message);
}

View File

@@ -53,6 +53,7 @@ import org.mvndaemon.mvnd.common.Message.BuildStarted;
import org.mvndaemon.mvnd.common.Message.ExecutionFailureEvent;
import org.mvndaemon.mvnd.common.Message.MojoStartedEvent;
import org.mvndaemon.mvnd.common.Message.ProjectEvent;
import org.mvndaemon.mvnd.common.Message.RequestInput;
import org.mvndaemon.mvnd.common.Message.StringMessage;
import org.mvndaemon.mvnd.common.Message.TransferEvent;
import org.mvndaemon.mvnd.common.OsUtils;
@@ -112,6 +113,8 @@ public class TerminalOutput implements ClientOutput {
private volatile Consumer<Message> daemonDispatch;
/** A sink for queuing messages to the main queue */
private volatile Consumer<Message> daemonReceive;
/** The project id which is trying to read the input stream */
private volatile String projectReadingInput;
/*
* The following non-final fields are read/written from the main thread only.
@@ -441,6 +444,15 @@ public class TerminalOutput implements ClientOutput {
failures.add(efe);
break;
}
case Message.REQUEST_INPUT: {
RequestInput ri = (RequestInput) entry;
projectReadingInput = ri.getProjectId();
break;
}
case Message.INPUT_DATA: {
daemonDispatch.accept(entry);
break;
}
default:
throw new IllegalStateException("Unexpected message " + entry);
}
@@ -480,7 +492,21 @@ public class TerminalOutput implements ClientOutput {
try {
while (!closing) {
if (readInput.readLock().tryLock(10, TimeUnit.MILLISECONDS)) {
try {
if (projectReadingInput != null) {
char[] buf = new char[256];
int idx = 0;
while (idx < buf.length) {
int c = terminal.reader().read(idx > 0 ? 1 : 10);
if (c < 0) {
break;
}
buf[idx++] = (char) c;
}
if (idx > 0) {
String data = String.valueOf(buf, 0, idx);
daemonReceive.accept(Message.inputResponse(data));
}
} else {
int c = terminal.reader().read(10);
if (c == -1) {
break;
@@ -488,9 +514,8 @@ public class TerminalOutput implements ClientOutput {
if (c == KEY_PLUS || c == KEY_MINUS || c == KEY_CTRL_L || c == KEY_CTRL_M || c == KEY_CTRL_B) {
daemonReceive.accept(Message.keyboardInput((char) c));
}
} finally {
readInput.readLock().unlock();
}
readInput.readLock().unlock();
}
}
} catch (InterruptedException e) {

View File

@@ -25,18 +25,23 @@ import static org.mvndaemon.mvnd.common.DaemonState.StopRequested;
import static org.mvndaemon.mvnd.common.DaemonState.Stopped;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
@@ -47,6 +52,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.maven.cli.DaemonMavenCli;
@@ -482,6 +488,8 @@ public class Server implements AutoCloseable, Runnable {
final BlockingQueue<Message> sendQueue = new PriorityBlockingQueue<>(64, Message.getMessageComparator());
final BlockingQueue<Message> recvQueue = new LinkedBlockingDeque<>();
final BuildEventListener buildEventListener = new ClientDispatcher(sendQueue);
final DaemonInputStream daemonInputStream =
new DaemonInputStream(projectId -> sendQueue.add(Message.requestInput(projectId)));
try (ProjectBuildLogAppender logAppender = new ProjectBuildLogAppender(buildEventListener)) {
LOGGER.info("Executing request");
@@ -529,6 +537,8 @@ public class Server implements AutoCloseable, Runnable {
if (message == Message.BareMessage.CANCEL_BUILD_SINGLETON) {
updateState(Canceled);
return;
} else if (message instanceof Message.InputData) {
daemonInputStream.addInputData(((Message.InputData) message).getData());
} else {
synchronized (recvQueue) {
recvQueue.put(message);
@@ -581,6 +591,7 @@ public class Server implements AutoCloseable, Runnable {
}
}
});
System.setIn(daemonInputStream);
System.setOut(new LoggingOutputStream(s -> sendQueue.add(Message.out(s))).printStream());
System.setErr(new LoggingOutputStream(s -> sendQueue.add(Message.err(s))).printStream());
int exitCode = cli.main(
@@ -650,4 +661,67 @@ public class Server implements AutoCloseable, Runnable {
public String toString() {
return info.toString();
}
static class DaemonInputStream extends InputStream {
private final Consumer<String> startReadingFromProject;
private final LinkedList<byte[]> datas = new LinkedList<>();
private int pos = -1;
private String projectReading = null;
DaemonInputStream(Consumer<String> startReadingFromProject) {
this.startReadingFromProject = startReadingFromProject;
}
@Override
public int available() throws IOException {
synchronized (datas) {
String projectId = ProjectBuildLogAppender.getProjectId();
if (!Objects.equals(projectId, projectReading)) {
projectReading = projectId;
startReadingFromProject.accept(projectId);
}
return datas.stream().mapToInt(a -> a.length).sum() - Math.max(pos, 0);
}
}
@Override
public int read() throws IOException {
synchronized (datas) {
String projectId = ProjectBuildLogAppender.getProjectId();
if (!Objects.equals(projectId, projectReading)) {
projectReading = projectId;
startReadingFromProject.accept(projectId);
// TODO: start a 10ms timer to turn data off
}
for (; ; ) {
if (datas.isEmpty()) {
try {
datas.wait();
} catch (InterruptedException e) {
throw new InterruptedIOException("Interrupted");
}
pos = -1;
continue;
}
byte[] curData = datas.getFirst();
if (pos >= curData.length) {
datas.removeFirst();
pos = -1;
continue;
}
if (pos < 0) {
pos = 0;
}
return curData[pos++];
}
}
}
public void addInputData(String data) {
synchronized (datas) {
datas.add(data.getBytes(Charset.forName(System.getProperty("file.encoding"))));
datas.notifyAll();
}
}
}
}