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 a4aa8f4a..0b43ca8e 100644 --- a/common/src/main/java/org/mvndaemon/mvnd/common/Message.java +++ b/common/src/main/java/org/mvndaemon/mvnd/common/Message.java @@ -55,6 +55,7 @@ public abstract class Message { public static final int TRANSFER_CORRUPTED = 21; public static final int TRANSFER_SUCCEEDED = 22; public static final int TRANSFER_FAILED = 23; + public static final int EXECUTION_FAILURE = 24; final int type; @@ -103,6 +104,8 @@ public abstract class Message { case TRANSFER_SUCCEEDED: case TRANSFER_FAILED: return TransferEvent.read(type, input); + case EXECUTION_FAILURE: + return ExecutionFailureEvent.read(input); } throw new IllegalStateException("Unexpected message type: " + type); } @@ -821,6 +824,56 @@ public abstract class Message { } } + public static class ExecutionFailureEvent extends Message { + + final String projectId; + final boolean halted; + final String exception; + + private ExecutionFailureEvent(String projectId, boolean halted, String exception) { + super(EXECUTION_FAILURE); + this.projectId = projectId; + this.halted = halted; + this.exception = exception; + } + + public String getProjectId() { + return projectId; + } + + public boolean isHalted() { + return halted; + } + + public String getException() { + return exception; + } + + @Override + public String toString() { + return "ExecutionFailure{" + + "projectId='" + projectId + '\'' + + ", halted=" + halted + + ", exception='" + exception + '\'' + + '}'; + } + + @Override + public void write(DataOutputStream output) throws IOException { + super.write(output); + writeUTF(output, projectId); + output.writeBoolean(halted); + writeUTF(output, exception); + } + + public static ExecutionFailureEvent read(DataInputStream input) throws IOException { + String projectId = readUTF(input); + boolean halted = input.readBoolean(); + String exception = readUTF(input); + return new ExecutionFailureEvent(projectId, halted, exception); + } + } + public static class TransferEvent extends Message { public static final int INITIATED = 0; @@ -982,6 +1035,10 @@ public abstract class Message { return new StringMessage(PROJECT_STOPPED, projectId); } + public static ExecutionFailureEvent executionFailure(String projectId, boolean halted, String exception) { + return new ExecutionFailureEvent(projectId, halted, exception); + } + 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); 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 a2ff9940..0e712168 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 @@ -46,6 +46,7 @@ import org.jline.utils.Display; import org.mvndaemon.mvnd.common.Message; import org.mvndaemon.mvnd.common.Message.BuildException; import org.mvndaemon.mvnd.common.Message.BuildStarted; +import org.mvndaemon.mvnd.common.Message.ExecutionFailureEvent; import org.mvndaemon.mvnd.common.Message.MojoStartedEvent; import org.mvndaemon.mvnd.common.Message.ProjectEvent; import org.mvndaemon.mvnd.common.Message.StringMessage; @@ -93,6 +94,7 @@ public class TerminalOutput implements ClientOutput { private final Terminal.SignalHandler previousIntHandler; private final Display display; private final Map> transfers = new LinkedHashMap<>(); + private final ArrayList failures = new ArrayList<>(); private final LinkedHashMap projects = new LinkedHashMap<>(); private final ClientLog log; private final Thread reader; @@ -409,6 +411,11 @@ public class TerminalOutput implements ClientOutput { .remove(te.getResourceName()); break; } + case Message.EXECUTION_FAILURE: { + final ExecutionFailureEvent efe = (ExecutionFailureEvent) entry; + failures.add(efe); + break; + } default: throw new IllegalStateException("Unexpected message " + entry); } @@ -521,12 +528,18 @@ public class TerminalOutput implements ClientOutput { // so keep one more line empty at the end dispLines--; - AttributedString globalTransfer = formatTransfers(""); - dispLines -= globalTransfer != null ? 1 : 0; - addStatusLine(lines, dispLines, projectsCount); + + AttributedString globalFailure = formatFailures(); + if (globalFailure != null) { + lines.add(globalFailure); + dispLines--; + } + + AttributedString globalTransfer = formatTransfers(""); if (globalTransfer != null) { lines.add(globalTransfer); + dispLines--; } if (projectsCount <= dispLines) { @@ -561,6 +574,43 @@ public class TerminalOutput implements ClientOutput { display.update(trimmed, -1); } + private AttributedString formatFailures() { + if (failures.isEmpty()) { + return null; + } + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold()); + if (failures.stream().anyMatch(ExecutionFailureEvent::isHalted)) { + asb.append("ABORTING "); + } + asb.append("FAILURE: "); + asb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + if (failures.size() == 1) { + ExecutionFailureEvent efe = failures.iterator().next(); + asb.append(efe.getProjectId()); + String exception = efe.getException(); + if (exception != null) { + if (exception.startsWith("org.apache.maven.lifecycle.LifecycleExecutionException: ")) { + exception = exception + .substring("org.apache.maven.lifecycle.LifecycleExecutionException: ".length()); + } + asb.append(": ").append(exception); + } + } else { + asb.append(String.valueOf(failures.size())).append(" projects failed: "); + asb.append(failures.stream().map(ExecutionFailureEvent::getProjectId).collect(Collectors.joining(", "))); + } + AttributedString as = asb.toAttributedString(); + if (as.columnLength() >= getTerminalWidth() - 1) { + asb = new AttributedStringBuilder(); + asb.append(as.columnSubSequence(0, getTerminalWidth() - 2)); + asb.style(AttributedStyle.DEFAULT); + asb.append("…"); + as = asb.toAttributedString(); + } + return as; + } + private AttributedString formatTransfers(String projectId) { Collection transfers = this.transfers.getOrDefault(projectId, Collections.emptyMap()).values(); if (transfers.isEmpty()) { 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 189eeff3..4511553e 100644 --- a/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java +++ b/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java @@ -66,7 +66,6 @@ import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.exception.DefaultExceptionHandler; import org.apache.maven.exception.ExceptionHandler; import org.apache.maven.exception.ExceptionSummary; -import org.apache.maven.execution.ExecutionListener; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenExecutionRequestPopulationException; import org.apache.maven.execution.MavenExecutionRequestPopulator; @@ -177,6 +176,8 @@ public class DaemonMavenCli { private final Map configurationProcessors; + private final LoggingExecutionListener executionListener; + /** Non-volatile, assuming that it is accessed only from the main thread */ private BuildEventListener buildEventListener = BuildEventListener.dummy(); @@ -197,6 +198,7 @@ public class DaemonMavenCli { configurationProcessors = container.lookupMap(ConfigurationProcessor.class); toolchainsBuilder = container.lookup(ToolchainsBuilder.class); dispatcher = (DefaultSecDispatcher) container.lookup(SecDispatcher.class, "maven"); + executionListener = container.lookup(LoggingExecutionListener.class); } @@ -579,8 +581,10 @@ public class DaemonMavenCli { final EventSpyDispatcher eventSpyDispatcher = container.lookup(EventSpyDispatcher.class); properties(cliRequest); configure(cliRequest, eventSpyDispatcher, configurationProcessors); + LoggingExecutionListener executionListener = container.lookup(LoggingExecutionListener.class); populateRequest(cliRequest, cliRequest.request, slf4jLogger, eventSpyDispatcher, - container.lookup(ModelProcessor.class), createTransferListener(cliRequest), buildEventListener); + container.lookup(ModelProcessor.class), createTransferListener(cliRequest), buildEventListener, + executionListener); executionRequestPopulator.populateDefaults(cliRequest.request); BootstrapCoreExtensionManager resolver = container.lookup(BootstrapCoreExtensionManager.class); return Collections @@ -1110,7 +1114,7 @@ public class DaemonMavenCli { private void populateRequest(CliRequest cliRequest) { populateRequest(cliRequest, cliRequest.request, slf4jLogger, eventSpyDispatcher, modelProcessor, - createTransferListener(cliRequest), buildEventListener); + createTransferListener(cliRequest), buildEventListener, executionListener); } private static void populateRequest( @@ -1120,7 +1124,8 @@ public class DaemonMavenCli { EventSpyDispatcher eventSpyDispatcher, ModelProcessor modelProcessor, TransferListener transferListener, - BuildEventListener buildEventListener) { + BuildEventListener buildEventListener, + LoggingExecutionListener executionListener) { CommandLine commandLine = cliRequest.commandLine; String workingDirectory = cliRequest.workingDirectory; boolean showErrors = cliRequest.showErrors; @@ -1219,12 +1224,10 @@ public class DaemonMavenCli { } } - ExecutionListener executionListener = new ExecutionEventLogger(); - if (eventSpyDispatcher != null) { - executionListener = new LoggingExecutionListener( - eventSpyDispatcher.chainListener(executionListener), - buildEventListener); - } + ExecutionEventLogger executionEventLogger = new ExecutionEventLogger(); + executionListener.init( + eventSpyDispatcher.chainListener(executionEventLogger), + buildEventListener); String alternatePomFile = null; if (commandLine.hasOption(CLIManager.ALTERNATE_POM_FILE)) { diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/builder/SmartBuilder.java b/daemon/src/main/java/org/mvndaemon/mvnd/builder/SmartBuilder.java index a25c25cc..a44d3a45 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/builder/SmartBuilder.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/builder/SmartBuilder.java @@ -92,6 +92,8 @@ public class SmartBuilder implements Builder { ProjectBuildList projectBuilds, final List taskSegments, ReactorBuildStatus reactorBuildStatus) throws ExecutionException, InterruptedException { + session.getRepositorySession().getData().set(ReactorBuildStatus.class, reactorBuildStatus); + DependencyGraph graph = (DependencyGraph) session.getRequest().getData() .get(DependencyGraph.class.getName()); 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 c8b00825..0fce8af4 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/daemon/ClientDispatcher.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/daemon/ClientDispatcher.java @@ -101,6 +101,11 @@ public class ClientDispatcher extends BuildEventListener { queue.add(Message.projectStopped(projectId)); } + public void executionFailure(String projectId, boolean halted, String exception) { + projects.put(projectId, Boolean.FALSE); + queue.add(Message.executionFailure(projectId, halted, exception)); + } + public void mojoStarted(ExecutionEvent event) { final MojoExecution execution = event.getMojoExecution(); queue.add(Message.mojoStarted( 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 99810d2c..92272a77 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 @@ -36,6 +36,9 @@ public abstract class BuildEventListener { public void projectFinished(String projectId) { } + public void executionFailure(String projectId, boolean halted, String exception) { + } + public void mojoStarted(ExecutionEvent event) { } @@ -70,6 +73,8 @@ public abstract class BuildEventListener { public abstract void projectFinished(String projectId); + public abstract void executionFailure(String projectId, boolean halted, String exception); + public abstract void mojoStarted(ExecutionEvent event); public abstract void finish(int exitCode) throws Exception; diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/LoggingExecutionListener.java b/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/LoggingExecutionListener.java index 63ac6cba..a8e3a21e 100644 --- a/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/LoggingExecutionListener.java +++ b/daemon/src/main/java/org/mvndaemon/mvnd/logging/smart/LoggingExecutionListener.java @@ -15,19 +15,52 @@ */ package org.mvndaemon.mvnd.logging.smart; +import javax.inject.Named; +import javax.inject.Singleton; import org.apache.maven.execution.ExecutionEvent; import org.apache.maven.execution.ExecutionListener; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.execution.ProjectExecutionEvent; +import org.apache.maven.execution.ProjectExecutionListener; +import org.apache.maven.lifecycle.LifecycleExecutionException; +import org.apache.maven.lifecycle.internal.ReactorBuildStatus; +import org.eclipse.sisu.Typed; -public class LoggingExecutionListener implements ExecutionListener { +@Singleton +@Named +@Typed({ LoggingExecutionListener.class, ExecutionListener.class, ProjectExecutionListener.class }) +public class LoggingExecutionListener implements ExecutionListener, ProjectExecutionListener { - private final ExecutionListener delegate; - private final BuildEventListener buildEventListener; + private ExecutionListener delegate; + private BuildEventListener buildEventListener; - public LoggingExecutionListener(ExecutionListener delegate, BuildEventListener buildEventListener) { + public void init(ExecutionListener delegate, BuildEventListener buildEventListener) { this.delegate = delegate; this.buildEventListener = buildEventListener; } + @Override + public void beforeProjectExecution(ProjectExecutionEvent projectExecutionEvent) throws LifecycleExecutionException { + } + + @Override + public void beforeProjectLifecycleExecution(ProjectExecutionEvent projectExecutionEvent) + throws LifecycleExecutionException { + } + + @Override + public void afterProjectExecutionSuccess(ProjectExecutionEvent projectExecutionEvent) throws LifecycleExecutionException { + } + + @Override + public void afterProjectExecutionFailure(ProjectExecutionEvent projectExecutionEvent) { + MavenSession session = projectExecutionEvent.getSession(); + ReactorBuildStatus status = (ReactorBuildStatus) session.getRepositorySession().getData().get(ReactorBuildStatus.class); + Throwable cause = projectExecutionEvent.getCause(); + buildEventListener.executionFailure(projectExecutionEvent.getProject().getArtifactId(), + status.isHalted(), cause != null ? cause.toString() : null); + } + @Override public void projectDiscoveryStarted(ExecutionEvent event) { setMdc(event); @@ -71,6 +104,7 @@ public class LoggingExecutionListener implements ExecutionListener { @Override public void projectSkipped(ExecutionEvent event) { setMdc(event); + buildEventListener.projectStarted(event.getProject().getArtifactId()); delegate.projectSkipped(event); buildEventListener.projectFinished(event.getProject().getArtifactId()); }