diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/Message.java b/common/src/main/java/org/mvndaemon/mvnd/common/Message.java index db03414b..48b2bce2 100644 --- a/common/src/main/java/org/mvndaemon/mvnd/common/Message.java +++ b/common/src/main/java/org/mvndaemon/mvnd/common/Message.java @@ -48,6 +48,12 @@ public abstract class Message { public static final int BUILD_STATUS = 14; public static final int KEYBOARD_INPUT = 15; public static final int CANCEL_BUILD = 16; + public static final int TRANSFER_INITIATED = 17; + public static final int TRANSFER_STARTED = 18; + public static final int TRANSFER_PROGRESSED = 19; + public static final int TRANSFER_CORRUPTED = 20; + public static final int TRANSFER_SUCCEEDED = 21; + public static final int TRANSFER_FAILED = 22; public static final BareMessage KEEP_ALIVE_SINGLETON = new BareMessage(KEEP_ALIVE); public static final BareMessage STOP_SINGLETON = new BareMessage(STOP); @@ -93,6 +99,13 @@ public abstract class Message { return StringMessage.read(type, input); case CANCEL_BUILD: return BareMessage.CANCEL_BUILD_SINGLETON; + case TRANSFER_INITIATED: + case TRANSFER_STARTED: + case TRANSFER_PROGRESSED: + case TRANSFER_CORRUPTED: + case TRANSFER_SUCCEEDED: + case TRANSFER_FAILED: + return TransferEvent.read(type, input); } throw new IllegalStateException("Unexpected message type: " + type); } @@ -760,6 +773,135 @@ public abstract class Message { } } + public static class TransferEvent extends Message { + + public static final int INITIATED = 0; + public static final int STARTED = 1; + public static final int PROGRESSED = 2; + public static final int CORRUPTED = 3; + public static final int SUCCEEDED = 4; + public static final int FAILED = 5; + + public static final int GET = 0; + public static final int GET_EXISTENCE = 1; + public static final int PUT = 2; + + final String projectId; + final int request; + final String repositoryId; + final String repositoryUrl; + final String resourceName; + final long contentLength; + final long transferredBytes; + final String exception; + + private TransferEvent(int type, String projectId, int request, + String repositoryId, String repositoryUrl, + String resourceName, long contentLength, long transferredBytes, + String exception) { + super(type); + this.projectId = projectId; + this.request = request; + this.repositoryId = repositoryId; + this.repositoryUrl = repositoryUrl; + this.resourceName = resourceName; + this.contentLength = contentLength; + this.transferredBytes = transferredBytes; + this.exception = exception; + } + + public String getProjectId() { + return projectId; + } + + public int getRequest() { + return request; + } + + public String getRepositoryId() { + return repositoryId; + } + + public String getRepositoryUrl() { + return repositoryUrl; + } + + public String getResourceName() { + return resourceName; + } + + public long getContentLength() { + return contentLength; + } + + public long getTransferredBytes() { + return transferredBytes; + } + + public String getException() { + return exception; + } + + @Override + public String toString() { + return mnemonic() + "{" + + "projectId=" + projectId + + ", request=" + request + + ", repositoryId='" + repositoryId + '\'' + + ", repositoryUrl='" + repositoryUrl + '\'' + + ", resourceName='" + resourceName + '\'' + + ", contentLength=" + contentLength + + ", transferredBytes=" + transferredBytes + + ", exception='" + exception + '\'' + + '}'; + } + + private String mnemonic() { + switch (type) { + case TRANSFER_INITIATED: + return "TransferInitiated"; + case TRANSFER_STARTED: + return "TransferStarted"; + case TRANSFER_PROGRESSED: + return "TransferProgressed"; + case TRANSFER_CORRUPTED: + return "TransferCorrupted"; + case TRANSFER_SUCCEEDED: + return "TransferSucceeded"; + case TRANSFER_FAILED: + return "TransferFailed"; + default: + throw new IllegalStateException("Unexpected type " + type); + } + } + + @Override + public void write(DataOutputStream output) throws IOException { + super.write(output); + writeUTF(output, projectId); + output.writeByte(request); + writeUTF(output, repositoryId); + writeUTF(output, repositoryUrl); + writeUTF(output, resourceName); + output.writeLong(contentLength); + output.writeLong(transferredBytes); + writeUTF(output, exception); + } + + public static TransferEvent read(int type, DataInputStream input) throws IOException { + String projectId = readUTF(input); + int request = input.readByte(); + String repositoryId = readUTF(input); + String repositoryUrl = readUTF(input); + String resourceName = readUTF(input); + long contentLength = input.readLong(); + long transferredBytes = input.readLong(); + String exception = readUTF(input); + return new TransferEvent(type, projectId, request, repositoryId, repositoryUrl, resourceName, + contentLength, transferredBytes, exception); + } + } + public int getType() { return type; } @@ -785,7 +927,7 @@ public abstract class Message { } public static StringMessage projectStarted(String projectId) { - return new StringMessage(Message.PROJECT_STARTED, projectId); + return new StringMessage(PROJECT_STARTED, projectId); } public static StringMessage projectStopped(String projectId) { @@ -795,11 +937,18 @@ public abstract class Message { public static Message mojoStarted(String artifactId, String pluginGroupId, String pluginArtifactId, String pluginVersion, String mojo, String executionId) { return new MojoStartedEvent(artifactId, pluginGroupId, pluginArtifactId, pluginVersion, mojo, executionId); - } public static ProjectEvent display(String projectId, String message) { return new ProjectEvent(Message.DISPLAY, projectId, message); } + public static TransferEvent transfer(String projectId, int event, int request, + String repositoryId, String repositoryUrl, + String resourceName, long contentLength, long transferredBytes, + String exception) { + return new TransferEvent(event, projectId, request, + repositoryId, repositoryUrl, resourceName, contentLength, transferredBytes, exception); + } + } diff --git a/common/src/main/java/org/mvndaemon/mvnd/common/logging/TerminalOutput.java b/common/src/main/java/org/mvndaemon/mvnd/common/logging/TerminalOutput.java index f0906703..e5e913f8 100644 --- a/common/src/main/java/org/mvndaemon/mvnd/common/logging/TerminalOutput.java +++ b/common/src/main/java/org/mvndaemon/mvnd/common/logging/TerminalOutput.java @@ -23,10 +23,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -47,6 +49,8 @@ import org.mvndaemon.mvnd.common.Message.BuildStarted; import org.mvndaemon.mvnd.common.Message.MojoStartedEvent; import org.mvndaemon.mvnd.common.Message.ProjectEvent; import org.mvndaemon.mvnd.common.Message.StringMessage; +import org.mvndaemon.mvnd.common.Message.TransferEvent; +import org.mvndaemon.mvnd.common.OsUtils; /** * A terminal {@link ClientOutput} based on JLine. @@ -62,6 +66,7 @@ public class TerminalOutput implements ClientOutput { private final Terminal terminal; private final Terminal.SignalHandler previousIntHandler; private final Display display; + private final Map> transfers = new LinkedHashMap<>(); private final LinkedHashMap projects = new LinkedHashMap<>(); private final ClientLog log; private final Thread reader; @@ -353,6 +358,22 @@ public class TerminalOutput implements ClientOutput { } break; } + case Message.TRANSFER_INITIATED: + case Message.TRANSFER_STARTED: + case Message.TRANSFER_PROGRESSED: { + final TransferEvent te = (TransferEvent) entry; + transfers.computeIfAbsent(orEmpty(te.getProjectId()), p -> new LinkedHashMap<>()) + .put(te.getResourceName(), te); + break; + } + case Message.TRANSFER_CORRUPTED: + case Message.TRANSFER_SUCCEEDED: + case Message.TRANSFER_FAILED: { + final TransferEvent te = (TransferEvent) entry; + transfers.computeIfAbsent(orEmpty(te.getProjectId()), p -> new LinkedHashMap<>()) + .remove(te.getResourceName()); + break; + } default: throw new IllegalStateException("Unexpected message " + entry); } @@ -360,6 +381,10 @@ public class TerminalOutput implements ClientOutput { return true; } + private String orEmpty(String s) { + return s != null ? s : ""; + } + private void applyNoBuffering() { projects.values().stream().flatMap(p -> p.log.stream()).forEach(log); projects.clear(); @@ -454,12 +479,22 @@ public class TerminalOutput implements ClientOutput { return; } final List lines = new ArrayList<>(rows); - int dispLines = rows - 1; // for the build status line - dispLines--; // there's a bug which sometimes make the cursor goes one line below, so keep one more line empty - // at the end final int projectsCount = projects.size(); + int dispLines = rows; + // status line + dispLines--; + // there's a bug which sometimes make the cursor goes one line below, + // so keep one more line empty at the end + dispLines--; + + AttributedString globalTransfer = formatTransfers(""); + dispLines -= globalTransfer != null ? 1 : 0; + addStatusLine(lines, dispLines, projectsCount); + if (globalTransfer != null) { + lines.add(globalTransfer); + } if (projectsCount <= dispLines) { int remLogLines = dispLines - projectsCount; @@ -490,6 +525,74 @@ public class TerminalOutput implements ClientOutput { display.update(trimmed, -1); } + private AttributedString formatTransfers(String projectId) { + Collection transfers = this.transfers.getOrDefault(projectId, Collections.emptyMap()).values(); + if (transfers.isEmpty()) { + return null; + } + TransferEvent event = transfers.iterator().next(); + String action = event.getRequest() == TransferEvent.PUT ? "Uploading" : "Downloading"; + if (transfers.size() == 1) { + String direction = event.getRequest() == TransferEvent.PUT ? "to" : "from"; + long cur = event.getTransferredBytes(); + long max = event.getContentLength(); + String prg = OsUtils.kbTohumanReadable(cur / 1024) + " / " + OsUtils.kbTohumanReadable(max / 1024); + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(action); + asb.append(" "); + asb.style(AttributedStyle.BOLD); + asb.append(pathToMaven(event.getResourceName())); + asb.style(AttributedStyle.DEFAULT); + asb.append(" "); + asb.append(direction); + asb.append(" "); + asb.append(event.getRepositoryId()); + if (cur > 0 && cur < max) { + asb.append(": "); + asb.append(prg); + } + return asb.toAttributedString(); + } else { + return new AttributedString(action + " " + transfers.size() + " files..."); + } + } + + public static String pathToMaven(String location) { + String[] p = location.split("/"); + if (p.length >= 4 && p[p.length - 1].startsWith(p[p.length - 3] + "-" + p[p.length - 2])) { + String artifactId = p[p.length - 3]; + String version = p[p.length - 2]; + String classifier; + String type; + String artifactIdVersion = artifactId + "-" + version; + StringBuilder sb = new StringBuilder(); + if (p[p.length - 1].charAt(artifactIdVersion.length()) == '-') { + classifier = p[p.length - 1].substring(artifactIdVersion.length() + 1, p[p.length - 1].lastIndexOf('.')); + } else { + classifier = null; + } + type = p[p.length - 1].substring(p[p.length - 1].lastIndexOf('.') + 1); + for (int j = 0; j < p.length - 3; j++) { + if (j > 0) { + sb.append('.'); + } + sb.append(p[j]); + } + sb.append(':').append(artifactId).append(':').append(version); + if (!"jar".equals(type) || classifier != null) { + sb.append(':'); + if (!"jar".equals(type)) { + sb.append(type); + } + if (classifier != null) { + sb.append(':').append(classifier); + } + } + return sb.toString(); + } + return location; + } + private void addStatusLine(final List lines, int dispLines, final int projectsCount) { if (name != null || buildStatus != null) { AttributedStringBuilder asb = new AttributedStringBuilder(); @@ -548,7 +651,13 @@ public class TerminalOutput implements ClientOutput { private void addProjectLine(final List lines, Project prj) { final MojoStartedEvent execution = prj.runningExecution; final AttributedStringBuilder asb = new AttributedStringBuilder(); - if (execution == null) { + AttributedString transfer = formatTransfers(prj.id); + if (transfer != null) { + asb + .append(':') + .append(String.format(artifactIdFormat, prj.id)) + .append(transfer); + } else if (execution == null) { asb .append(':') .append(prj.id); 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 34179a44..0dc9cc26 100644 --- a/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java +++ b/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java @@ -94,6 +94,7 @@ import org.mvndaemon.mvnd.logging.internal.Slf4jLoggerManager; import org.mvndaemon.mvnd.logging.smart.BuildEventListener; import org.mvndaemon.mvnd.logging.smart.LoggingExecutionListener; import org.mvndaemon.mvnd.logging.smart.LoggingOutputStream; +import org.mvndaemon.mvnd.transfer.DaemonMavenTransferListener; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1301,14 +1302,14 @@ public class DaemonMavenCli { // If we're logging to a file then we don't want the console transfer listener as it will spew // download progress all over the place // - return getConsoleTransferListener(cliRequest.commandLine.hasOption(CLIManager.DEBUG)); + return getDaemonTransferListener(); } else { return getBatchTransferListener(); } } - protected TransferListener getConsoleTransferListener(boolean printResourceNames) { - return new Slf4jMavenTransferListener(); // see https://github.com/mvndaemon/mvnd/issues/284 + protected TransferListener getDaemonTransferListener() { + return new DaemonMavenTransferListener(buildEventListener); } protected TransferListener getBatchTransferListener() { diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/ClientDispatcher.java b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/ClientDispatcher.java index b9509dcf..672f0baa 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/ClientDispatcher.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/ClientDispatcher.java @@ -25,6 +25,7 @@ import org.apache.maven.execution.ExecutionEvent; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.project.MavenProject; +import org.eclipse.aether.transfer.TransferEvent; import org.mvndaemon.mvnd.builder.DependencyGraph; import org.mvndaemon.mvnd.common.Message; import org.mvndaemon.mvnd.common.Message.BuildException; @@ -112,6 +113,41 @@ public class ClientDispatcher extends BuildEventListener { queue.add(Message.log(trimTrailingEols(msg))); } + public void transfer(String projectId, TransferEvent e) { + int event; + switch (e.getType()) { + case INITIATED: + event = Message.TRANSFER_INITIATED; + break; + case STARTED: + event = Message.TRANSFER_STARTED; + break; + case PROGRESSED: + event = Message.TRANSFER_PROGRESSED; + break; + case CORRUPTED: + event = Message.TRANSFER_CORRUPTED; + break; + case SUCCEEDED: + event = Message.TRANSFER_SUCCEEDED; + break; + case FAILED: + event = Message.TRANSFER_FAILED; + break; + default: + throw new IllegalStateException(); + } + int request = e.getRequestType().ordinal(); + String repositoryId = e.getResource().getRepositoryId(); + String repositoryUrl = e.getResource().getRepositoryUrl(); + String resourceName = e.getResource().getResourceName(); + long contentLength = e.getResource().getContentLength(); + long transferredBytes = e.getTransferredBytes(); + String exception = e.getException() != null ? e.getException().toString() : null; + queue.add(Message.transfer(projectId, event, request, repositoryId, repositoryUrl, resourceName, + contentLength, transferredBytes, exception)); + } + private MavenProject getCurrentProject(MavenSession mavenSession) { // Workaround for https://issues.apache.org/jira/browse/MNG-6979 // MavenSession.getCurrentProject() does not return the correct value in some cases diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java index 44291dab..eebb59eb 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java @@ -567,6 +567,15 @@ public class Server implements AutoCloseable, Runnable { return 3; case Message.MOJO_STARTED: return 4; + case Message.TRANSFER_INITIATED: + case Message.TRANSFER_STARTED: + return 40; + case Message.TRANSFER_PROGRESSED: + return 41; + case Message.TRANSFER_CORRUPTED: + case Message.TRANSFER_SUCCEEDED: + case Message.TRANSFER_FAILED: + return 42; case Message.PROJECT_LOG_MESSAGE: return 50; case Message.BUILD_LOG_MESSAGE: diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/BuildEventListener.java b/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/BuildEventListener.java index 48db5ec6..32396a6e 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/BuildEventListener.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/BuildEventListener.java @@ -16,6 +16,7 @@ package org.mvndaemon.mvnd.logging.smart; import org.apache.maven.execution.ExecutionEvent; +import org.eclipse.aether.transfer.TransferEvent; /** * An abstract build event sink. @@ -47,6 +48,8 @@ public abstract class BuildEventListener { public void log(String msg) { } + public void transfer(String projectId, TransferEvent e) { + } }; /** @@ -74,4 +77,6 @@ public abstract class BuildEventListener { public abstract void fail(Throwable t) throws Exception; public abstract void log(String msg); + + public abstract void transfer(String projectId, TransferEvent e); } diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/transfer/DaemonMavenTransferListener.java b/daemon/src/main/java/org/mvndaemon/mvnd/transfer/DaemonMavenTransferListener.java new file mode 100644 index 00000000..b61c4c3a --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/transfer/DaemonMavenTransferListener.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 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.mvndaemon.mvnd.transfer; + +import org.eclipse.aether.transfer.TransferCancelledException; +import org.eclipse.aether.transfer.TransferEvent; +import org.eclipse.aether.transfer.TransferListener; +import org.mvndaemon.mvnd.logging.smart.BuildEventListener; +import org.mvndaemon.mvnd.logging.smart.ProjectBuildLogAppender; + +public class DaemonMavenTransferListener implements TransferListener { + + private final BuildEventListener dispatcher; + + public DaemonMavenTransferListener(BuildEventListener dispatcher) { + this.dispatcher = dispatcher; + } + + @Override + public void transferInitiated(TransferEvent event) throws TransferCancelledException { + dispatcher.transfer(ProjectBuildLogAppender.getProjectId(), event); + } + + @Override + public void transferStarted(TransferEvent event) throws TransferCancelledException { + dispatcher.transfer(ProjectBuildLogAppender.getProjectId(), event); + } + + @Override + public void transferProgressed(TransferEvent event) throws TransferCancelledException { + dispatcher.transfer(ProjectBuildLogAppender.getProjectId(), event); + } + + @Override + public void transferCorrupted(TransferEvent event) throws TransferCancelledException { + dispatcher.transfer(ProjectBuildLogAppender.getProjectId(), event); + } + + @Override + public void transferSucceeded(TransferEvent event) { + dispatcher.transfer(ProjectBuildLogAppender.getProjectId(), event); + } + + @Override + public void transferFailed(TransferEvent event) { + dispatcher.transfer(ProjectBuildLogAppender.getProjectId(), event); + } +}