From 716fa0d0d3bbe15c6cd77c63171c147ab8974fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Sun, 3 Mar 2024 19:31:29 +0100 Subject: [PATCH] Make the remove maven listener a process in the launch Currently m2e uses an additional socket connection to receive project events from the remote maven project. This is not visible to the user and requires some internal maps to maintain state. This refactors this into a more generic event facility and make it visible as a Process in the debug view, this also has the advantage that the process itself can be used to maintain the state. --- m2e-core-tests | 2 +- org.eclipse.m2e.launching/plugin.xml | 9 + ...MavenBuildConnectionLaunchParticipant.java | 79 +++++++++ .../launch/MavenBuildConnectionProcess.java | 155 +++++++++++++++++ .../MavenBuildProjectDataConnection.java | 107 ------------ .../launch/MavenConsoleLineTracker.java | 39 +++-- .../launch/MavenLaunchExtensionsSupport.java | 1 - org.eclipse.m2e.maven.runtime/pom.xml | 2 +- .../listener/M2EMavenBuildDataBridge.java | 158 +++++------------- .../internal/maven/listener/M2eEventSpy.java | 58 +++++++ .../maven/listener/MavenBuildConnection.java | 46 +++++ .../MavenBuildConnectionListener.java | 23 +++ .../maven/listener/MavenProjectBuildData.java | 79 +++++++++ pom.xml | 2 +- 14 files changed, 515 insertions(+), 245 deletions(-) create mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionLaunchParticipant.java create mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java delete mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2eEventSpy.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnection.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnectionListener.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java diff --git a/m2e-core-tests b/m2e-core-tests index 813942ef2f..9feae7ba91 160000 --- a/m2e-core-tests +++ b/m2e-core-tests @@ -1 +1 @@ -Subproject commit 813942ef2fbc42c989f2104d59d213aa70340d12 +Subproject commit 9feae7ba91aba0894ba76b9fca1b0f33f186c3de diff --git a/org.eclipse.m2e.launching/plugin.xml b/org.eclipse.m2e.launching/plugin.xml index fec1e03370..e44cc75756 100644 --- a/org.eclipse.m2e.launching/plugin.xml +++ b/org.eclipse.m2e.launching/plugin.xml @@ -243,5 +243,14 @@ class="org.eclipse.m2e.internal.launch.MavenConsoleLineTracker" processType="java"/> + + + + diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionLaunchParticipant.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionLaunchParticipant.java new file mode 100644 index 0000000000..ac3426a8c1 --- /dev/null +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionLaunchParticipant.java @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2022, 2024 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + * Christoph Läubrich - refactor into {@link IMavenLaunchParticipant} + ********************************************************************************/ + +package org.eclipse.m2e.internal.launch; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchesListener2; +import org.eclipse.debug.core.sourcelookup.ISourceLookupParticipant; + +import org.eclipse.m2e.core.internal.launch.MavenEmbeddedRuntime; +import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge; + + +public class MavenBuildConnectionLaunchParticipant implements IMavenLaunchParticipant { + + static { + DebugPlugin.getDefault().getLaunchManager().addLaunchListener(new ILaunchesListener2() { + public void launchesRemoved(ILaunch[] launches) { + ensureClosed(launches); + } + + private void ensureClosed(ILaunch[] launches) { + Arrays.stream(launches).flatMap(l -> MavenBuildConnectionProcess.get(l).stream()) + .forEach(MavenBuildConnectionProcess::terminate); + } + + public void launchesTerminated(ILaunch[] launches) { + ensureClosed(launches); + } + + public void launchesAdded(ILaunch[] launches) { // ignore + } + + public void launchesChanged(ILaunch[] launches) { // ignore + } + }); + } + + public String getProgramArguments(ILaunchConfiguration configuration, ILaunch launch, IProgressMonitor monitor) { + try { + if(MavenLaunchUtils.getMavenRuntime(launch.getLaunchConfiguration()) instanceof MavenEmbeddedRuntime) { + + MavenBuildConnectionProcess process = new MavenBuildConnectionProcess(launch); + return M2EMavenBuildDataBridge.prepareConnection(launch.getLaunchConfiguration().getName(), process); + } + } catch(CoreException | IOException ex) { // ignore + } + return null; + } + + public String getVMArguments(ILaunchConfiguration configuration, ILaunch launch, IProgressMonitor monitor) { + return null; + } + + public List getSourceLookupParticipants(ILaunchConfiguration configuration, ILaunch launch, + IProgressMonitor monitor) { + return List.of(); + } + +} diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java new file mode 100644 index 0000000000..cac21828cf --- /dev/null +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java @@ -0,0 +1,155 @@ +/******************************************************************************** + * Copyright (c) 2024 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + ********************************************************************************/ + +package org.eclipse.m2e.internal.launch; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.debug.core.DebugEvent; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.debug.core.model.IStreamsProxy; + +import org.eclipse.m2e.core.embedder.ArtifactKey; +import org.eclipse.m2e.internal.maven.listener.MavenBuildConnection; +import org.eclipse.m2e.internal.maven.listener.MavenBuildConnectionListener; +import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData; + + +/** + * This is the representation of the MavenBuildConnection we use to communicate with the remote maven process. + */ +public class MavenBuildConnectionProcess implements IProcess, MavenBuildConnectionListener { + + Map> projects = new ConcurrentHashMap<>(); + + private ILaunch launch; + + private MavenBuildConnection connection; + + private Map attributes = new HashMap<>(); + + private AtomicBoolean terminated = new AtomicBoolean(); + + private String label; + + /** + * @param launch + */ + public MavenBuildConnectionProcess(ILaunch launch) { + this.launch = launch; + } + + public T getAdapter(Class adapter) { + return null; + } + + public boolean canTerminate() { + return false; + } + + public boolean isTerminated() { + return connection != null && connection.isCompleted(); + } + + public void terminate() { + if(connection != null && terminated.compareAndSet(false, true)) { + connection.close(); + fireEvent(new DebugEvent(this, DebugEvent.TERMINATE)); + for(CompletableFuture future : projects.values()) { + future.cancel(true); + } + } + } + + public String getLabel() { + // TODO fetch the maven version from the remove process like mvn -V ... + if(label != null) { + return "Maven<" + label + ">"; + } + return "Maven"; + } + + public ILaunch getLaunch() { + return launch; + } + + public IStreamsProxy getStreamsProxy() { + return null; + } + + public void setAttribute(String key, String value) { + attributes.put(key, value); + fireEvent(new DebugEvent(this, DebugEvent.CHANGE)); + } + + public String getAttribute(String key) { + return attributes.get(key); + } + + public int getExitValue() { + return 0; + } + + private static void fireEvent(DebugEvent event) { + DebugPlugin manager = DebugPlugin.getDefault(); + if(manager != null) { + manager.fireDebugEventSet(new DebugEvent[] {event}); + } + } + + public static Optional get(ILaunch launch) { + for(IProcess process : launch.getProcesses()) { + if(process instanceof MavenBuildConnectionProcess p) { + return Optional.of(p); + } + } + return Optional.empty(); + } + + /** + * @param launch2 + * @param groupId + * @param artifactId + * @param version + * @return + */ + public CompletableFuture getBuildProject(String groupId, String artifactId, String version) { + return projects.computeIfAbsent(new ArtifactKey(groupId, artifactId, version, null), + x -> new CompletableFuture<>()); + } + + public void onOpen(String label, MavenBuildConnection connection) { + this.label = label; + this.connection = connection; + getLaunch().addProcess(this); + fireEvent(new DebugEvent(this, DebugEvent.CREATE)); + + } + + public void onClose() { + terminate(); + } + + public void onData(MavenProjectBuildData buildData) { + projects.computeIfAbsent(new ArtifactKey(buildData.groupId, buildData.artifactId, buildData.version, null), x -> new CompletableFuture<>()) + .complete(buildData); + } + +} diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java deleted file mode 100644 index cd9abfbcd0..0000000000 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java +++ /dev/null @@ -1,107 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2022, 2022 Hannes Wellmann and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Hannes Wellmann - initial API and implementation - ********************************************************************************/ - -package org.eclipse.m2e.internal.launch; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.debug.core.DebugPlugin; -import org.eclipse.debug.core.ILaunch; -import org.eclipse.debug.core.ILaunchesListener2; - -import org.eclipse.m2e.core.embedder.ArtifactKey; -import org.eclipse.m2e.core.internal.launch.MavenEmbeddedRuntime; -import org.eclipse.m2e.internal.launch.MavenRuntimeLaunchSupport.VMArguments; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge.MavenBuildConnection; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge.MavenProjectBuildData; - - -public class MavenBuildProjectDataConnection { - - private static record MavenBuildConnectionData(Map projects, - MavenBuildConnection connection) { - } - - private static final Map LAUNCH_PROJECT_DATA = new ConcurrentHashMap<>(); - - static { - DebugPlugin.getDefault().getLaunchManager().addLaunchListener(new ILaunchesListener2() { - public void launchesRemoved(ILaunch[] launches) { - closeServers(Arrays.stream(launches).map(LAUNCH_PROJECT_DATA::remove)); - } - - public void launchesTerminated(ILaunch[] launches) { - closeServers(Arrays.stream(launches).map(LAUNCH_PROJECT_DATA::get)); - } - - private static void closeServers(Stream connectionData) { - connectionData.filter(Objects::nonNull).forEach(c -> { - try { - c.connection().close(); - } catch(IOException ex) { // ignore - } - }); - } - - public void launchesAdded(ILaunch[] launches) { // ignore - } - - public void launchesChanged(ILaunch[] launches) { // ignore - } - }); - } - - static void openListenerConnection(ILaunch launch, VMArguments arguments) { - try { - if(MavenLaunchUtils.getMavenRuntime(launch.getLaunchConfiguration()) instanceof MavenEmbeddedRuntime) { - - Map projects = new ConcurrentHashMap<>(); - - MavenBuildConnection connection = M2EMavenBuildDataBridge.prepareConnection( - launch.getLaunchConfiguration().getName(), - d -> projects.put(new ArtifactKey(d.groupId, d.artifactId, d.version, null), d)); - - if(LAUNCH_PROJECT_DATA.putIfAbsent(launch, new MavenBuildConnectionData(projects, connection)) != null) { - connection.close(); - throw new IllegalStateException( - "Maven bridge already created for launch of" + launch.getLaunchConfiguration().getName()); - } - arguments.append(connection.getMavenVMArguments()); - } - } catch(CoreException | IOException ex) { // ignore - } - } - - static MavenProjectBuildData getBuildProject(ILaunch launch, String groupId, String artifactId, String version) { - MavenBuildConnectionData build = LAUNCH_PROJECT_DATA.get(launch); - if(build == null) { - return null; - } - ArtifactKey key = new ArtifactKey(groupId, artifactId, version, null); - while(true) { - MavenProjectBuildData buildProject = build.projects().get(key); - if(buildProject != null || build.connection().isReadCompleted()) { - return buildProject; - } - Thread.onSpinWait(); // Await completion of project data read. It has to become available soon, since its GAV was printed on the console - } - } - -} diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java index 923df503c7..db9fcc4116 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java @@ -29,6 +29,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -65,7 +67,7 @@ import org.eclipse.m2e.core.internal.IMavenConstants; import org.eclipse.m2e.core.project.IBuildProjectFileResolver; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge.MavenProjectBuildData; +import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData; /** @@ -227,10 +229,13 @@ private String getText(IRegion lineRegion) throws BadLocationException { if(gaMatcher.matches()) { String groupId = gaMatcher.group(GROUP_ID); String artifactId = gaMatcher.group(ARTIFACT_ID); - - mavenProject = getProject(groupId, artifactId, version); - if(mavenProject != null) { - addProjectLink(line3Region, gaMatcher, GROUP_ID, ARTIFACT_ID, removedLine3Locations); + try { + mavenProject = getProject(groupId, artifactId, version).join(); //TODO can we do this with future notification? + if(mavenProject != null) { + addProjectLink(line3Region, gaMatcher, GROUP_ID, ARTIFACT_ID, removedLine3Locations); + } + } catch(CancellationException e) { + mavenProject = null; } } } @@ -265,18 +270,18 @@ private void addProjectLink(IRegion line, Matcher matcher, int startGroup, int e console.addLink(link, line.getOffset() + start, end - start); } - private ProjectReference getProject(String groupId, String artifactId, String version) { - MavenProjectBuildData buildProject = MavenBuildProjectDataConnection.getBuildProject(launch, groupId, artifactId, - version); - if(buildProject == null) { - return null; - } - IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); - URI basedirURI = buildProject.projectBasedir.toUri(); - Optional project = Arrays.stream(wsRoot.findContainersForLocationURI(basedirURI)) - .filter(IProject.class::isInstance).map(IProject.class::cast).findFirst(); - //if project is absent, the project build in Maven is not in the workspace - return project.isPresent() ? new ProjectReference(project.get(), buildProject) : null; + private CompletableFuture getProject(String groupId, String artifactId, String version) { + return MavenBuildConnectionProcess.get(launch).map(process -> process.getBuildProject(groupId, artifactId, version)) + .map(pdf -> { + return pdf.thenApply(buildProject -> { + IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); + URI basedirURI = buildProject.projectBasedir.toUri(); + Optional project = Arrays.stream(wsRoot.findContainersForLocationURI(basedirURI)) + .filter(IProject.class::isInstance).map(IProject.class::cast).findFirst(); + //if project is absent, the project build in Maven is not in the workspace + return project.isPresent() ? new ProjectReference(project.get(), buildProject) : null; + }); + }).orElseGet(() -> CompletableFuture.completedFuture(null)); } @Override diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchExtensionsSupport.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchExtensionsSupport.java index 892a25aa85..19484380e4 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchExtensionsSupport.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchExtensionsSupport.java @@ -98,7 +98,6 @@ public void appendVMArguments(VMArguments arguments, ILaunchConfiguration config for(IMavenLaunchParticipant participant : participants) { arguments.append(participant.getVMArguments(configuration, launch, monitor)); } - MavenBuildProjectDataConnection.openListenerConnection(launch, arguments); } } diff --git a/org.eclipse.m2e.maven.runtime/pom.xml b/org.eclipse.m2e.maven.runtime/pom.xml index b1146a7ed1..15d8e92376 100644 --- a/org.eclipse.m2e.maven.runtime/pom.xml +++ b/org.eclipse.m2e.maven.runtime/pom.xml @@ -20,7 +20,7 @@ org.eclipse.m2e.maven.runtime - 3.9.600-SNAPSHOT + 3.9.601-SNAPSHOT jar M2E Embedded Maven Runtime (includes Incubating components) diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java index 72d113fe06..aa0a8e704b 100644 --- a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java @@ -21,22 +21,14 @@ import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.StringJoiner; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import javax.inject.Named; import javax.inject.Singleton; +import org.apache.maven.AbstractMavenLifecycleParticipant; +import org.apache.maven.MavenExecutionException; import org.apache.maven.eventspy.EventSpy; -import org.apache.maven.execution.ExecutionEvent; -import org.apache.maven.execution.ExecutionEvent.Type; -import org.apache.maven.project.MavenProject; +import org.apache.maven.execution.MavenSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,9 +40,9 @@ * @author Hannes Wellmann * */ -@Named @Singleton -public class M2EMavenBuildDataBridge implements EventSpy { +@Named("m2e") +public class M2EMavenBuildDataBridge extends AbstractMavenLifecycleParticipant { private static final String SOCKET_FILE_PROPERTY_NAME = "m2e.build.project.data.socket.port"; private static final String DATA_SET_SEPARATOR = ";;"; @@ -60,7 +52,7 @@ public class M2EMavenBuildDataBridge implements EventSpy { private SocketChannel writeChannel; @Override - public void init(Context context) throws IOException { + public synchronized void afterSessionStart(MavenSession session) throws MavenExecutionException { String socketPort = System.getProperty(SOCKET_FILE_PROPERTY_NAME); if (socketPort != null) { try { @@ -76,79 +68,37 @@ public void init(Context context) throws IOException { } @Override - public void close() throws IOException { - writeChannel.close(); + public void afterSessionEnd(MavenSession session) throws MavenExecutionException { + close(); } - @Override - public void onEvent(Object event) throws Exception { - if (writeChannel != null && event instanceof ExecutionEvent - && ((ExecutionEvent) event).getType() == Type.ProjectStarted) { - - String message = serializeProjectData(((ExecutionEvent) event).getProject()); - - ByteBuffer buffer = ByteBuffer.wrap(message.getBytes()); - synchronized (writeChannel) { - while (buffer.hasRemaining()) { - writeChannel.write(buffer); - } + private synchronized void close() { + if (writeChannel != null) { + try { + writeChannel.close(); + } catch (IOException e) { + // nothing we want to do here... } } + writeChannel = null; } - private static String serializeProjectData(MavenProject project) { - StringJoiner data = new StringJoiner(","); - add(data, "groupId", project.getGroupId()); - add(data, "artifactId", project.getArtifactId()); - add(data, "version", project.getVersion()); - add(data, "file", project.getFile()); - add(data, "basedir", project.getBasedir()); - add(data, "build.directory", project.getBuild().getDirectory()); - return data.toString() + DATA_SET_SEPARATOR; - } - - private static void add(StringJoiner data, String key, Object value) { - data.add(key + "=" + value); + public synchronized boolean isActive() { + return writeChannel != null; } - /** - *

- * This method is supposed to be called from M2E within the Eclipse-IDE JVM. - *

- * - * @param dataSet the data-set to parse - * @return the {@link MavenProjectBuildData} parsed from the given string - */ - private static MavenProjectBuildData parseMavenBuildProject(String dataSet) { - Map data = new HashMap<>(8); - for (String entry : dataSet.split(",")) { - String[] keyValue = entry.split("="); - if (keyValue.length != 2) { - throw new IllegalStateException("Invalid data-set format" + dataSet); - } - data.put(keyValue[0], keyValue[1]); + public synchronized void sendMessage(String message) { + if (writeChannel == null) { + return; } - return new MavenProjectBuildData(data); - } - - public static final class MavenProjectBuildData { - public final String groupId; - public final String artifactId; - public final String version; - public final Path projectBasedir; - public final Path projectFile; - public final Path projectBuildDirectory; - - MavenProjectBuildData(Map data) { - if (data.size() != 6) { - throw new IllegalArgumentException(); + ByteBuffer buffer = ByteBuffer.wrap((message + DATA_SET_SEPARATOR).getBytes()); + while (buffer.hasRemaining()) { + try { + writeChannel.write(buffer); + } catch (IOException e) { + // channel seems dead... + close(); } - this.groupId = Objects.requireNonNull(data.get("groupId")); - this.artifactId = Objects.requireNonNull(data.get("artifactId")); - this.version = Objects.requireNonNull(data.get("version")); - this.projectBasedir = Paths.get(data.get("basedir")); - this.projectFile = Paths.get(data.get("file")); - this.projectBuildDirectory = Paths.get(data.get("build.directory")); } } @@ -156,15 +106,14 @@ public static final class MavenProjectBuildData { * Prepares the connection to a {@code Maven build JVM} to be launched and is * intended to be called from the Eclipse IDE JVM. * - * @param label the label of the listener thread - * @param datasetListener the listener, which is notified whenever a new - * {@link MavenProjectBuildData MavenProjectBuildDataSet} - * has arived from the Maven VM in the Eclipse-IDE VM. - * @return the preapre {@link MavenBuildConnection} - * @throws IOException + * @param label the label of the listener thread + * @param listener the listener, which is notified whenever a new + * {@link MavenProjectBuildData MavenProjectBuildDataSet} has + * arived from the Maven VM in the Eclipse-IDE VM. + * @return the maven arguments + * @throws IOException if init of connection failed */ - public static MavenBuildConnection prepareConnection(String label, Consumer datasetListener) - throws IOException { + public static String prepareConnection(String label, MavenBuildConnectionListener listener) throws IOException { // TODO: use UNIX domain socket once Java-17 is required by Maven // Path socketFile = Files.createTempFile("m2e.maven.build.listener", ".socket"); @@ -176,7 +125,7 @@ public static MavenBuildConnection prepareConnection(String label, Consumer { try (ServerSocketChannel s = server; SocketChannel readChannel = server.accept()) { @@ -187,9 +136,8 @@ public static MavenBuildConnection prepareConnection(String label, Consumer= 0;) { String dataSet = message.substring(0, terminatorIndex); message.delete(0, terminatorIndex + DATA_SET_SEPARATOR.length()); - - MavenProjectBuildData buildData = parseMavenBuildProject(dataSet); - datasetListener.accept(buildData); + MavenProjectBuildData buildData = MavenProjectBuildData.parseMavenBuildProject(dataSet); + listener.onData(buildData); } // Explicit cast for compatibility with covariant return type on JDK 9's // ByteBuffer @@ -197,7 +145,7 @@ public static MavenBuildConnection prepareConnection(String label, Consumer connection reader"); reader.setDaemon(true); reader.start(); - return connection; + listener.onOpen(label, connection); + String port = Integer.toString(((InetSocketAddress) server.getLocalAddress()).getPort()); + return "-D" + SOCKET_FILE_PROPERTY_NAME + "=" + port; } - - public static final class MavenBuildConnection { - private final ServerSocketChannel server; - private final AtomicBoolean readCompleted = new AtomicBoolean(false); - - MavenBuildConnection(ServerSocketChannel server) { - this.server = server; - } - - public String getMavenVMArguments() throws IOException { - String port = Integer.toString(((InetSocketAddress) server.getLocalAddress()).getPort()); - return "-D" + SOCKET_FILE_PROPERTY_NAME + "=" + port; - } - - public boolean isReadCompleted() { - return readCompleted.get(); - } - - public void close() throws IOException { - // Close the server to ensure the reader-thread does not wait forever for a - // connection from the Maven-process in case something went wrong during - // launching or while setting up the connection. - server.close(); - } - } - } diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2eEventSpy.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2eEventSpy.java new file mode 100644 index 0000000000..0f284903bf --- /dev/null +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2eEventSpy.java @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2022, 2024 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + * Christoph Läubrich - factor out into dedicated component + ********************************************************************************/ +package org.eclipse.m2e.internal.maven.listener; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.apache.maven.eventspy.EventSpy; +import org.apache.maven.execution.ExecutionEvent; +import org.apache.maven.execution.ExecutionEvent.Type; + +@Singleton +@Named("m2e") +public class M2eEventSpy implements EventSpy { + + private M2EMavenBuildDataBridge bridge; + + @Inject + public M2eEventSpy(M2EMavenBuildDataBridge bridge) { + this.bridge = bridge; + } + + @Override + public void init(Context context) throws Exception { + + } + + @Override + public void onEvent(Object event) throws Exception { + if (bridge.isActive()) { + if (event instanceof ExecutionEvent) { + ExecutionEvent executionEvent = (ExecutionEvent) event; + if (executionEvent.getType() == Type.ProjectStarted) { + String message = MavenProjectBuildData.serializeProjectData(((ExecutionEvent) event).getProject()); + bridge.sendMessage(message); + } + } + } + } + + @Override + public void close() throws Exception { + + } + +} diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnection.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnection.java new file mode 100644 index 0000000000..de5abb5916 --- /dev/null +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnection.java @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2022, 2024 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + * Christoph Läubrich - factor out into dedicated component + ********************************************************************************/ +package org.eclipse.m2e.internal.maven.listener; + +import java.io.IOException; +import java.nio.channels.ServerSocketChannel; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class MavenBuildConnection { + private final ServerSocketChannel server; + private final AtomicBoolean closed = new AtomicBoolean(false); + private MavenBuildConnectionListener listener; + + MavenBuildConnection(ServerSocketChannel server, MavenBuildConnectionListener listener) { + this.server = server; + this.listener = listener; + } + + public boolean isCompleted() { + return closed.get(); + } + + public void close() { + if (closed.compareAndSet(false, true)) { + listener.onClose(); + // Close the server to ensure the reader-thread does not wait forever for a + // connection from the Maven-process in case something went wrong during + // launching or while setting up the connection. + try { + server.close(); + } catch (IOException e) { + } + } + } +} \ No newline at end of file diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnectionListener.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnectionListener.java new file mode 100644 index 0000000000..5141f03020 --- /dev/null +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildConnectionListener.java @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (c) 2024 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + ********************************************************************************/ +package org.eclipse.m2e.internal.maven.listener; + +public interface MavenBuildConnectionListener { + + void onOpen(String label, MavenBuildConnection connection); + + void onClose(); + + void onData(MavenProjectBuildData buildData); + +} diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java new file mode 100644 index 0000000000..befa0e5241 --- /dev/null +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2022, 2024 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + * Christoph Läubrich - factor out into dedicated component + ********************************************************************************/ +package org.eclipse.m2e.internal.maven.listener; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.StringJoiner; + +import org.apache.maven.project.MavenProject; + +public final class MavenProjectBuildData { + public final String groupId; + public final String artifactId; + public final String version; + public final Path projectBasedir; + public final Path projectFile; + public final Path projectBuildDirectory; + + MavenProjectBuildData(Map data) { + if (data.size() != 6) { + throw new IllegalArgumentException(); + } + this.groupId = Objects.requireNonNull(data.get("groupId")); + this.artifactId = Objects.requireNonNull(data.get("artifactId")); + this.version = Objects.requireNonNull(data.get("version")); + this.projectBasedir = Paths.get(data.get("basedir")); + this.projectFile = Paths.get(data.get("file")); + this.projectBuildDirectory = Paths.get(data.get("build.directory")); + } + + /** + *

+ * This method is supposed to be called from M2E within the Eclipse-IDE JVM. + *

+ * + * @param dataSet the data-set to parse + * @return the {@link MavenProjectBuildData} parsed from the given string + */ + static MavenProjectBuildData parseMavenBuildProject(String dataSet) { + Map data = new HashMap<>(8); + for (String entry : dataSet.split(",")) { + String[] keyValue = entry.split("="); + if (keyValue.length != 2) { + throw new IllegalStateException("Invalid data-set format" + dataSet); + } + data.put(keyValue[0], keyValue[1]); + } + return new MavenProjectBuildData(data); + } + + static String serializeProjectData(MavenProject project) { + StringJoiner data = new StringJoiner(","); + add(data, "groupId", project.getGroupId()); + add(data, "artifactId", project.getArtifactId()); + add(data, "version", project.getVersion()); + add(data, "file", project.getFile()); + add(data, "basedir", project.getBasedir()); + add(data, "build.directory", project.getBuild().getDirectory()); + return data.toString(); + } + + private static void add(StringJoiner data, String key, Object value) { + data.add(key + "=" + value); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4950bcd1b8..c39883b38e 100644 --- a/pom.xml +++ b/pom.xml @@ -103,7 +103,7 @@ org.eclipse.m2e org.eclipse.m2e.maven.runtime - 3.9.600-SNAPSHOT + 3.9.601-SNAPSHOT