Skip to content

Commit

Permalink
Provide smarter output on the client, fixes apache#77
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
gnodet committed Oct 6, 2020
1 parent 81f78a8 commit 6dd889a
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 135 deletions.
155 changes: 121 additions & 34 deletions client/src/main/java/org/jboss/fuse/mvnd/client/ClientOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,15 @@
package org.jboss.fuse.mvnd.client;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Consumer;
Expand All @@ -43,24 +41,50 @@
/**
* 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 projectFinished(String projectId);

/** Receive a log message */
public void accept(String message);
public void accept(String projectId, String message);

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.
*/
static class TerminalOutput implements ClientOutput {
private static final Logger LOGGER = LoggerFactory.getLogger(TerminalOutput.class);
private final TerminalUpdater updater;
private final BlockingQueue<Map.Entry<String, String>> queue;
private final BlockingQueue<Event> queue;

public TerminalOutput(Path logFile) throws IOException {
this.queue = new LinkedBlockingDeque<>();
Expand All @@ -69,24 +93,24 @@ public TerminalOutput(Path logFile) throws IOException {

public void projectStateChanged(String projectId, String task) {
try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(projectId, task));
queue.put(new Event(EventType.PROJECT_STATUS, projectId, task));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

public void projectFinished(String projectId) {
try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(projectId, null));
queue.put(new Event(EventType.PROJECT_STATUS, projectId, null));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

@Override
public void accept(String message) {
public void accept(String projectId, String message) {
try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(TerminalUpdater.LOG, message));
queue.put(new Event(EventType.LOG, projectId, message));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Expand All @@ -106,65 +130,117 @@ public void error(BuildException error) {
msg = error.getClassName() + ": " + error.getMessage();
}
try {
queue.put(new AbstractMap.SimpleImmutableEntry<>(TerminalUpdater.ERROR, msg));
queue.put(new Event(EventType.ERROR, null, msg));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

static class TerminalUpdater implements AutoCloseable {
private static final String LOG = "<log>";
private static final String ERROR = "<error>";
private static final String END_OF_STREAM = "<eos>";
private final BlockingQueue<Map.Entry<String, String>> queue;
private final BlockingQueue<Event> queue;
private final Terminal terminal;
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 Thread worker;
private final Thread reader;
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();
this.terminal = TerminalBuilder.terminal();
terminal.enterRawMode();
this.display = new Display(terminal, false);
this.log = logFile == null ? new ClientOutput.Log.MessageCollector(terminal)
: new ClientOutput.Log.FileLog(logFile);
this.queue = queue;
final Thread w = new Thread(this::run);
w.start();
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() {
final List<Entry<String, String>> entries = new ArrayList<>();
final List<Event> entries = new ArrayList<>();

while (true) {
try {
entries.add(queue.take());
queue.drainTo(entries);
for (Entry<String, String> entry : entries) {
final String key = entry.getKey();
final String value = entry.getValue();
if (key == END_OF_STREAM) {
for (Event entry : entries) {
switch (entry.type) {
case END_OF_STREAM: {
projects.values().stream().flatMap(p -> p.log.stream()).forEach(log);
display.update(Collections.emptyList(), 0);
LOGGER.debug("Done receiving, printing log");
log.close();
LOGGER.debug("Done !");
terminal.flush();
return;
} else if (key == LOG) {
log.accept(value);
} else if (key == ERROR) {
}
case LOG: {
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);
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();
return;
} else if (value == null) {
projects.remove(key);
} else {
projects.put(key, value);
}
case PROJECT_STATUS:
if (entry.message != null) {
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();
Expand All @@ -179,8 +255,12 @@ void run() {

@Override
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();
reader.join();
terminal.close();
if (exception != null) {
throw exception;
}
Expand All @@ -197,15 +277,22 @@ private void update() {
}
final int displayableProjectCount = rows - 1;
final int skipRows = projects.size() > displayableProjectCount ? projects.size() - displayableProjectCount : 0;
final int remRows = displayableProjectCount > projects.size() ? displayableProjectCount - projects.size() : 0;
final List<AttributedString> lines = new ArrayList<>(projects.size() - skipRows);
final int lineMaxLength = size.getColumns();
int i = 0;
int j = 0;
lines.add(new AttributedString("Building..." + (skipRows > 0 ? " (" + skipRows + " more)" : "")));
for (String line : projects.values()) {
for (Project prj : projects.values()) {
if (i < skipRows) {
i++;
} else {
lines.add(shortenIfNeeded(AttributedString.fromAnsi(line), lineMaxLength));
lines.add(shortenIfNeeded(AttributedString.fromAnsi(prj.status), lineMaxLength));
for (String line : prj.log.subList(Math.max(0, prj.log.size() - linesPerProject), prj.log.size())) {
if (j++ < remRows) {
lines.add(shortenIfNeeded(AttributedString.fromAnsi(" " + line), lineMaxLength));
}
}
}
}
display.update(lines, -1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public ExecutionResult execute(ClientOutput output, List<String> argv) {
+ "-" + buildProperties.getOsArch()
+ nativeSuffix)
.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
* Maven artifacts
Expand All @@ -140,9 +140,9 @@ public ExecutionResult execute(ClientOutput output, List<String> argv) {
try (DaemonRegistry registry = new DaemonRegistry(layout.registry())) {
boolean status = args.remove("--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"));
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(),
LocalDateTime.ofInstant(
Instant.ofEpochMilli(Math.max(d.getLastIdle(), d.getLastBusy())),
Expand All @@ -153,7 +153,7 @@ public ExecutionResult execute(ClientOutput output, List<String> argv) {
if (stop) {
DaemonInfo[] dis = registry.getAll().toArray(new DaemonInfo[0]);
if (dis.length > 0) {
output.accept("Stopping " + dis.length + " running daemons");
output.accept(null, "Stopping " + dis.length + " running daemons");
for (DaemonInfo di : dis) {
try {
ProcessHandle.of(di.getPid()).ifPresent(ProcessHandle::destroyForcibly);
Expand Down Expand Up @@ -204,15 +204,19 @@ public ExecutionResult execute(ClientOutput output, List<String> argv) {
return new DefaultResult(argv, null);
case ProjectStarted:
case MojoStarted:
output.projectStateChanged(be.getProjectId(), be.getDisplay());
break;
case MojoStopped:
output.projectStateChanged(be.getProjectId(), be.getDisplay());
output.projectStateChanged(be.getProjectId(), ":" + be.getProjectId());
break;
case ProjectStopped:
output.projectFinished(be.getProjectId());
break;
}
} else if (m instanceof BuildMessage) {
BuildMessage bm = (BuildMessage) m;
output.accept(bm.getMessage());
output.accept(bm.getProjectId(), bm.getMessage());
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
12 changes: 10 additions & 2 deletions common/src/main/java/org/jboss/fuse/mvnd/common/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,22 @@ public String toString() {
}

public static class BuildMessage extends Message {
final String projectId;
final String message;

public BuildMessage(String message) {
public BuildMessage(String projectId, String message) {
this.projectId = projectId;
this.message = message;
}

public String getMessage() {
return message;
}

public String getProjectId() {
return projectId;
}

@Override
public String toString() {
return "BuildMessage{" +
Expand Down Expand Up @@ -236,11 +242,13 @@ private void writeBuildEvent(DataOutputStream output, BuildEvent value) throws I
}

private BuildMessage readBuildMessage(DataInputStream input) throws IOException {
String projectId = 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 {
writeUTF(output, value.projectId != null ? value.projectId : "");
writeUTF(output, value.message);
}

Expand Down
Loading

0 comments on commit 6dd889a

Please sign in to comment.