mirror of
https://github.com/apache/maven-mvnd.git
synced 2025-10-17 16:07:25 +00:00
@@ -1054,30 +1054,38 @@ public abstract class Message {
|
|||||||
public static class RequestInput extends Message {
|
public static class RequestInput extends Message {
|
||||||
|
|
||||||
private String projectId;
|
private String projectId;
|
||||||
|
private int bytesToRead;
|
||||||
|
|
||||||
public static RequestInput read(DataInputStream input) throws IOException {
|
public static RequestInput read(DataInputStream input) throws IOException {
|
||||||
String projectId = readUTF(input);
|
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);
|
super(REQUEST_INPUT);
|
||||||
this.projectId = projectId;
|
this.projectId = projectId;
|
||||||
|
this.bytesToRead = bytesToRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getProjectId() {
|
public String getProjectId() {
|
||||||
return projectId;
|
return projectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getBytesToRead() {
|
||||||
|
return bytesToRead;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "RequestInput{" + "projectId='" + projectId + '\'' + '}';
|
return "RequestInput{" + "projectId='" + projectId + '\'' + ", bytesToRead=" + bytesToRead + '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream output) throws IOException {
|
public void write(DataOutputStream output) throws IOException {
|
||||||
super.write(output);
|
super.write(output);
|
||||||
writeUTF(output, projectId);
|
writeUTF(output, projectId);
|
||||||
|
output.writeInt(bytesToRead);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1099,9 +1107,13 @@ public abstract class Message {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isEof() {
|
||||||
|
return data == null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "InputResponse{" + "data='" + data + "\'" + '}';
|
return "InputResponse{" + (data == null ? "eof" : "data='" + data + "'") + '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1119,14 +1131,18 @@ public abstract class Message {
|
|||||||
return new StringMessage(BUILD_STATUS, payload);
|
return new StringMessage(BUILD_STATUS, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RequestInput requestInput(String projectId) {
|
public static RequestInput requestInput(String projectId, int bytesToRead) {
|
||||||
return new RequestInput(projectId);
|
return new RequestInput(projectId, bytesToRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InputData inputResponse(String data) {
|
public static InputData inputResponse(String data) {
|
||||||
return new InputData(data);
|
return new InputData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static InputData inputEof() {
|
||||||
|
return new InputData(null);
|
||||||
|
}
|
||||||
|
|
||||||
public static StringMessage out(String message) {
|
public static StringMessage out(String message) {
|
||||||
return new StringMessage(PRINT_OUT, message);
|
return new StringMessage(PRINT_OUT, message);
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -19,7 +19,6 @@
|
|||||||
package org.mvndaemon.mvnd.common.logging;
|
package org.mvndaemon.mvnd.common.logging;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -32,9 +31,6 @@ import java.util.Deque;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.function.Consumer;
|
||||||
import java.util.stream.Collector;
|
import java.util.stream.Collector;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -102,19 +98,16 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
private final ArrayList<ExecutionFailureEvent> failures = new ArrayList<>();
|
private final ArrayList<ExecutionFailureEvent> failures = new ArrayList<>();
|
||||||
private final LinkedHashMap<String, Project> projects = new LinkedHashMap<>();
|
private final LinkedHashMap<String, Project> projects = new LinkedHashMap<>();
|
||||||
private final ClientLog log;
|
private final ClientLog log;
|
||||||
private final Thread reader;
|
|
||||||
private volatile Exception exception;
|
private volatile Exception exception;
|
||||||
private volatile boolean closing;
|
private volatile boolean closing;
|
||||||
private final long start;
|
private final long start;
|
||||||
private final ReadWriteLock readInput = new ReentrantReadWriteLock();
|
|
||||||
private final boolean dumb;
|
private final boolean dumb;
|
||||||
|
private final TerminalInputHandler inputHandler;
|
||||||
|
|
||||||
/** A sink for sending messages back to the daemon */
|
/** A sink for sending messages back to the daemon */
|
||||||
private volatile Consumer<Message> daemonDispatch;
|
private volatile Consumer<Message> daemonDispatch;
|
||||||
/** A sink for queuing messages to the main queue */
|
/** A sink for queuing messages to the main queue */
|
||||||
private volatile Consumer<Message> daemonReceive;
|
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.
|
* 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));
|
Terminal.Signal.INT, sig -> daemonDispatch.accept(Message.BareMessage.CANCEL_BUILD_SINGLETON));
|
||||||
this.display = new Display(terminal, false);
|
this.display = new Display(terminal, false);
|
||||||
this.log = logFile == null ? new MessageCollector() : new FileLog(logFile);
|
this.log = logFile == null ? new MessageCollector() : new FileLog(logFile);
|
||||||
if (!dumb) {
|
this.inputHandler = new TerminalInputHandler(terminal, this.dumb);
|
||||||
final Thread r = new Thread(this::readInputLoop);
|
|
||||||
r.setDaemon(true);
|
|
||||||
r.start();
|
|
||||||
this.reader = r;
|
|
||||||
} else {
|
|
||||||
this.reader = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -191,11 +177,13 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
@Override
|
@Override
|
||||||
public void setDaemonDispatch(Consumer<Message> daemonDispatch) {
|
public void setDaemonDispatch(Consumer<Message> daemonDispatch) {
|
||||||
this.daemonDispatch = daemonDispatch;
|
this.daemonDispatch = daemonDispatch;
|
||||||
|
this.inputHandler.setDaemonDispatch(daemonDispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDaemonReceive(Consumer<Message> daemonReceive) {
|
public void setDaemonReceive(Consumer<Message> daemonReceive) {
|
||||||
this.daemonReceive = daemonReceive;
|
this.daemonReceive = daemonReceive;
|
||||||
|
this.inputHandler.setDaemonReceive(daemonReceive);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -226,6 +214,7 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
final int totalProjectsDigits = (int) (Math.log10(totalProjects) + 1);
|
final int totalProjectsDigits = (int) (Math.log10(totalProjects) + 1);
|
||||||
this.projectsDoneFomat = "%" + totalProjectsDigits + "d";
|
this.projectsDoneFomat = "%" + totalProjectsDigits + "d";
|
||||||
this.maxThreads = bs.getMaxThreads();
|
this.maxThreads = bs.getMaxThreads();
|
||||||
|
this.inputHandler.setMaxThreads(maxThreads);
|
||||||
this.artifactIdFormat = "%-" + bs.getArtifactIdDisplayLength() + "s ";
|
this.artifactIdFormat = "%-" + bs.getArtifactIdDisplayLength() + "s ";
|
||||||
final int maxThreadsDigits = (int) (Math.log10(maxThreads) + 1);
|
final int maxThreadsDigits = (int) (Math.log10(maxThreads) + 1);
|
||||||
this.threadsFormat = "%" + (maxThreadsDigits * 3 + 2) + "s";
|
this.threadsFormat = "%" + (maxThreadsDigits * 3 + 2) + "s";
|
||||||
@@ -343,42 +332,8 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
terminal.writer().println("");
|
terminal.writer().println("");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
readInput.writeLock().lock();
|
clearDisplay();
|
||||||
try {
|
inputHandler.requestPrompt(prompt);
|
||||||
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();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Message.BUILD_LOG_MESSAGE: {
|
case Message.BUILD_LOG_MESSAGE: {
|
||||||
@@ -456,7 +411,7 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
}
|
}
|
||||||
case Message.REQUEST_INPUT: {
|
case Message.REQUEST_INPUT: {
|
||||||
RequestInput ri = (RequestInput) entry;
|
RequestInput ri = (RequestInput) entry;
|
||||||
projectReadingInput = ri.getProjectId();
|
inputHandler.requestProjectInput(ri.getProjectId(), ri.getBytesToRead());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Message.INPUT_DATA: {
|
case Message.INPUT_DATA: {
|
||||||
@@ -502,45 +457,6 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
return terminal;
|
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() {
|
private void clearDisplay() {
|
||||||
if (!noBuffering && !dumb) {
|
if (!noBuffering && !dumb) {
|
||||||
display.update(Collections.emptyList(), 0);
|
display.update(Collections.emptyList(), 0);
|
||||||
@@ -560,9 +476,7 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
closing = true;
|
closing = true;
|
||||||
if (reader != null) {
|
inputHandler.close();
|
||||||
reader.interrupt();
|
|
||||||
}
|
|
||||||
log.close();
|
log.close();
|
||||||
terminal.handle(Terminal.Signal.INT, previousIntHandler);
|
terminal.handle(Terminal.Signal.INT, previousIntHandler);
|
||||||
terminal.close();
|
terminal.close();
|
||||||
@@ -781,13 +695,11 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
.style(AttributedStyle.BOLD)
|
.style(AttributedStyle.BOLD)
|
||||||
.append(String.format(
|
.append(String.format(
|
||||||
threadsFormat,
|
threadsFormat,
|
||||||
new StringBuilder(threadsFormat.length())
|
String.valueOf(projectsCount)
|
||||||
.append(projectsCount)
|
+ '/'
|
||||||
.append('/')
|
+ Math.max(0, projectsCount - dispLines)
|
||||||
.append(Math.max(0, projectsCount - dispLines))
|
+ '/'
|
||||||
.append('/')
|
+ maxThreads))
|
||||||
.append(maxThreads)
|
|
||||||
.toString()))
|
|
||||||
.style(AttributedStyle.DEFAULT);
|
.style(AttributedStyle.DEFAULT);
|
||||||
|
|
||||||
/* Progress */
|
/* Progress */
|
||||||
@@ -801,7 +713,7 @@ public class TerminalOutput implements ClientOutput {
|
|||||||
.append('%')
|
.append('%')
|
||||||
.style(AttributedStyle.DEFAULT);
|
.style(AttributedStyle.DEFAULT);
|
||||||
|
|
||||||
} else if (buildStatus != null) {
|
} else {
|
||||||
asb.style(AttributedStyle.BOLD).append(buildStatus).style(AttributedStyle.DEFAULT);
|
asb.style(AttributedStyle.BOLD).append(buildStatus).style(AttributedStyle.DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -54,14 +54,13 @@ public class DaemonMavenInvoker extends ResidentMavenInvoker {
|
|||||||
if (context.coloredOutput != null) {
|
if (context.coloredOutput != null) {
|
||||||
builder.color(context.coloredOutput);
|
builder.color(context.coloredOutput);
|
||||||
}
|
}
|
||||||
|
// we do want to pause input
|
||||||
|
builder.paused(true);
|
||||||
},
|
},
|
||||||
terminal -> doConfigureWithTerminal(context, terminal));
|
terminal -> doConfigureWithTerminal(context, terminal));
|
||||||
context.terminal = MessageUtils.getTerminal();
|
context.terminal = MessageUtils.getTerminal();
|
||||||
context.closeables.add(MessageUtils::systemUninstall);
|
context.closeables.add(MessageUtils::systemUninstall);
|
||||||
MessageUtils.registerShutdownHook();
|
MessageUtils.registerShutdownHook();
|
||||||
if (context.coloredOutput != null) {
|
|
||||||
MessageUtils.setColorEnabled(context.coloredOutput);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -20,22 +20,18 @@ package org.mvndaemon.mvnd.daemon;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.ServerSocketChannel;
|
import java.nio.channels.ServerSocketChannel;
|
||||||
import java.nio.channels.SocketChannel;
|
import java.nio.channels.SocketChannel;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Executors;
|
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.Condition;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
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> sendQueue = new PriorityBlockingQueue<>(64, Message.getMessageComparator());
|
||||||
final BlockingQueue<Message> recvQueue = new LinkedBlockingDeque<>();
|
final BlockingQueue<Message> recvQueue = new LinkedBlockingDeque<>();
|
||||||
final BuildEventListener buildEventListener = new ClientDispatcher(sendQueue);
|
final BuildEventListener buildEventListener = new ClientDispatcher(sendQueue);
|
||||||
final DaemonInputStream daemonInputStream =
|
final DaemonInputStream daemonInputStream = new DaemonInputStream(
|
||||||
new DaemonInputStream(projectId -> sendQueue.add(Message.requestInput(projectId)));
|
(projectId, bytesToRead) -> sendQueue.add(Message.requestInput(projectId, bytesToRead)));
|
||||||
|
InputStream in = System.in;
|
||||||
try {
|
try {
|
||||||
|
System.setIn(daemonInputStream);
|
||||||
|
|
||||||
LOGGER.info("Executing request");
|
LOGGER.info("Executing request");
|
||||||
|
|
||||||
@@ -639,6 +636,7 @@ public class Server implements AutoCloseable, Runnable {
|
|||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
LOGGER.error("Error while building project", t);
|
LOGGER.error("Error while building project", t);
|
||||||
} finally {
|
} finally {
|
||||||
|
System.setIn(in);
|
||||||
if (!noDaemon) {
|
if (!noDaemon) {
|
||||||
LOGGER.info("Daemon back to idle");
|
LOGGER.info("Daemon back to idle");
|
||||||
updateState(DaemonState.Idle);
|
updateState(DaemonState.Idle);
|
||||||
@@ -688,67 +686,4 @@ public class Server implements AutoCloseable, Runnable {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return info.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
@@ -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 {}
|
85
integration-tests/src/test/projects/input-stream/pom.xml
Normal file
85
integration-tests/src/test/projects/input-stream/pom.xml
Normal 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>
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user