Refactor input stream support (fixes #1257) (#1268)

This commit is contained in:
Guillaume Nodet
2025-02-17 23:12:16 +01:00
committed by GitHub
parent 4f4e05f973
commit 9e32662401
11 changed files with 668 additions and 182 deletions

View File

@@ -1054,30 +1054,38 @@ public abstract class Message {
public static class RequestInput extends Message {
private String projectId;
private int bytesToRead;
public static RequestInput read(DataInputStream input) throws IOException {
String projectId = readUTF(input);
return new RequestInput(projectId);
int bytesToRead = input.readInt();
return new RequestInput(projectId, bytesToRead);
}
public RequestInput(String projectId) {
public RequestInput(String projectId, int bytesToRead) {
super(REQUEST_INPUT);
this.projectId = projectId;
this.bytesToRead = bytesToRead;
}
public String getProjectId() {
return projectId;
}
public int getBytesToRead() {
return bytesToRead;
}
@Override
public String toString() {
return "RequestInput{" + "projectId='" + projectId + '\'' + '}';
return "RequestInput{" + "projectId='" + projectId + '\'' + ", bytesToRead=" + bytesToRead + '}';
}
@Override
public void write(DataOutputStream output) throws IOException {
super.write(output);
writeUTF(output, projectId);
output.writeInt(bytesToRead);
}
}
@@ -1099,9 +1107,13 @@ public abstract class Message {
return data;
}
public boolean isEof() {
return data == null;
}
@Override
public String toString() {
return "InputResponse{" + "data='" + data + "\'" + '}';
return "InputResponse{" + (data == null ? "eof" : "data='" + data + "'") + '}';
}
@Override
@@ -1119,14 +1131,18 @@ public abstract class Message {
return new StringMessage(BUILD_STATUS, payload);
}
public static RequestInput requestInput(String projectId) {
return new RequestInput(projectId);
public static RequestInput requestInput(String projectId, int bytesToRead) {
return new RequestInput(projectId, bytesToRead);
}
public static InputData inputResponse(String data) {
return new InputData(data);
}
public static InputData inputEof() {
return new InputData(null);
}
public static StringMessage out(String message) {
return new StringMessage(PRINT_OUT, message);
}

View File

@@ -0,0 +1,244 @@
/*
* 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.mvndaemon.mvnd.common.logging;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.jline.terminal.Terminal;
import org.mvndaemon.mvnd.common.Message;
/**
* Handles terminal input in a clean, thread-safe manner using a producer-consumer pattern.
*
* This class is responsible for:
* 1. Reading input from the terminal based on different types of requests:
* - Project input: Reading raw input for a specific project
* - Prompts: Handling interactive prompts with user feedback
* - Control keys: Monitoring for special control keys in non-dumb terminals
*
* 2. Managing input state through InputRequest objects which specify:
* - The type of input needed (project input, prompt, or control keys)
* - The project requiring input
* - How many bytes to read
*
* 3. Converting input to appropriate Message objects and sending them to either:
* - daemonDispatch: for prompt responses
* - daemonReceive: for project input and control keys
*
* The class detects end-of-stream conditions (EOF) and communicates them back through
* the message system, which is crucial for handling piped input (e.g., cat file | mvnd ...).
*
* Input handling differs based on terminal type:
* - Normal terminals: Handle all input types including control keys
* - Dumb terminals: Only handle project input and prompts, ignore control keys
*/
public class TerminalInputHandler implements AutoCloseable {
private final Terminal terminal;
private final BlockingQueue<InputRequest> inputRequests;
private volatile boolean closing;
private final Thread inputThread;
private final boolean dumb;
private volatile int maxThreads;
private volatile Consumer<Message> daemonDispatch;
private volatile Consumer<Message> daemonReceive;
private static class InputRequest {
final String projectId; // null for control keys
final Message.Prompt prompt; // non-null only for prompt requests
final boolean isControlKey; // true for control key listening
final int bytesToRead; // max number of bytes to read
private InputRequest(String projectId, Message.Prompt prompt, boolean isControlKey, int bytesToRead) {
this.projectId = projectId;
this.prompt = prompt;
this.isControlKey = isControlKey;
this.bytesToRead = bytesToRead;
}
static InputRequest forProject(String projectId, int bytesToRead) {
return new InputRequest(projectId, null, false, bytesToRead);
}
static InputRequest forPrompt(Message.Prompt prompt) {
return new InputRequest(prompt.getProjectId(), prompt, false, 0);
}
static InputRequest forControlKeys() {
return new InputRequest(null, null, true, 0);
}
}
public TerminalInputHandler(Terminal terminal, boolean dumb) {
this.terminal = terminal;
this.inputRequests = new LinkedBlockingQueue<>();
this.dumb = dumb;
// Always create input thread as we always need to handle prompts and project input
this.inputThread = new Thread(() -> {
try {
while (!closing) {
InputRequest request = inputRequests.poll(10, TimeUnit.MILLISECONDS);
if (request == null) {
// No active request
if (!dumb) {
// Only listen for control keys in non-dumb mode
handleControlKeys();
}
} else if (request.prompt != null) {
// Always handle prompts
handlePrompt(request.prompt);
} else if (request.projectId != null) {
// Always handle project input
handleProjectInput(request.projectId, request.bytesToRead);
} else if (!dumb && request.isControlKey) {
// Only handle control keys in non-dumb mode
handleControlKeys();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
// Handle terminal IO exception
}
});
inputThread.setDaemon(true);
inputThread.start();
}
private void handleProjectInput(String projectId, int bytesToRead) throws IOException {
if (daemonReceive == null) {
return;
}
char[] buf = bytesToRead > 0 ? new char[bytesToRead] : new char[8192];
int idx = 0;
int timeout = 10; // Initial timeout for first read
while ((bytesToRead < 0 || idx < bytesToRead) && idx < buf.length) {
int c = terminal.reader().read(timeout);
if (c < 0) {
// End of stream reached
daemonReceive.accept(Message.inputEof());
break;
}
buf[idx++] = (char) c;
timeout = idx > 0 ? 1 : 10; // Shorter timeout after first char
}
if (idx > 0) {
String data = String.valueOf(buf, 0, idx);
daemonReceive.accept(Message.inputResponse(data));
}
}
private void handleControlKeys() throws IOException {
if (daemonReceive == null) {
return;
}
int c = terminal.reader().read(10);
if (c != -1 && isControlKey(c)) {
daemonReceive.accept(Message.keyboardInput((char) c));
}
}
private void handlePrompt(Message.Prompt prompt) throws IOException {
if (daemonDispatch == null) {
return;
}
if (prompt.getMessage() != null) {
String msg = formatPromptMessage(prompt);
terminal.writer().print(msg);
}
terminal.flush();
StringBuilder sb = new StringBuilder();
while (true) {
int c = terminal.reader().read();
if (c < 0) {
break;
} else if (c == '\n' || c == '\r') {
terminal.writer().println();
daemonDispatch.accept(prompt.response(sb.toString()));
break;
} else if (c == 127) { // Backspace
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
terminal.writer().write("\b \b");
terminal.writer().flush();
}
} else {
terminal.writer().print((char) c);
terminal.writer().flush();
sb.append((char) c);
}
}
// After prompt is handled, go back to control key listening only if not dumb
if (!dumb) {
inputRequests.offer(InputRequest.forControlKeys());
}
}
private boolean isControlKey(int c) {
return c == TerminalOutput.KEY_PLUS
|| c == TerminalOutput.KEY_MINUS
|| c == TerminalOutput.KEY_CTRL_L
|| c == TerminalOutput.KEY_CTRL_M
|| c == TerminalOutput.KEY_CTRL_B;
}
private String formatPromptMessage(Message.Prompt prompt) {
return (maxThreads > 1)
? String.format("[%s] %s", prompt.getProjectId(), prompt.getMessage())
: prompt.getMessage();
}
public void setDaemonDispatch(Consumer<Message> daemonDispatch) {
this.daemonDispatch = daemonDispatch;
}
public void setDaemonReceive(Consumer<Message> daemonReceive) {
this.daemonReceive = daemonReceive;
}
public void setMaxThreads(int maxThreads) {
this.maxThreads = maxThreads;
}
public void requestProjectInput(String projectId, int bytesToRead) {
inputRequests.clear(); // Clear any pending requests
inputRequests.offer(InputRequest.forProject(projectId, bytesToRead));
}
public void requestPrompt(Message.Prompt prompt) {
inputRequests.clear(); // Clear any pending requests
inputRequests.offer(InputRequest.forPrompt(prompt));
}
@Override
public void close() {
closing = true;
if (inputThread != null) {
inputThread.interrupt();
}
}
}

View File

@@ -19,7 +19,6 @@
package org.mvndaemon.mvnd.common.logging;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -32,9 +31,6 @@ import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.stream.Collector;
import java.util.stream.Collectors;
@@ -102,19 +98,16 @@ public class TerminalOutput implements ClientOutput {
private final ArrayList<ExecutionFailureEvent> failures = new ArrayList<>();
private final LinkedHashMap<String, Project> projects = new LinkedHashMap<>();
private final ClientLog log;
private final Thread reader;
private volatile Exception exception;
private volatile boolean closing;
private final long start;
private final ReadWriteLock readInput = new ReentrantReadWriteLock();
private final boolean dumb;
private final TerminalInputHandler inputHandler;
/** A sink for sending messages back to the daemon */
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.
@@ -173,14 +166,7 @@ public class TerminalOutput implements ClientOutput {
Terminal.Signal.INT, sig -> daemonDispatch.accept(Message.BareMessage.CANCEL_BUILD_SINGLETON));
this.display = new Display(terminal, false);
this.log = logFile == null ? new MessageCollector() : new FileLog(logFile);
if (!dumb) {
final Thread r = new Thread(this::readInputLoop);
r.setDaemon(true);
r.start();
this.reader = r;
} else {
this.reader = null;
}
this.inputHandler = new TerminalInputHandler(terminal, this.dumb);
}
@Override
@@ -191,11 +177,13 @@ public class TerminalOutput implements ClientOutput {
@Override
public void setDaemonDispatch(Consumer<Message> daemonDispatch) {
this.daemonDispatch = daemonDispatch;
this.inputHandler.setDaemonDispatch(daemonDispatch);
}
@Override
public void setDaemonReceive(Consumer<Message> daemonReceive) {
this.daemonReceive = daemonReceive;
this.inputHandler.setDaemonReceive(daemonReceive);
}
@Override
@@ -226,6 +214,7 @@ public class TerminalOutput implements ClientOutput {
final int totalProjectsDigits = (int) (Math.log10(totalProjects) + 1);
this.projectsDoneFomat = "%" + totalProjectsDigits + "d";
this.maxThreads = bs.getMaxThreads();
this.inputHandler.setMaxThreads(maxThreads);
this.artifactIdFormat = "%-" + bs.getArtifactIdDisplayLength() + "s ";
final int maxThreadsDigits = (int) (Math.log10(maxThreads) + 1);
this.threadsFormat = "%" + (maxThreadsDigits * 3 + 2) + "s";
@@ -343,42 +332,8 @@ public class TerminalOutput implements ClientOutput {
terminal.writer().println("");
break;
}
readInput.writeLock().lock();
try {
clearDisplay();
if (prompt.getMessage() != null) {
String msg = (maxThreads > 1)
? String.format("[%s] %s", prompt.getProjectId(), prompt.getMessage())
: prompt.getMessage();
terminal.writer().print(msg);
}
terminal.flush();
StringBuilder sb = new StringBuilder();
while (true) {
int c = terminal.reader().read();
if (c < 0) {
break;
} else if (c == '\n' || c == '\r') {
terminal.writer().println();
daemonDispatch.accept(prompt.response(sb.toString()));
break;
} else if (c == 127) {
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
terminal.writer().write("\b \b");
terminal.writer().flush();
}
} else {
terminal.writer().print((char) c);
terminal.writer().flush();
sb.append((char) c);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
readInput.writeLock().unlock();
}
clearDisplay();
inputHandler.requestPrompt(prompt);
break;
}
case Message.BUILD_LOG_MESSAGE: {
@@ -456,7 +411,7 @@ public class TerminalOutput implements ClientOutput {
}
case Message.REQUEST_INPUT: {
RequestInput ri = (RequestInput) entry;
projectReadingInput = ri.getProjectId();
inputHandler.requestProjectInput(ri.getProjectId(), ri.getBytesToRead());
break;
}
case Message.INPUT_DATA: {
@@ -502,45 +457,6 @@ public class TerminalOutput implements ClientOutput {
return terminal;
}
void readInputLoop() {
try {
while (!closing) {
if (readInput.readLock().tryLock(10, TimeUnit.MILLISECONDS)) {
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;
}
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));
}
}
readInput.readLock().unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (InterruptedIOException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
this.exception = e;
}
}
private void clearDisplay() {
if (!noBuffering && !dumb) {
display.update(Collections.emptyList(), 0);
@@ -560,9 +476,7 @@ public class TerminalOutput implements ClientOutput {
@Override
public void close() throws Exception {
closing = true;
if (reader != null) {
reader.interrupt();
}
inputHandler.close();
log.close();
terminal.handle(Terminal.Signal.INT, previousIntHandler);
terminal.close();
@@ -781,13 +695,11 @@ public class TerminalOutput implements ClientOutput {
.style(AttributedStyle.BOLD)
.append(String.format(
threadsFormat,
new StringBuilder(threadsFormat.length())
.append(projectsCount)
.append('/')
.append(Math.max(0, projectsCount - dispLines))
.append('/')
.append(maxThreads)
.toString()))
String.valueOf(projectsCount)
+ '/'
+ Math.max(0, projectsCount - dispLines)
+ '/'
+ maxThreads))
.style(AttributedStyle.DEFAULT);
/* Progress */
@@ -801,7 +713,7 @@ public class TerminalOutput implements ClientOutput {
.append('%')
.style(AttributedStyle.DEFAULT);
} else if (buildStatus != null) {
} else {
asb.style(AttributedStyle.BOLD).append(buildStatus).style(AttributedStyle.DEFAULT);
}

View File

@@ -54,14 +54,13 @@ public class DaemonMavenInvoker extends ResidentMavenInvoker {
if (context.coloredOutput != null) {
builder.color(context.coloredOutput);
}
// we do want to pause input
builder.paused(true);
},
terminal -> doConfigureWithTerminal(context, terminal));
context.terminal = MessageUtils.getTerminal();
context.closeables.add(MessageUtils::systemUninstall);
MessageUtils.registerShutdownHook();
if (context.coloredOutput != null) {
MessageUtils.setColorEnabled(context.coloredOutput);
}
}
@Override

View File

@@ -0,0 +1,150 @@
/*
* 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.mvndaemon.mvnd.daemon;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.Objects;
import java.util.function.BiConsumer;
import org.apache.maven.logging.ProjectBuildLogAppender;
/**
* An InputStream implementation that manages input for Maven daemon processes.
*
* This class implements a buffered input stream that:
* 1. Tracks which project is currently reading input using ProjectBuildLogAppender
* 2. Requests input from the client when needed through a callback
* 3. Buffers received input data in memory
*
* Key behaviors:
* - Input is requested through startReadingFromProject callback whenever:
* a) The reading project changes
* b) The input buffer is empty and more data is needed
* - The callback receives both the project ID and the number of bytes requested
* - Data is added to the buffer through addInputData, which can be called from another thread
* - EOF is signaled by calling addInputData with null
*
* The stream coordinates between multiple threads:
* - Reader thread(s): Calling read() methods to get input
* - Writer thread: Calling addInputData to provide input data
*
* Synchronization:
* - All buffer access is synchronized on the datas collection
* - Readers wait when no data is available using datas.wait()
* - Writers notify readers when new data arrives using datas.notifyAll()
*
* This implementation is particularly important for:
* 1. Handling piped input (e.g., cat file | mvnd ...)
* 2. Supporting interactive input during builds
* 3. Managing input across multiple project builds
*/
class DaemonInputStream extends InputStream {
private final BiConsumer<String, Integer> startReadingFromProject;
private final LinkedList<byte[]> datas = new LinkedList<>();
private final Charset charset;
private int pos = -1;
private String projectReading = null;
private volatile boolean eof = false;
DaemonInputStream(BiConsumer<String, Integer> startReadingFromProject) {
this.startReadingFromProject = startReadingFromProject;
this.charset = Charset.forName(System.getProperty("file.encoding"));
}
@Override
public int available() throws IOException {
synchronized (datas) {
String projectId = ProjectBuildLogAppender.getProjectId();
if (!eof && !Objects.equals(projectId, projectReading)) {
projectReading = projectId;
startReadingFromProject.accept(projectId, 1);
}
return datas.stream().mapToInt(a -> a.length).sum() - Math.max(pos, 0);
}
}
@Override
public int read() throws IOException {
byte[] b = new byte[1];
int read = read(b, 0, 1);
if (read == 1) {
return b[0];
}
return -1;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
synchronized (datas) {
if (eof && datas.isEmpty()) {
return -1; // Return EOF if we've reached the end and no more data
}
String projectId = ProjectBuildLogAppender.getProjectId();
if (!Objects.equals(projectId, projectReading)) {
projectReading = projectId;
}
int read = 0;
while (read < len) {
if (datas.isEmpty()) {
if (eof) {
return read > 0 ? read : -1; // Exit properly on EOF
}
if (read > 0) {
break;
}
// Always notify we need input when waiting for data
startReadingFromProject.accept(projectReading, len - read);
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;
}
b[off + read++] = curData[pos++];
}
return read;
}
}
public void addInputData(String data) {
synchronized (datas) {
if (data == null) {
eof = true;
} else {
datas.add(data.getBytes(charset));
}
datas.notifyAll();
}
}
}

View File

@@ -20,22 +20,18 @@ package org.mvndaemon.mvnd.daemon;
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.CompletableFuture;
import java.util.concurrent.Executors;
@@ -46,7 +42,6 @@ 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;
@@ -513,9 +508,11 @@ 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)));
final DaemonInputStream daemonInputStream = new DaemonInputStream(
(projectId, bytesToRead) -> sendQueue.add(Message.requestInput(projectId, bytesToRead)));
InputStream in = System.in;
try {
System.setIn(daemonInputStream);
LOGGER.info("Executing request");
@@ -639,6 +636,7 @@ public class Server implements AutoCloseable, Runnable {
} catch (Throwable t) {
LOGGER.error("Error while building project", t);
} finally {
System.setIn(in);
if (!noDaemon) {
LOGGER.info("Daemon back to idle");
updateState(DaemonState.Idle);
@@ -688,67 +686,4 @@ 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();
}
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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.mvndaemon.mvnd.it;
import javax.inject.Inject;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.mvndaemon.mvnd.assertj.TestClientOutput;
import org.mvndaemon.mvnd.client.Client;
import org.mvndaemon.mvnd.client.DaemonParameters;
import org.mvndaemon.mvnd.common.Message;
import org.mvndaemon.mvnd.junit.MvndNativeTest;
@MvndNativeTest(projectDir = "src/test/projects/input-stream")
class InputStreamNativeIT {
@Inject
Client client;
@Inject
DaemonParameters parameters;
@Test
void installPluginAndTest() throws IOException, InterruptedException {
final TestClientOutput output = new TestClientOutput() {
int input = 0;
@Override
public void accept(Message message) {
if (message instanceof Message.RequestInput) {
if (input++ < 10) {
daemonDispatch.accept(Message.inputResponse("0123456789\n"));
} else {
daemonDispatch.accept(Message.inputEof());
}
}
if (!(message instanceof Message.TransferEvent)) {
super.accept(message);
}
}
};
client.execute(output, "install").assertSuccess();
client.execute(output, "org.mvndaemon.mvnd.test.input-stream:echo-maven-plugin:echo");
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.mvndaemon.mvnd.it;
import org.mvndaemon.mvnd.junit.MvndTest;
@MvndTest(projectDir = "src/test/projects/input-stream")
class InputStreamTest extends InputStreamNativeIT {}

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.mvndaemon.mvnd.test.input-stream</groupId>
<artifactId>echo-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>echo-maven-plugin Maven Plugin</name>
<prerequisites>
<maven>${maven.version}</maven>
</prerequisites>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.version>3.9.9</maven.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.13.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<configuration>
<!-- <skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>-->
</configuration>
<executions>
<execution>
<id>mojo-descriptor</id>
<goals>
<goal>descriptor</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,57 @@
/*
* 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.its;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;
/**
* Goal which copy stdin to stdout.
*/
@Mojo( name = "echo", requiresProject = false )
public class EchoMojo
extends AbstractMojo
{
public void execute()
throws MojoExecutionException
{
getLog().info("Reading from standard input. Type 'exit' to stop.");
try (Scanner scanner = new Scanner(System.in)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if ("exit".equalsIgnoreCase(line.trim())) {
getLog().info("Exiting...");
break;
}
System.out.println(line);
}
}
}
}