diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java b/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java index ceb4d0d49..bf1328131 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java @@ -120,6 +120,9 @@ protected void doReceive() { try { while (running.get()) { Message m = connection.receive(); + if (m == null) { + break; + } queue.put(m); } } catch (Exception e) { diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java b/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java index bcc4b5810..bcf7a3d74 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java @@ -243,6 +243,13 @@ public ExecutionResult execute(ClientOutput output, List argv) { output.accept(bm.getProjectId(), bm.getMessage()); } else if (m == Message.KEEP_ALIVE_SINGLETON) { output.keepAlive(); + } else if (m instanceof Message.Display) { + Message.Display d = (Message.Display) m; + output.display(d.getProjectId(), d.getMessage()); + } else if (m instanceof Message.Prompt) { + Message.Prompt p = (Message.Prompt) m; + String response = output.prompt(p.getProjectId(), p.getMessage(), p.isPassword()); + daemon.dispatch(new Message.PromptResponse(p.getProjectId(), p.getUid(), response)); } } } diff --git a/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java b/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java index 76167b280..a231c0fd6 100644 --- a/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java +++ b/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java @@ -35,6 +35,9 @@ public abstract class Message { static final int KEEP_ALIVE = 4; static final int STOP = 5; static final int BUILD_STARTED = 6; + static final int DISPLAY = 7; + static final int PROMPT = 8; + static final int PROMPT_RESPONSE = 9; public static final SimpleMessage KEEP_ALIVE_SINGLETON = new SimpleMessage(Message.KEEP_ALIVE, "KEEP_ALIVE"); public static final SimpleMessage STOP_SINGLETON = new SimpleMessage(Message.STOP, "STOP"); @@ -59,6 +62,12 @@ public static Message read(DataInputStream input) throws IOException { return SimpleMessage.KEEP_ALIVE_SINGLETON; case STOP: return SimpleMessage.STOP_SINGLETON; + case DISPLAY: + return Display.read(input); + case PROMPT: + return Prompt.read(input); + case PROMPT_RESPONSE: + return PromptResponse.read(input); } throw new IllegalStateException("Unexpected message type: " + type); } @@ -489,4 +498,151 @@ public void write(DataOutputStream output) throws IOException { } } + public static class Display extends Message { + + final String projectId; + final String message; + + public static Message read(DataInputStream input) throws IOException { + String projectId = readUTF(input); + String message = readUTF(input); + return new Display(projectId, message); + } + + public Display(String projectId, String message) { + this.projectId = projectId; + this.message = message; + } + + public String getProjectId() { + return projectId; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "Display{" + + "projectId='" + projectId + '\'' + + ", message='" + message + '\'' + + '}'; + } + + @Override + public void write(DataOutputStream output) throws IOException { + output.write(DISPLAY); + writeUTF(output, projectId); + writeUTF(output, message); + } + } + + public static class Prompt extends Message { + + final String projectId; + final String uid; + final String message; + final boolean password; + + public static Message read(DataInputStream input) throws IOException { + String projectId = Message.readUTF(input); + String uid = Message.readUTF(input); + String message = Message.readUTF(input); + boolean password = input.readBoolean(); + return new Prompt(projectId, uid, message, password); + } + + public Prompt(String projectId, String uid, String message, boolean password) { + this.projectId = projectId; + this.uid = uid; + this.message = message; + this.password = password; + } + + public String getProjectId() { + return projectId; + } + + public String getUid() { + return uid; + } + + public String getMessage() { + return message; + } + + public boolean isPassword() { + return password; + } + + @Override + public String toString() { + return "Prompt{" + + "projectId='" + projectId + '\'' + + ", uid='" + uid + '\'' + + ", message='" + message + '\'' + + ", password=" + password + + '}'; + } + + @Override + public void write(DataOutputStream output) throws IOException { + output.write(PROMPT); + writeUTF(output, projectId); + writeUTF(output, uid); + writeUTF(output, message); + output.writeBoolean(password); + } + } + + public static class PromptResponse extends Message { + + final String projectId; + final String uid; + final String message; + + public static Message read(DataInputStream input) throws IOException { + String projectId = Message.readUTF(input); + String uid = Message.readUTF(input); + String message = Message.readUTF(input); + return new PromptResponse(projectId, uid, message); + } + + public PromptResponse(String projectId, String uid, String message) { + this.projectId = projectId; + this.uid = uid; + this.message = message; + } + + public String getProjectId() { + return projectId; + } + + public String getUid() { + return uid; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "PromptResponse{" + + "projectId='" + projectId + '\'' + + ", uid='" + uid + '\'' + + ", message='" + message + '\'' + + '}'; + } + + @Override + public void write(DataOutputStream output) throws IOException { + output.write(PROMPT_RESPONSE); + writeUTF(output, projectId); + writeUTF(output, uid); + writeUTF(output, message); + } + } + } diff --git a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java index 7dcb832f9..c654a91e6 100644 --- a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java +++ b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java @@ -38,4 +38,7 @@ public interface ClientOutput extends AutoCloseable { void buildStatus(String status); + void display(String projectId, String message); + + String prompt(String projectId, String message, boolean password); } diff --git a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java index 26141deaa..014aac361 100644 --- a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java +++ b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java @@ -30,6 +30,10 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.SynchronousQueue; +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; @@ -61,6 +65,7 @@ public class TerminalOutput implements ClientOutput { private volatile boolean closing; private final CountDownLatch closed = new CountDownLatch(1); private final long start; + private final ReadWriteLock readInput = new ReentrantReadWriteLock(); private volatile String name; private volatile int totalProjects; @@ -79,7 +84,10 @@ enum EventType { ERROR, END_OF_STREAM, INPUT, - KEEP_ALIVE + KEEP_ALIVE, + DISPLAY, + PROMPT, + PROMPT_PASSWORD } static class Event { @@ -87,11 +95,17 @@ static class Event { public final EventType type; public final String projectId; public final String message; + public final SynchronousQueue response; public Event(EventType type, String projectId, String message) { + this(type, projectId, message, null); + } + + public Event(EventType type, String projectId, String message, SynchronousQueue response) { this.type = type; this.projectId = projectId; this.message = message; + this.response = response; } } @@ -202,17 +216,44 @@ public void buildStatus(String status) { } } + @Override + public void display(String projectId, String message) { + try { + queue.put(new Event(EventType.DISPLAY, projectId, message)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public String prompt(String projectId, String message, boolean password) { + String response = null; + try { + SynchronousQueue sq = new SynchronousQueue<>(); + queue.put(new Event(password ? EventType.PROMPT_PASSWORD : EventType.PROMPT, projectId, message, sq)); + response = sq.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return response; + } + void readInputLoop() { try { while (!closing) { - int c = terminal.reader().read(10); - if (c == -1) { - break; - } - if (c == '+' || c == '-' || c == CTRL_L || c == CTRL_M) { - queue.add(new Event(EventType.INPUT, null, Character.toString((char) c))); + if (readInput.readLock().tryLock(10, TimeUnit.MILLISECONDS)) { + int c = terminal.reader().read(10); + if (c == -1) { + break; + } + if (c == '+' || c == '-' || c == CTRL_L || c == CTRL_M) { + queue.add(new Event(EventType.INPUT, null, Character.toString((char) c))); + } + readInput.readLock().unlock(); } } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } catch (InterruptedIOException e) { Thread.currentThread().interrupt(); } catch (IOException e) { @@ -289,6 +330,42 @@ void displayLoop() { break; } break; + case DISPLAY: + display.update(Collections.emptyList(), 0); + terminal.writer().printf("[%s] %s%n", entry.projectId, entry.message); + break; + case PROMPT: + case PROMPT_PASSWORD: { + readInput.writeLock().lock(); + try { + display.update(Collections.emptyList(), 0); + terminal.writer().printf("[%s] %s", entry.projectId, entry.message); + terminal.flush(); + StringBuilder sb = new StringBuilder(); + while (true) { + int c = terminal.reader().read(); + if (c < 0) { + break; + } else if (c == '\n' || c == '\r') { + entry.response.put(sb.toString()); + terminal.writer().println(); + 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); + } + } + } finally { + readInput.writeLock().unlock(); + } + } } } entries.clear(); diff --git a/daemon/pom.xml b/daemon/pom.xml index 1a84e1c46..d12c54630 100644 --- a/daemon/pom.xml +++ b/daemon/pom.xml @@ -49,6 +49,11 @@ io.takari.aether takari-local-repository + + org.codehaus.plexus + plexus-interactivity-api + 1.0 + diff --git a/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java b/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java index 9587cc01d..04635256c 100644 --- a/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java +++ b/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java @@ -456,6 +456,9 @@ void container() exportedArtifacts.addAll(extension.getExportedArtifacts()); exportedPackages.addAll(extension.getExportedPackages()); } + exportedPackages.add("org.codehaus.plexus.components.interactivity"); + exportedPackages.add("org.jboss.fuse.mvnd.interactivity"); + exportedArtifacts.add("org.codehaus.plexus:plexus-interactivity-api"); final CoreExports exports = new CoreExports(containerRealm, exportedArtifacts, exportedPackages); diff --git a/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Connection.java b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Connection.java new file mode 100644 index 000000000..2cae7a68d --- /dev/null +++ b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Connection.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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. + */ +package org.jboss.fuse.mvnd.daemon; + +import java.util.function.Predicate; +import org.jboss.fuse.mvnd.common.Message; + +public interface Connection { + + static Connection getCurrent() { + return Holder.CURRENT; + } + + static void setCurrent(Connection connection) { + Holder.CURRENT = connection; + } + + void dispatch(Message message); + + T request(Message request, Class responseType, Predicate matcher); + + class Holder { + static Connection CURRENT; + } + +} diff --git a/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java index ad370665c..a0a81cf44 100644 --- a/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java +++ b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java @@ -29,12 +29,15 @@ import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; 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.Predicate; import java.util.stream.Collectors; import org.apache.maven.cli.DaemonMavenCli; import org.apache.maven.execution.MavenSession; @@ -53,6 +56,9 @@ import org.jboss.fuse.mvnd.common.Message.BuildMessage; import org.jboss.fuse.mvnd.common.Message.BuildRequest; import org.jboss.fuse.mvnd.common.Message.BuildStarted; +import org.jboss.fuse.mvnd.common.Message.Display; +import org.jboss.fuse.mvnd.common.Message.Prompt; +import org.jboss.fuse.mvnd.common.Message.PromptResponse; import org.jboss.fuse.mvnd.daemon.DaemonExpiration.DaemonExpirationResult; import org.jboss.fuse.mvnd.daemon.DaemonExpiration.DaemonExpirationStrategy; import org.jboss.fuse.mvnd.logging.smart.AbstractLoggingSpy; @@ -181,8 +187,9 @@ public DaemonThread(Runnable target) { private void accept() { try { while (true) { - SocketChannel socket = this.socket.accept(); - new DaemonThread(() -> client(socket)).start(); + try (SocketChannel socket = this.socket.accept()) { + client(socket); + } } } catch (Throwable t) { LOGGER.error("Error running daemon loop", t); @@ -192,17 +199,21 @@ private void accept() { private void client(SocketChannel socket) { LOGGER.info("Client connected"); try (DaemonConnection connection = new DaemonConnection(socket)) { - while (true) { - LOGGER.info("Waiting for request"); + LOGGER.info("Waiting for request"); + SynchronousQueue request = new SynchronousQueue<>(); + new DaemonThread(() -> { Message message = connection.receive(); - if (message == null) { - return; - } - LOGGER.info("Request received: " + message); - - if (message instanceof BuildRequest) { - handle(connection, (BuildRequest) message); - } + request.offer(message); + }).start(); + Message message = request.poll(1, TimeUnit.MINUTES); + if (message == null) { + LOGGER.info("Could not receive request after one minute, dropping connection"); + updateState(DaemonState.Idle); + return; + } + LOGGER.info("Request received: " + message); + if (message instanceof BuildRequest) { + handle(connection, (BuildRequest) message); } } catch (Throwable t) { LOGGER.error("Error reading request", t); @@ -394,24 +405,25 @@ private void handle(DaemonConnection connection, BuildRequest buildRequest) { LOGGER.info("Executing request"); - BlockingQueue queue = new PriorityBlockingQueue(64, + BlockingQueue sendQueue = new PriorityBlockingQueue<>(64, Comparator.comparingInt(this::getClassOrder).thenComparingLong(Message::timestamp)); + BlockingQueue recvQueue = new LinkedBlockingDeque<>(); - DaemonLoggingSpy loggingSpy = new DaemonLoggingSpy(queue); + DaemonLoggingSpy loggingSpy = new DaemonLoggingSpy(sendQueue); AbstractLoggingSpy.instance(loggingSpy); - Thread pumper = new Thread(() -> { + Thread sender = new Thread(() -> { try { boolean flushed = true; while (true) { Message m; if (flushed) { - m = queue.poll(keepAlive, TimeUnit.MILLISECONDS); + m = sendQueue.poll(keepAlive, TimeUnit.MILLISECONDS); if (m == null) { m = Message.KEEP_ALIVE_SINGLETON; } flushed = false; } else { - m = queue.poll(); + m = sendQueue.poll(); if (m == null) { connection.flush(); flushed = true; @@ -430,9 +442,64 @@ private void handle(DaemonConnection connection, BuildRequest buildRequest) { LOGGER.error("Error dispatching events", t); } }); - pumper.start(); + sender.start(); + Thread receiver = new Thread(() -> { + try { + while (true) { + Message message = connection.receive(); + if (message == null) { + break; + } + LOGGER.info("Received message: {}", message); + synchronized (recvQueue) { + recvQueue.put(message); + recvQueue.notifyAll(); + } + } + } catch (Throwable t) { + LOGGER.error("Error receiving events", t); + } + }); + receiver.start(); try { - cli.main(buildRequest.getArgs(), + Connection.setCurrent(new Connection() { + @Override + public void dispatch(Message message) { + try { + sendQueue.put(message); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public T request(Message request, Class responseType, Predicate matcher) { + try { + synchronized (recvQueue) { + sendQueue.put(request); + LOGGER.info("Waiting for response"); + while (true) { + T t = recvQueue.stream() + .filter(responseType::isInstance) + .map(responseType::cast) + .filter(matcher) + .findFirst() + .orElse(null); + if (t != null) { + recvQueue.remove(t); + LOGGER.info("Received response: {}", t); + return t; + } + recvQueue.wait(); + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }); + cli.main( + buildRequest.getArgs(), buildRequest.getWorkingDir(), buildRequest.getProjectDir(), buildRequest.getEnv()); @@ -442,7 +509,7 @@ private void handle(DaemonConnection connection, BuildRequest buildRequest) { LOGGER.error("Error while building project", t); loggingSpy.fail(t); } finally { - pumper.join(); + sender.join(); } } catch (Throwable t) { LOGGER.error("Error while building project", t); @@ -458,10 +525,12 @@ int getClassOrder(Message m) { return 0; } else if (m instanceof BuildStarted) { return 1; - } else if (m instanceof BuildEvent && ((BuildEvent) m).getType() == Type.ProjectStarted) { + } else if (m instanceof Prompt || m instanceof PromptResponse || m instanceof Display) { return 2; - } else if (m instanceof BuildEvent && ((BuildEvent) m).getType() == Type.MojoStarted) { + } else if (m instanceof BuildEvent && ((BuildEvent) m).getType() == Type.ProjectStarted) { return 3; + } else if (m instanceof BuildEvent && ((BuildEvent) m).getType() == Type.MojoStarted) { + return 4; } else if (m instanceof BuildMessage) { return 50; } else if (m instanceof BuildEvent && ((BuildEvent) m).getType() == Type.ProjectStopped) { diff --git a/daemon/src/main/java/org/jboss/fuse/mvnd/interactivity/DaemonPrompter.java b/daemon/src/main/java/org/jboss/fuse/mvnd/interactivity/DaemonPrompter.java new file mode 100644 index 000000000..b55487ca2 --- /dev/null +++ b/daemon/src/main/java/org/jboss/fuse/mvnd/interactivity/DaemonPrompter.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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. + */ +package org.jboss.fuse.mvnd.interactivity; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import javax.inject.Named; +import org.codehaus.plexus.components.interactivity.AbstractInputHandler; +import org.codehaus.plexus.components.interactivity.InputHandler; +import org.codehaus.plexus.components.interactivity.OutputHandler; +import org.codehaus.plexus.components.interactivity.Prompter; +import org.codehaus.plexus.components.interactivity.PrompterException; +import org.codehaus.plexus.util.StringUtils; +import org.eclipse.sisu.Priority; +import org.eclipse.sisu.Typed; +import org.jboss.fuse.mvnd.common.Message; +import org.jboss.fuse.mvnd.daemon.Connection; +import org.jboss.fuse.mvnd.logging.smart.ProjectBuildLogAppender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +@Named +@Priority(10) +@Typed({ Prompter.class, InputHandler.class, OutputHandler.class }) +public class DaemonPrompter extends AbstractInputHandler implements Prompter, InputHandler, OutputHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(DaemonPrompter.class); + + @Override + public String prompt(String message) throws PrompterException { + return prompt(message, null, null); + } + + @Override + public String prompt(String message, String defaultReply) throws PrompterException { + return prompt(message, null, defaultReply); + } + + @Override + public String prompt(String message, List possibleValues) throws PrompterException { + return prompt(message, possibleValues, null); + } + + @Override + public String prompt(String message, List possibleValues, String defaultReply) throws PrompterException { + return doPrompt(message, possibleValues, defaultReply, false); + } + + @Override + public String promptForPassword(String message) throws PrompterException { + return doPrompt(message, null, null, true); + } + + @Override + public void showMessage(String message) throws PrompterException { + try { + doDisplay(message); + } catch (IOException e) { + throw new PrompterException("Failed to present prompt", e); + } + } + + @Override + public String readLine() throws IOException { + return doPrompt(null, false); + } + + @Override + public String readPassword() throws IOException { + return doPrompt(null, true); + } + + @Override + public void write(String line) throws IOException { + doDisplay(line); + } + + @Override + public void writeLine(String line) throws IOException { + doDisplay(line + "\n"); + } + + String doPrompt(String message, List possibleValues, String defaultReply, boolean password) + throws PrompterException { + String formattedMessage = formatMessage(message, possibleValues, defaultReply); + String line; + do { + try { + line = doPrompt(formattedMessage, password); + if (line == null && defaultReply == null) { + throw new IOException("EOF"); + } + } catch (IOException e) { + throw new PrompterException("Failed to prompt user", e); + } + if (StringUtils.isEmpty(line)) { + line = defaultReply; + } + if (line != null && (possibleValues != null && !possibleValues.contains(line))) { + try { + doDisplay("Invalid selection.\n"); + } catch (IOException e) { + throw new PrompterException("Failed to present feedback", e); + } + } + } while (line == null || (possibleValues != null && !possibleValues.contains(line))); + return line; + } + + private String formatMessage(String message, List possibleValues, String defaultReply) { + StringBuilder formatted = new StringBuilder(message.length() * 2); + formatted.append(message); + if (possibleValues != null && !possibleValues.isEmpty()) { + formatted.append(" ("); + for (Iterator it = possibleValues.iterator(); it.hasNext();) { + String possibleValue = String.valueOf(it.next()); + formatted.append(possibleValue); + if (it.hasNext()) { + formatted.append('/'); + } + } + formatted.append(')'); + } + if (defaultReply != null) { + formatted.append(' ').append(defaultReply).append(": "); + } + return formatted.toString(); + } + + private void doDisplay(String message) throws IOException { + try { + Connection con = Objects.requireNonNull(Connection.getCurrent()); + String projectId = MDC.get(ProjectBuildLogAppender.KEY_PROJECT_ID); + Message.Display msg = new Message.Display(projectId, message); + LOGGER.info("Sending display request: " + msg); + con.dispatch(msg); + } catch (Exception e) { + throw new IOException("Unable to display message", e); + } + } + + private String doPrompt(String message, boolean password) throws IOException { + try { + Connection con = Objects.requireNonNull(Connection.getCurrent()); + String projectId = MDC.get(ProjectBuildLogAppender.KEY_PROJECT_ID); + String uid = UUID.randomUUID().toString(); + Message.Prompt msg = new Message.Prompt(projectId, uid, message, password); + LOGGER.info("Requesting prompt: " + msg); + Message.PromptResponse res = con.request(msg, Message.PromptResponse.class, + r -> uid.equals(r.getUid())); + LOGGER.info("Received response: " + res.getMessage()); + return res.getMessage(); + } catch (Exception e) { + throw new IOException("Unable to prompt user", e); + } + } +} diff --git a/dist/src/main/provisio/maven-distro.xml b/dist/src/main/provisio/maven-distro.xml index c22fd6890..db9b5cfb1 100644 --- a/dist/src/main/provisio/maven-distro.xml +++ b/dist/src/main/provisio/maven-distro.xml @@ -58,6 +58,7 @@ + diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InteractiveTest.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InteractiveTest.java new file mode 100644 index 000000000..b2b11ca64 --- /dev/null +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/InteractiveTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 the original author or authors. + * + * 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. + */ +package org.jboss.fuse.mvnd.it; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.jboss.fuse.mvnd.client.Client; +import org.jboss.fuse.mvnd.client.DaemonParameters; +import org.jboss.fuse.mvnd.common.logging.ClientOutput; +import org.jboss.fuse.mvnd.junit.MvndTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@MvndTest(projectDir = "src/test/projects/single-module") +public class InteractiveTest { + + @Inject + Client client; + + @Inject + DaemonParameters parameters; + + @Test + void versionsSet() throws IOException, InterruptedException { + final String version = MvndTestUtil.version(parameters.multiModuleProjectDirectory().resolve("pom.xml")); + Assertions.assertEquals("0.0.1-SNAPSHOT", version); + + final ClientOutput o = Mockito.mock(ClientOutput.class); + Mockito.when(o.prompt("single-module", "Enter the new version to set 0.0.1-SNAPSHOT: ", false)) + .thenReturn("0.1.0-SNAPSHOT"); + client.execute(o, "versions:set").assertSuccess(); + + final String newVersion = MvndTestUtil.version(parameters.multiModuleProjectDirectory().resolve("pom.xml")); + Assertions.assertEquals("0.1.0-SNAPSHOT", newVersion); + } + +} diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/MvndTestUtil.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/MvndTestUtil.java index 0e5564452..0e13c7772 100644 --- a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/MvndTestUtil.java +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/MvndTestUtil.java @@ -42,4 +42,13 @@ public static Properties properties(Path pomXmlPath) { } } + public static String version(Path pomXmlPath) { + try (Reader runtimeReader = Files.newBufferedReader(pomXmlPath, StandardCharsets.UTF_8)) { + final MavenXpp3Reader rxppReader = new MavenXpp3Reader(); + return rxppReader.read(runtimeReader).getVersion(); + } catch (IOException | XmlPullParserException e) { + throw new RuntimeException("Could not read or parse " + pomXmlPath); + } + } + }