Provide smarter output on the client, fixes #77

All events are directly forwarded to the client.  The client is now responsible for ordering them per project and displaying them if needed.  A thread is now started to read the terminal input with support for '+' to display one more line per project, '-' to display one line less, and 'Ctrl+L' to redraw the display which could become messed if the build messages are a bit unusual (this may require a better fix though).
This commit is contained in:
Guillaume Nodet
2020-10-07 13:44:48 +02:00
parent 41869a7115
commit dd32f41580
13 changed files with 305 additions and 156 deletions

View File

@@ -16,25 +16,28 @@
package org.jboss.fuse.mvnd.client; package org.jboss.fuse.mvnd.client;
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;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.AbstractMap; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
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.Entry;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.jboss.fuse.mvnd.common.Message.BuildException; import org.jboss.fuse.mvnd.common.Message.BuildException;
import org.jline.terminal.Size; import org.jline.terminal.Size;
import org.jline.terminal.Terminal; import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder; import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedString; import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle; import org.jline.utils.AttributedStyle;
import org.jline.utils.Display; import org.jline.utils.Display;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -43,24 +46,50 @@ import org.slf4j.LoggerFactory;
/** /**
* A sink for various kinds of events sent by the daemon. * A sink for various kinds of events sent by the daemon.
*/ */
public interface ClientOutput extends AutoCloseable, Consumer<String> { public interface ClientOutput extends AutoCloseable {
int CTRL_L = 'L' & 0x1f;
public void projectStateChanged(String projectId, String display); public void projectStateChanged(String projectId, String display);
public void projectFinished(String projectId); public void projectFinished(String projectId);
/** Receive a log message */ public void accept(String projectId, String message);
public void accept(String message);
public void error(BuildException m); public void error(BuildException m);
enum EventType {
PROJECT_STATUS,
LOG,
ERROR,
END_OF_STREAM,
INPUT
}
class Event {
public final EventType type;
public final String projectId;
public final String message;
public Event(EventType type, String projectId, String message) {
this.type = type;
this.projectId = projectId;
this.message = message;
}
}
class Project {
String status;
final List<String> log = new ArrayList<>();
}
/** /**
* A terminal {@link ClientOutput} based on JLine. * A terminal {@link ClientOutput} based on JLine.
*/ */
static class TerminalOutput implements ClientOutput { static class TerminalOutput implements ClientOutput {
private static final Logger LOGGER = LoggerFactory.getLogger(TerminalOutput.class); private static final Logger LOGGER = LoggerFactory.getLogger(TerminalOutput.class);
private final TerminalUpdater updater; private final TerminalUpdater updater;
private final BlockingQueue<Map.Entry<String, String>> queue; private final BlockingQueue<Event> queue;
public TerminalOutput(Path logFile) throws IOException { public TerminalOutput(Path logFile) throws IOException {
this.queue = new LinkedBlockingDeque<>(); this.queue = new LinkedBlockingDeque<>();
@@ -69,7 +98,7 @@ public interface ClientOutput extends AutoCloseable, Consumer<String> {
public void projectStateChanged(String projectId, String task) { public void projectStateChanged(String projectId, String task) {
try { try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(projectId, task)); queue.put(new Event(EventType.PROJECT_STATUS, projectId, task));
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
@@ -77,16 +106,16 @@ public interface ClientOutput extends AutoCloseable, Consumer<String> {
public void projectFinished(String projectId) { public void projectFinished(String projectId) {
try { try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(projectId, null)); queue.put(new Event(EventType.PROJECT_STATUS, projectId, null));
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
@Override @Override
public void accept(String message) { public void accept(String projectId, String message) {
try { try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(TerminalUpdater.LOG, message)); queue.put(new Event(EventType.LOG, projectId, message));
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
@@ -106,27 +135,28 @@ public interface ClientOutput extends AutoCloseable, Consumer<String> {
msg = error.getClassName() + ": " + error.getMessage(); msg = error.getClassName() + ": " + error.getMessage();
} }
try { try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(TerminalUpdater.ERROR, msg)); queue.put(new Event(EventType.ERROR, null, msg));
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
static class TerminalUpdater implements AutoCloseable { static class TerminalUpdater implements AutoCloseable {
private static final String LOG = "<log>"; private final BlockingQueue<Event> queue;
private static final String ERROR = "<error>";
private static final String END_OF_STREAM = "<eos>";
private final BlockingQueue<Map.Entry<String, String>> queue;
private final Terminal terminal; private final Terminal terminal;
private final Display display; private final Display display;
private final LinkedHashMap<String, String> projects = new LinkedHashMap<>(); private final LinkedHashMap<String, Project> projects = new LinkedHashMap<>();
private final Log log; private final Log log;
private final Thread worker; private final Thread worker;
private final Thread reader;
private volatile Exception exception; private volatile Exception exception;
private volatile boolean closing;
private int linesPerProject = 0;
public TerminalUpdater(BlockingQueue<Entry<String, String>> queue, Path logFile) throws IOException { public TerminalUpdater(BlockingQueue<Event> queue, Path logFile) throws IOException {
super(); super();
this.terminal = TerminalBuilder.terminal(); this.terminal = TerminalBuilder.terminal();
terminal.enterRawMode();
this.display = new Display(terminal, false); this.display = new Display(terminal, false);
this.log = logFile == null ? new ClientOutput.Log.MessageCollector(terminal) this.log = logFile == null ? new ClientOutput.Log.MessageCollector(terminal)
: new ClientOutput.Log.FileLog(logFile); : new ClientOutput.Log.FileLog(logFile);
@@ -134,37 +164,88 @@ public interface ClientOutput extends AutoCloseable, Consumer<String> {
final Thread w = new Thread(this::run); final Thread w = new Thread(this::run);
w.start(); w.start();
this.worker = w; this.worker = w;
final Thread r = new Thread(this::read);
r.start();
this.reader = r;
}
void read() {
try {
while (!closing) {
int c = terminal.reader().read(10);
if (c == -1) {
break;
}
if (c == '+' || c == '-' || c == CTRL_L) {
queue.add(new Event(EventType.INPUT, null, Character.toString(c)));
}
}
} catch (InterruptedIOException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
this.exception = e;
}
} }
void run() { void run() {
final List<Entry<String, String>> entries = new ArrayList<>(); final List<Event> entries = new ArrayList<>();
while (true) { while (true) {
try { try {
entries.add(queue.take()); entries.add(queue.take());
queue.drainTo(entries); queue.drainTo(entries);
for (Entry<String, String> entry : entries) { for (Event entry : entries) {
final String key = entry.getKey(); switch (entry.type) {
final String value = entry.getValue(); case END_OF_STREAM: {
if (key == END_OF_STREAM) { projects.values().stream().flatMap(p -> p.log.stream()).forEach(log);
display.update(Collections.emptyList(), 0); display.update(Collections.emptyList(), 0);
LOGGER.debug("Done receiving, printing log"); LOGGER.debug("Done receiving, printing log");
log.close(); log.close();
LOGGER.debug("Done !"); LOGGER.debug("Done !");
terminal.flush(); terminal.flush();
return; return;
} else if (key == LOG) { }
log.accept(value); case LOG: {
} else if (key == ERROR) { if (entry.projectId != null) {
Project prj = projects.computeIfAbsent(entry.projectId, p -> new Project());
prj.log.add(entry.message);
} else {
log.accept(entry.message);
}
break;
}
case ERROR: {
projects.values().stream().flatMap(p -> p.log.stream()).forEach(log);
display.update(Collections.emptyList(), 0); display.update(Collections.emptyList(), 0);
final AttributedStyle s = new AttributedStyle().bold().foreground(AttributedStyle.RED); final AttributedStyle s = new AttributedStyle().bold().foreground(AttributedStyle.RED);
terminal.writer().println(new AttributedString(value, s).toAnsi()); terminal.writer().println(new AttributedString(entry.message, s).toAnsi());
terminal.flush(); terminal.flush();
return; return;
} else if (value == null) { }
projects.remove(key); case PROJECT_STATUS:
} else { if (entry.message != null) {
projects.put(key, value); Project prj = projects.computeIfAbsent(entry.projectId, p -> new Project());
prj.status = entry.message;
} else {
Project prj = projects.remove(entry.projectId);
if (prj != null) {
prj.log.forEach(log);
}
}
break;
case INPUT:
switch (entry.message.charAt(0)) {
case '+':
linesPerProject = Math.min(10, linesPerProject + 1);
break;
case '-':
linesPerProject = Math.max(0, linesPerProject - 1);
break;
case CTRL_L:
display.reset();
break;
}
break;
} }
} }
entries.clear(); entries.clear();
@@ -179,8 +260,12 @@ public interface ClientOutput extends AutoCloseable, Consumer<String> {
@Override @Override
public void close() throws Exception { public void close() throws Exception {
queue.put(new AbstractMap.SimpleImmutableEntry<>(END_OF_STREAM, null)); closing = true;
reader.interrupt();
queue.put(new Event(EventType.END_OF_STREAM, null, null));
worker.join(); worker.join();
reader.join();
terminal.close();
if (exception != null) { if (exception != null) {
throw exception; throw exception;
} }
@@ -190,35 +275,61 @@ public interface ClientOutput extends AutoCloseable, Consumer<String> {
// no need to refresh the display at every single step // no need to refresh the display at every single step
final Size size = terminal.getSize(); final Size size = terminal.getSize();
final int rows = size.getRows(); final int rows = size.getRows();
final int cols = size.getColumns();
display.resize(rows, size.getColumns()); display.resize(rows, size.getColumns());
if (rows <= 0) { if (rows <= 0) {
display.update(Collections.emptyList(), 0); display.update(Collections.emptyList(), 0);
return; return;
} }
final int displayableProjectCount = rows - 1; final List<AttributedString> lines = new ArrayList<>(rows);
final int skipRows = projects.size() > displayableProjectCount ? projects.size() - displayableProjectCount : 0; final int dispLines = rows - 1;
final List<AttributedString> lines = new ArrayList<>(projects.size() - skipRows); if (projects.size() <= dispLines) {
final int lineMaxLength = size.getColumns(); lines.add(new AttributedString("Building..."));
int i = 0; int remLogLines = dispLines - projects.size();
lines.add(new AttributedString("Building..." + (skipRows > 0 ? " (" + skipRows + " more)" : ""))); for (Project prj : projects.values()) {
for (String line : projects.values()) { lines.add(AttributedString.fromAnsi(prj.status));
if (i < skipRows) { // get the last lines of the project log, taking multi-line logs into account
i++; List<AttributedString> logs = lastN(prj.log, linesPerProject).stream()
} else { .flatMap(s -> AttributedString.fromAnsi(s).columnSplitLength(Integer.MAX_VALUE).stream())
lines.add(shortenIfNeeded(AttributedString.fromAnsi(line), lineMaxLength)); .map(s -> concat(" ", s))
.collect(lastN(Math.min(remLogLines, linesPerProject)));
lines.addAll(logs);
remLogLines -= logs.size();
} }
} else {
lines.add(new AttributedString("Building... (" + (projects.size() - dispLines) + " more)"));
lines.addAll(projects.values().stream()
.map(prj -> AttributedString.fromAnsi(prj.status))
.collect(lastN(dispLines)));
} }
display.update(lines, -1); List<AttributedString> trimmed = lines.stream()
.map(s -> s.columnSubSequence(0, cols))
.collect(Collectors.toList());
display.update(trimmed, -1);
} }
static AttributedString shortenIfNeeded(AttributedString s, int length) { private static <T> List<T> lastN(List<T> list, int n) {
if (s == null) { return list.subList(Math.max(0, list.size() - n), list.size());
return null; }
}
if (s.length() > length) { private static <T> Collector<T, ?, List<T>> lastN(int n) {
return s.columnSubSequence(0, length - 1); return Collector.<T, Deque<T>, List<T>> of(ArrayDeque::new, (acc, t) -> {
} if (acc.size() == n)
return s; acc.pollFirst();
acc.add(t);
}, (acc1, acc2) -> {
while (acc2.size() < n && !acc1.isEmpty()) {
acc2.addFirst(acc1.pollLast());
}
return acc2;
}, ArrayList::new);
}
private static AttributedString concat(String s1, AttributedString s2) {
AttributedStringBuilder asb = new AttributedStringBuilder();
asb.append(s1);
asb.append(s2);
return asb.toAttributedString();
} }
} }

View File

@@ -128,7 +128,7 @@ public class DefaultClient implements Client {
+ "-" + buildProperties.getOsArch() + "-" + buildProperties.getOsArch()
+ nativeSuffix) + nativeSuffix)
.reset().toString(); .reset().toString();
output.accept(v); output.accept(null, v);
/* /*
* Do not return, rather pass -v to the server so that the client module does not need to depend on any * Do not return, rather pass -v to the server so that the client module does not need to depend on any
* Maven artifacts * Maven artifacts
@@ -140,9 +140,9 @@ public class DefaultClient implements Client {
try (DaemonRegistry registry = new DaemonRegistry(layout.registry())) { try (DaemonRegistry registry = new DaemonRegistry(layout.registry())) {
boolean status = args.remove("--status"); boolean status = args.remove("--status");
if (status) { if (status) {
output.accept(String.format(" %36s %7s %5s %7s %s", output.accept(null, String.format(" %36s %7s %5s %7s %s",
"UUID", "PID", "Port", "Status", "Last activity")); "UUID", "PID", "Port", "Status", "Last activity"));
registry.getAll().forEach(d -> output.accept(String.format(" %36s %7s %5s %7s %s", registry.getAll().forEach(d -> output.accept(null, String.format(" %36s %7s %5s %7s %s",
d.getUid(), d.getPid(), d.getAddress(), d.getState(), d.getUid(), d.getPid(), d.getAddress(), d.getState(),
LocalDateTime.ofInstant( LocalDateTime.ofInstant(
Instant.ofEpochMilli(Math.max(d.getLastIdle(), d.getLastBusy())), Instant.ofEpochMilli(Math.max(d.getLastIdle(), d.getLastBusy())),
@@ -153,7 +153,7 @@ public class DefaultClient implements Client {
if (stop) { if (stop) {
DaemonInfo[] dis = registry.getAll().toArray(new DaemonInfo[0]); DaemonInfo[] dis = registry.getAll().toArray(new DaemonInfo[0]);
if (dis.length > 0) { if (dis.length > 0) {
output.accept("Stopping " + dis.length + " running daemons"); output.accept(null, "Stopping " + dis.length + " running daemons");
for (DaemonInfo di : dis) { for (DaemonInfo di : dis) {
try { try {
ProcessHandle.of(di.getPid()).ifPresent(ProcessHandle::destroyForcibly); ProcessHandle.of(di.getPid()).ifPresent(ProcessHandle::destroyForcibly);
@@ -204,15 +204,19 @@ public class DefaultClient implements Client {
return new DefaultResult(argv, null); return new DefaultResult(argv, null);
case ProjectStarted: case ProjectStarted:
case MojoStarted: case MojoStarted:
output.projectStateChanged(be.getProjectId(), be.getDisplay());
break;
case MojoStopped: case MojoStopped:
output.projectStateChanged(be.getProjectId(), be.getDisplay()); output.projectStateChanged(be.getProjectId(), be.getDisplay());
output.projectStateChanged(be.getProjectId(), ":" + be.getProjectId());
break; break;
case ProjectStopped: case ProjectStopped:
output.projectFinished(be.getProjectId()); output.projectFinished(be.getProjectId());
break;
} }
} else if (m instanceof BuildMessage) { } else if (m instanceof BuildMessage) {
BuildMessage bm = (BuildMessage) m; BuildMessage bm = (BuildMessage) m;
output.accept(bm.getMessage()); output.accept(bm.getProjectId(), bm.getMessage());
} }
} }
} }

View File

@@ -51,6 +51,11 @@
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -146,9 +146,11 @@ public abstract class Message {
} }
public static class BuildMessage extends Message { public static class BuildMessage extends Message {
final String projectId;
final String message; final String message;
public BuildMessage(String message) { public BuildMessage(String projectId, String message) {
this.projectId = projectId;
this.message = message; this.message = message;
} }
@@ -156,6 +158,10 @@ public abstract class Message {
return message; return message;
} }
public String getProjectId() {
return projectId;
}
@Override @Override
public String toString() { public String toString() {
return "BuildMessage{" + return "BuildMessage{" +
@@ -236,11 +242,13 @@ public abstract class Message {
} }
private BuildMessage readBuildMessage(DataInputStream input) throws IOException { private BuildMessage readBuildMessage(DataInputStream input) throws IOException {
String projectId = readUTF(input);
String message = readUTF(input); String message = readUTF(input);
return new BuildMessage(message); return new BuildMessage(projectId.isEmpty() ? null : projectId, message);
} }
private void writeBuildMessage(DataOutputStream output, BuildMessage value) throws IOException { private void writeBuildMessage(DataOutputStream output, BuildMessage value) throws IOException {
writeUTF(output, value.projectId != null ? value.projectId : "");
writeUTF(output, value.message); writeUTF(output, value.message);
} }

View File

@@ -514,7 +514,6 @@ public class Server implements AutoCloseable, Runnable {
@Override @Override
public void close() throws Exception { public void close() throws Exception {
sendBuildMessages();
super.close(); super.close();
} }
@@ -529,43 +528,38 @@ public class Server implements AutoCloseable, Runnable {
} }
@Override @Override
protected void onStartProject(ProjectBuild project) { protected void onStartProject(String projectId, String display) {
super.onStartProject(project); super.onStartProject(projectId, display);
sendEvent(Type.ProjectStarted, project); sendEvent(Type.ProjectStarted, projectId, display);
} }
@Override @Override
protected void onStopProject(ProjectBuild project) { protected void onStopProject(String projectId, String display) {
sendEvent(Type.ProjectStopped, project); sendEvent(Type.ProjectStopped, projectId, display);
super.onStopProject(project); super.onStopProject(projectId, display);
} }
@Override @Override
protected void onStartMojo(ProjectBuild project) { protected void onStartMojo(String projectId, String display) {
super.onStartMojo(project); super.onStartMojo(projectId, display);
sendEvent(Type.MojoStarted, project); sendEvent(Type.MojoStarted, projectId, display);
} }
@Override @Override
protected void onStopMojo(ProjectBuild project) { protected void onStopMojo(String projectId, String display) {
sendEvent(Type.MojoStopped, project); sendEvent(Type.MojoStopped, projectId, display);
super.onStopMojo(project); super.onStopMojo(projectId, display);
} }
private void sendEvent(Type type, ProjectBuild project) { @Override
String projectId = project.projectId(); protected void onProjectLog(String projectId, String message) {
String disp = project.toDisplay().toAnsi(256, false); queue.add(new BuildMessage(projectId, message));
queue.add(new BuildEvent(type, projectId, disp)); super.onProjectLog(projectId, message);
sendBuildMessages();
} }
private synchronized void sendBuildMessages() { private void sendEvent(Type type, String projectId, String display) {
if (events != null) { queue.add(new BuildEvent(type, projectId, display));
events.stream()
.map(s -> s.endsWith("\n") ? s.substring(0, s.length() - 1) : s)
.map(BuildMessage::new).forEachOrdered(queue::add);
events.clear();
}
} }
} }
} }

View File

@@ -15,14 +15,13 @@
*/ */
package org.jboss.fuse.mvnd.logging.smart; package org.jboss.fuse.mvnd.logging.smart;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.maven.eventspy.AbstractEventSpy; import org.apache.maven.eventspy.AbstractEventSpy;
import org.apache.maven.execution.ExecutionEvent; import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProject;
import org.jboss.fuse.mvnd.common.Message;
import org.jline.utils.AttributedString; import org.jline.utils.AttributedString;
import org.slf4j.MDC; import org.slf4j.MDC;
@@ -44,17 +43,14 @@ public abstract class AbstractLoggingSpy extends AbstractEventSpy {
} }
protected Map<String, ProjectBuild> projects; protected Map<String, ProjectBuild> projects;
protected List<String> events; protected List<Message.BuildMessage> events;
@Override @Override
public synchronized void init(Context context) throws Exception { public synchronized void init(Context context) throws Exception {
projects = new LinkedHashMap<>();
events = new ArrayList<>();
} }
@Override @Override
public synchronized void close() throws Exception { public synchronized void close() throws Exception {
events = null;
projects = null; projects = null;
} }
@@ -97,73 +93,67 @@ public abstract class AbstractLoggingSpy extends AbstractEventSpy {
protected void notifySessionFinish(ExecutionEvent event) { protected void notifySessionFinish(ExecutionEvent event) {
} }
protected synchronized void notifyProjectBuildStart(ExecutionEvent event) { protected void notifyProjectBuildStart(ExecutionEvent event) {
ProjectBuild pb = new ProjectBuild(); onStartProject(getProjectId(event), getProjectDisplay(event));
pb.project = event.getProject();
pb.execution = event.getMojoExecution();
pb.events = new ArrayList<>();
projects.putIfAbsent(event.getProject().getId(), pb);
onStartProject(pb);
} }
protected void onStartProject(ProjectBuild project) { protected void notifyProjectBuildFinish(ExecutionEvent event) throws Exception {
MDC.put(KEY_PROJECT_ID, project.project.getId()); onStopProject(getProjectId(event), getProjectDisplay(event));
}
protected void notifyMojoExecutionStart(ExecutionEvent event) {
onStartMojo(getProjectId(event), getProjectDisplay(event));
}
protected void notifyMojoExecutionFinish(ExecutionEvent event) {
onStopMojo(getProjectId(event), getProjectDisplay(event));
}
protected void onStartProject(String projectId, String display) {
MDC.put(KEY_PROJECT_ID, projectId);
update(); update();
} }
protected synchronized void notifyProjectBuildFinish(ExecutionEvent event) throws Exception { protected void onStopProject(String projectId, String display) {
ProjectBuild pb = projects.remove(event.getProject().getId());
if (pb != null) {
events.addAll(pb.events);
onStopProject(pb);
}
}
protected void onStopProject(ProjectBuild project) {
update(); update();
MDC.remove(KEY_PROJECT_ID); MDC.remove(KEY_PROJECT_ID);
} }
protected synchronized void notifyMojoExecutionStart(ExecutionEvent event) { protected void onStartMojo(String projectId, String display) {
ProjectBuild pb = projects.get(event.getProject().getId());
if (pb != null) {
pb.execution = event.getMojoExecution();
onStartMojo(pb);
}
}
protected void onStartMojo(ProjectBuild project) {
update(); update();
} }
protected synchronized void notifyMojoExecutionFinish(ExecutionEvent event) { protected void onStopMojo(String projectId, String display) {
ProjectBuild pb = projects.get(event.getProject().getId()); update();
if (pb != null) {
pb.execution = null;
onStopMojo(pb);
}
} }
protected void onStopMojo(ProjectBuild project) { protected void onProjectLog(String projectId, String message) {
update(); update();
} }
protected void update() { protected void update() {
} }
public synchronized void append(String projectId, String event) { private String getProjectId(ExecutionEvent event) {
ProjectBuild project = projectId != null ? projects.get(projectId) : null; return event.getProject().getArtifactId();
if (project != null) { }
project.events.add(event);
} else { private String getProjectDisplay(ExecutionEvent event) {
events.add(event); String projectId = getProjectId(event);
} String disp = event.getMojoExecution() != null
? ":" + projectId + ":" + event.getMojoExecution().toString()
: ":" + projectId;
return disp;
}
public void append(String projectId, String event) {
String msg = event.endsWith("\n") ? event.substring(0, event.length() - 1) : event;
onProjectLog(projectId, msg);
} }
protected static class ProjectBuild { protected static class ProjectBuild {
MavenProject project; MavenProject project;
volatile MojoExecution execution; volatile MojoExecution execution;
List<String> events;
@Override @Override
public String toString() { public String toString() {

View File

@@ -17,7 +17,9 @@ package org.jboss.fuse.mvnd.logging.smart;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.jline.terminal.Size; import org.jline.terminal.Size;
import org.jline.terminal.Terminal; import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder; import org.jline.terminal.TerminalBuilder;
@@ -26,6 +28,7 @@ import org.jline.utils.Display;
public class MavenLoggingSpy extends AbstractLoggingSpy { public class MavenLoggingSpy extends AbstractLoggingSpy {
private Map<String, String> projects = new LinkedHashMap<>();
private Terminal terminal; private Terminal terminal;
private Display display; private Display display;
@@ -45,9 +48,6 @@ public class MavenLoggingSpy extends AbstractLoggingSpy {
@Override @Override
public void close() throws Exception { public void close() throws Exception {
display.update(Collections.emptyList(), 0); display.update(Collections.emptyList(), 0);
for (String event : events) {
terminal.writer().print(event);
}
terminal.flush(); terminal.flush();
terminal.close(); terminal.close();
terminal = null; terminal = null;
@@ -55,13 +55,42 @@ public class MavenLoggingSpy extends AbstractLoggingSpy {
super.close(); super.close();
} }
@Override
protected void onStartProject(String projectId, String display) {
projects.put(projectId, display);
super.onStartProject(projectId, display);
}
@Override
protected void onStopProject(String projectId, String display) {
projects.remove(projectId);
super.onStopProject(projectId, display);
}
@Override
protected void onStartMojo(String projectId, String display) {
projects.put(projectId, display);
super.onStartMojo(projectId, display);
}
@Override
protected void onStopMojo(String projectId, String display) {
projects.put(projectId, display);
super.onStopMojo(projectId, display);
}
@Override
protected void onProjectLog(String projectId, String message) {
super.onProjectLog(projectId, message);
}
protected void update() { protected void update() {
Size size = terminal.getSize(); Size size = terminal.getSize();
display.resize(size.getRows(), size.getColumns()); display.resize(size.getRows(), size.getColumns());
List<AttributedString> lines = new ArrayList<>(); List<AttributedString> lines = new ArrayList<>();
lines.add(new AttributedString("Building...")); lines.add(new AttributedString("Building..."));
for (ProjectBuild build : projects.values()) { for (String build : projects.values()) {
lines.add(build.toDisplay()); lines.add(new AttributedString(build));
} }
display.update(lines, -1); display.update(lines, -1);
} }

View File

@@ -17,6 +17,7 @@ package org.jboss.fuse.mvnd.assertj;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -43,7 +44,7 @@ public class MatchInOrderAmongOthers<T extends List<? extends String>> extends C
.filter(pat -> pat.matcher(m).find()) .filter(pat -> pat.matcher(m).find())
.findFirst() .findFirst()
.orElse(null)) .orElse(null))
.filter(pat -> pat != null) /* remove null patterns */ .filter(Objects::nonNull) /* remove null patterns */
.collect(Collectors.toList()) .collect(Collectors.toList())
/* if the mapped patterns equal the input patterns then each pattern matched exactly once */ /* if the mapped patterns equal the input patterns then each pattern matched exactly once */
.equals(patterns), .equals(patterns),

View File

@@ -32,6 +32,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import static org.mockito.ArgumentMatchers.any;
@MvndTest(projectDir = "src/test/projects/multi-module") @MvndTest(projectDir = "src/test/projects/multi-module")
public class MultiModuleTest { public class MultiModuleTest {
@@ -71,7 +73,7 @@ public class MultiModuleTest {
client.execute(output, "clean", "install", "-e").assertSuccess(); client.execute(output, "clean", "install", "-e").assertSuccess();
final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class); final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class);
Mockito.verify(output, Mockito.atLeast(1)).accept(logMessage.capture()); Mockito.verify(output, Mockito.atLeast(1)).accept(any(), logMessage.capture());
Assertions.assertThat(logMessage.getAllValues()) Assertions.assertThat(logMessage.getAllValues())
.satisfiesAnyOf( /* Two orderings are possible */ .satisfiesAnyOf( /* Two orderings are possible */
messages -> Assertions.assertThat(messages) messages -> Assertions.assertThat(messages)

View File

@@ -30,6 +30,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.Mockito; import org.mockito.Mockito;
import static org.mockito.ArgumentMatchers.any;
@MvndNativeTest(projectDir = "src/test/projects/single-module") @MvndNativeTest(projectDir = "src/test/projects/single-module")
public class SingleModuleNativeIT { public class SingleModuleNativeIT {
@@ -57,14 +59,14 @@ public class SingleModuleNativeIT {
final Properties props = MvndTestUtil.properties(layout.multiModuleProjectDirectory().resolve("pom.xml")); final Properties props = MvndTestUtil.properties(layout.multiModuleProjectDirectory().resolve("pom.xml"));
final InOrder inOrder = Mockito.inOrder(o); final InOrder inOrder = Mockito.inOrder(o);
inOrder.verify(o).accept(Mockito.contains("Building single-module")); inOrder.verify(o).accept(any(), Mockito.contains("Building single-module"));
inOrder.verify(o).accept(Mockito.contains(MvndTestUtil.plugin(props, "maven-clean-plugin") + ":clean")); inOrder.verify(o).accept(any(), Mockito.contains(MvndTestUtil.plugin(props, "maven-clean-plugin") + ":clean"));
inOrder.verify(o).accept(Mockito.contains(MvndTestUtil.plugin(props, "maven-compiler-plugin") + ":compile")); inOrder.verify(o).accept(any(), Mockito.contains(MvndTestUtil.plugin(props, "maven-compiler-plugin") + ":compile"));
inOrder.verify(o).accept(Mockito.contains(MvndTestUtil.plugin(props, "maven-compiler-plugin") + ":testCompile")); inOrder.verify(o).accept(any(), Mockito.contains(MvndTestUtil.plugin(props, "maven-compiler-plugin") + ":testCompile"));
inOrder.verify(o).accept(Mockito.contains(MvndTestUtil.plugin(props, "maven-surefire-plugin") + ":test")); inOrder.verify(o).accept(any(), Mockito.contains(MvndTestUtil.plugin(props, "maven-surefire-plugin") + ":test"));
inOrder.verify(o).accept(Mockito.contains(MvndTestUtil.plugin(props, "maven-install-plugin") + ":install")); inOrder.verify(o).accept(any(), Mockito.contains(MvndTestUtil.plugin(props, "maven-install-plugin") + ":install"));
inOrder.verify(o) inOrder.verify(o).accept(any(),
.accept(Mockito.contains("SUCCESS build of project org.jboss.fuse.mvnd.test.single-module:single-module")); Mockito.contains("SUCCESS build of project org.jboss.fuse.mvnd.test.single-module:single-module"));
assertJVM(o, props); assertJVM(o, props);

View File

@@ -29,6 +29,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import static org.mockito.ArgumentMatchers.any;
@MvndTest(projectDir = "src/test/projects/single-module") @MvndTest(projectDir = "src/test/projects/single-module")
public class StopStatusTest { public class StopStatusTest {
@@ -54,7 +56,7 @@ public class StopStatusTest {
final ClientOutput output = Mockito.mock(ClientOutput.class); final ClientOutput output = Mockito.mock(ClientOutput.class);
client.execute(output, "--status").assertSuccess(); client.execute(output, "--status").assertSuccess();
final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class); final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class);
Mockito.verify(output, Mockito.atLeast(1)).accept(logMessage.capture()); Mockito.verify(output, Mockito.atLeast(1)).accept(any(), logMessage.capture());
Assertions.assertThat(logMessage.getAllValues()) Assertions.assertThat(logMessage.getAllValues())
.is(new MatchInOrderAmongOthers<>( .is(new MatchInOrderAmongOthers<>(
d.getUid() + " +" + d.getPid() + " +" + d.getAddress())); d.getUid() + " +" + d.getPid() + " +" + d.getAddress()));
@@ -76,7 +78,7 @@ public class StopStatusTest {
final ClientOutput output = Mockito.mock(ClientOutput.class); final ClientOutput output = Mockito.mock(ClientOutput.class);
client.execute(output, "--status").assertSuccess(); client.execute(output, "--status").assertSuccess();
final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class); final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class);
Mockito.verify(output, Mockito.atLeast(1)).accept(logMessage.capture()); Mockito.verify(output, Mockito.atLeast(1)).accept(any(), logMessage.capture());
Assertions.assertThat( Assertions.assertThat(
logMessage.getAllValues().stream() logMessage.getAllValues().stream()
.filter(m -> m.contains(d.getUid())) .filter(m -> m.contains(d.getUid()))

View File

@@ -28,6 +28,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import static org.mockito.ArgumentMatchers.any;
@MvndNativeTest(projectDir = MvndTestExtension.TEMP_EXTERNAL) @MvndNativeTest(projectDir = MvndTestExtension.TEMP_EXTERNAL)
public class VersionNativeIT { public class VersionNativeIT {
@@ -44,7 +46,7 @@ public class VersionNativeIT {
client.execute(output, "-v").assertSuccess(); client.execute(output, "-v").assertSuccess();
final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class); final ArgumentCaptor<String> logMessage = ArgumentCaptor.forClass(String.class);
Mockito.verify(output, Mockito.atLeast(1)).accept(logMessage.capture()); Mockito.verify(output, Mockito.atLeast(1)).accept(any(), logMessage.capture());
Assertions.assertThat(logMessage.getAllValues()) Assertions.assertThat(logMessage.getAllValues())
.is(new MatchInOrderAmongOthers<>( .is(new MatchInOrderAmongOthers<>(

View File

@@ -26,7 +26,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jboss.fuse.mvnd.client.Client; import org.jboss.fuse.mvnd.client.Client;
import org.jboss.fuse.mvnd.client.ClientLayout; import org.jboss.fuse.mvnd.client.ClientLayout;
import org.jboss.fuse.mvnd.client.ClientOutput; import org.jboss.fuse.mvnd.client.ClientOutput;
@@ -57,7 +56,7 @@ public class NativeTestClient implements Client {
public ExecutionResult execute(ClientOutput output, List<String> args) throws InterruptedException { public ExecutionResult execute(ClientOutput output, List<String> args) throws InterruptedException {
final List<String> cmd = new ArrayList<String>(args.size() + 1); final List<String> cmd = new ArrayList<String>(args.size() + 1);
cmd.add(mvndNativeExecutablePath.toString()); cmd.add(mvndNativeExecutablePath.toString());
args.stream().forEach(cmd::add); cmd.addAll(args);
if (!Environment.MVND_PROPERTIES_PATH.hasCommandLineProperty(args)) { if (!Environment.MVND_PROPERTIES_PATH.hasCommandLineProperty(args)) {
cmd.add(Environment.MVND_PROPERTIES_PATH.asCommandLineProperty(layout.getMvndPropertiesPath().toString())); cmd.add(Environment.MVND_PROPERTIES_PATH.asCommandLineProperty(layout.getMvndPropertiesPath().toString()));
} }
@@ -81,9 +80,9 @@ public class NativeTestClient implements Client {
if (!Environment.JAVA_HOME.hasCommandLineProperty(args)) { if (!Environment.JAVA_HOME.hasCommandLineProperty(args)) {
env.put("JAVA_HOME", System.getProperty("java.home")); env.put("JAVA_HOME", System.getProperty("java.home"));
} }
final String cmdString = cmd.stream().collect(Collectors.joining(" ")); final String cmdString = String.join(" ", cmd);
output.accept("Executing " + cmdString); output.accept(null, "Executing " + cmdString);
try (CommandProcess process = new CommandProcess(builder.start(), cmd, output)) { try (CommandProcess process = new CommandProcess(builder.start(), cmd, s -> output.accept(null, s))) {
return process.waitFor(timeoutMs); return process.waitFor(timeoutMs);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Could not execute: " + cmdString, e); throw new RuntimeException("Could not execute: " + cmdString, e);
@@ -127,7 +126,7 @@ public class NativeTestClient implements Client {
} }
sb.append("\n--- stderr+stdout start ---"); sb.append("\n--- stderr+stdout start ---");
synchronized (log) { synchronized (log) {
log.stream().forEach(s -> sb.append('\n').append(s)); log.forEach(s -> sb.append('\n').append(s));
} }
sb.append("\n--- stderr+stdout end ---"); sb.append("\n--- stderr+stdout end ---");
throw new AssertionError(sb); throw new AssertionError(sb);