Make Classworld setup more alike to vanilla Maven (#784)

Use the plexus Launcher to start the daemon server, just like we would
launch a normal Maven process.

This improves compatibility with any extensions or plugins that assume that
their ClassLoader is a ClassRealm.
This commit is contained in:
Stefan Oehme
2023-02-14 17:06:34 +01:00
committed by GitHub
parent ddc8ef9a10
commit 1bcfc29d45
7 changed files with 60 additions and 491 deletions

View File

@@ -18,7 +18,6 @@
*/
package org.mvndaemon.mvnd.client;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@@ -52,7 +51,6 @@ import org.mvndaemon.mvnd.common.DaemonRegistry;
import org.mvndaemon.mvnd.common.DaemonState;
import org.mvndaemon.mvnd.common.DaemonStopEvent;
import org.mvndaemon.mvnd.common.Environment;
import org.mvndaemon.mvnd.common.MavenDaemon;
import org.mvndaemon.mvnd.common.Message;
import org.mvndaemon.mvnd.common.Os;
import org.mvndaemon.mvnd.common.SocketFamily;
@@ -339,33 +337,43 @@ public class DaemonConnector {
final Path mvndHome = parameters.mvndHome();
final Path workingDir = parameters.userDir();
String command = "";
try (DirectoryStream<Path> jarPaths =
Files.newDirectoryStream(mvndHome.resolve("lib").resolve("ext"))) {
try {
List<String> args = new ArrayList<>();
// executable
final String java = Os.current().isUnixLike() ? "bin/java" : "bin\\java.exe";
args.add(parameters.javaHome().resolve(java).toString());
// classpath
String mvndCommonPath = null;
String mvndAgentPath = null;
for (Path jar : jarPaths) {
String s = jar.getFileName().toString();
if (s.endsWith(".jar")) {
if (s.startsWith("mvnd-common-")) {
mvndCommonPath = jar.toString();
} else if (s.startsWith("mvnd-agent-")) {
mvndAgentPath = jar.toString();
String plexusClassworldsPath = null;
try (DirectoryStream<Path> jarPaths =
Files.newDirectoryStream(mvndHome.resolve("lib").resolve("ext"))) {
for (Path jar : jarPaths) {
String s = jar.getFileName().toString();
if (s.endsWith(".jar")) {
if (s.startsWith("mvnd-agent-")) {
mvndAgentPath = jar.toString();
}
}
}
}
if (mvndCommonPath == null) {
throw new IllegalStateException("Could not find mvnd-common jar in lib/");
try (DirectoryStream<Path> jarPaths = Files.newDirectoryStream(mvndHome.resolve("boot"))) {
for (Path jar : jarPaths) {
String s = jar.getFileName().toString();
if (s.endsWith(".jar")) {
if (s.startsWith("plexus-classworlds-")) {
plexusClassworldsPath = jar.toString();
}
}
}
}
if (mvndAgentPath == null) {
throw new IllegalStateException("Could not find mvnd-agent jar in lib/");
throw new IllegalStateException("Could not find mvnd-agent jar in lib/ext/");
}
if (plexusClassworldsPath == null) {
throw new IllegalStateException("Could not find plexus-classworlds jar in boot/");
}
args.add("-classpath");
args.add(mvndCommonPath + File.pathSeparator + mvndAgentPath);
args.add(plexusClassworldsPath);
args.add("-javaagent:" + mvndAgentPath);
// debug options
if (parameters.property(Environment.MVND_DEBUG).asBoolean()) {
@@ -422,6 +430,7 @@ public class DaemonConnector {
Environment.MVND_HOME.addSystemProperty(args, mvndHome.toString());
args.add("-Dmaven.home=" + mvndHome);
args.add("-Dmaven.conf=" + mvndHome.resolve("conf"));
args.add("-Dclassworlds.conf=" + mvndHome.resolve("bin").resolve("mvnd-server.conf"));
Environment.MVND_JAVA_HOME.addSystemProperty(
args, parameters.javaHome().toString());
@@ -439,7 +448,7 @@ public class DaemonConnector {
.orElseGet(() -> getJavaVersion() >= 16.0f ? SocketFamily.unix : SocketFamily.inet)
.toString());
parameters.discriminatingSystemProperties(args);
args.add(MavenDaemon.class.getName());
args.add("org.codehaus.plexus.classworlds.launcher.Launcher");
command = String.join(" ", args);
LOGGER.debug(

View File

@@ -1,85 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.common;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class MavenDaemon {
public static void main(String[] args) throws Exception {
final Path mvndHome = Environment.MVND_HOME.asPath();
URL[] classpath = Stream.concat(
/* jars */
Stream.of("lib/ext", "lib", "boot")
.map(mvndHome::resolve)
.flatMap((Path p) -> {
try {
return Files.list(p);
} catch (java.io.IOException e) {
throw new RuntimeException("Could not list " + p, e);
}
})
.filter(p -> {
final String fileName = p.getFileName().toString();
return fileName.endsWith(".jar") && !fileName.startsWith("mvnd-client-");
})
.filter(Files::isRegularFile),
/* resources */
Stream.of(mvndHome.resolve("conf"), mvndHome.resolve("conf/logging")))
.map(Path::normalize)
.map(Path::toUri)
.map(uri -> {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
ClassLoader loader = new URLClassLoader(classpath, null) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
return MavenDaemon.class.getClassLoader().loadClass(name);
}
}
@Override
public URL getResource(String name) {
URL url = super.getResource(name);
if (url == null) {
url = MavenDaemon.class.getClassLoader().getResource(name);
}
return url;
}
};
Thread.currentThread().setContextClassLoader(loader);
Class<?> clazz = loader.loadClass("org.mvndaemon.mvnd.daemon.Server");
try (AutoCloseable server = (AutoCloseable) clazz.getConstructor().newInstance()) {
((Runnable) server).run();
}
}
}

View File

@@ -1,385 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.classrealm;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.extension.internal.CoreExportsProvider;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.codehaus.plexus.MutablePlexusContainer;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.sisu.Priority;
/**
* This class is a copy of DefaultClassRealmManager with one modification:
* the {@link #PARENT_CLASSLOADER} is set to null instead of classworld's classloader.
* The reason is that mvnd is booted by {@link org.mvndaemon.mvnd.common.MavenDaemon}
* instead of {@link org.codehaus.plexus.classworlds.launcher.Launcher} and classworld
* is contained by the maven classloader. This can cause problems with extensions
* as their parent contains the whole maven classloader.
* See <a href="https://github.com/apache/maven-mvnd/issues/690">mvnd#690</a>
*/
@Named
@Singleton
@Priority(10)
public class MvndClassRealmManager implements ClassRealmManager {
public static final String API_REALMID = "maven.api";
/**
* During normal command line build, ClassWorld is loaded by jvm system classloader, which only includes
* plexus-classworlds jar and possibly javaagent classes, see https://issues.apache.org/jira/browse/MNG-4747.
* <p>
* Using ClassWorld to determine plugin/extensions realm parent classloaders gives m2e and integration test harness
* flexibility to load multiple version of maven into dedicated classloaders without assuming state of jvm system
* classloader.
*/
private static final ClassLoader PARENT_CLASSLOADER = ClassLoader.getSystemClassLoader();
private final Logger logger;
private final ClassWorld world;
private final ClassRealm containerRealm;
// this is a live injected collection
private final List<ClassRealmManagerDelegate> delegates;
private final ClassRealm mavenApiRealm;
/**
* Patterns of artifacts provided by maven core and exported via maven api realm. These artifacts are filtered from
* plugin and build extensions realms to avoid presence of duplicate and possibly conflicting classes on classpath.
*/
private final Set<String> providedArtifacts;
@Inject
public MvndClassRealmManager(
Logger logger,
PlexusContainer container,
List<ClassRealmManagerDelegate> delegates,
CoreExportsProvider exports) {
this.logger = logger;
this.world = ((MutablePlexusContainer) container).getClassWorld();
this.containerRealm = container.getContainerRealm();
this.delegates = delegates;
Map<String, ClassLoader> foreignImports = exports.get().getExportedPackages();
this.mavenApiRealm = createRealm(
API_REALMID,
ClassRealmRequest.RealmType.Core,
null /* parent */,
null /* parentImports */,
foreignImports,
null /* artifacts */);
this.providedArtifacts = exports.get().getExportedArtifacts();
}
private ClassRealm newRealm(String id) {
synchronized (world) {
String realmId = id;
Random random = new Random();
while (true) {
try {
ClassRealm classRealm = world.newRealm(realmId, null);
if (logger.isDebugEnabled()) {
logger.debug("Created new class realm " + realmId);
}
return classRealm;
} catch (DuplicateRealmException e) {
realmId = id + '-' + random.nextInt();
}
}
}
}
public ClassRealm getMavenApiRealm() {
return mavenApiRealm;
}
/**
* Creates a new class realm with the specified parent and imports.
*
* @param baseRealmId The base id to use for the new realm, must not be {@code null}.
* @param type The type of the class realm, must not be {@code null}.
* @param parent The parent realm for the new realm, may be {@code null}.
* @param parentImports The packages/types to import from the parent realm, may be {@code null}.
* @param foreignImports The packages/types to import from foreign realms, may be {@code null}.
* @param artifacts The artifacts to add to the realm, may be {@code null}. Unresolved artifacts (i.e. with a
* missing file) will automatically be excluded from the realm.
* @return The created class realm, never {@code null}.
*/
private ClassRealm createRealm(
String baseRealmId,
ClassRealmRequest.RealmType type,
ClassLoader parent,
List<String> parentImports,
Map<String, ClassLoader> foreignImports,
List<Artifact> artifacts) {
Set<String> artifactIds = new LinkedHashSet<>();
List<ClassRealmConstituent> constituents = new ArrayList<>();
if (artifacts != null) {
for (Artifact artifact : artifacts) {
if (!isProvidedArtifact(artifact)) {
artifactIds.add(getId(artifact));
if (artifact.getFile() != null) {
constituents.add(new ArtifactClassRealmConstituent(artifact));
}
}
}
}
if (parentImports != null) {
parentImports = new ArrayList<>(parentImports);
} else {
parentImports = new ArrayList<>();
}
if (foreignImports != null) {
foreignImports = new TreeMap<>(foreignImports);
} else {
foreignImports = new TreeMap<>();
}
ClassRealm classRealm = newRealm(baseRealmId);
if (parent != null) {
classRealm.setParentClassLoader(parent);
}
callDelegates(classRealm, type, parent, parentImports, foreignImports, constituents);
wireRealm(classRealm, parentImports, foreignImports);
Set<String> includedIds = populateRealm(classRealm, constituents);
if (logger.isDebugEnabled()) {
artifactIds.removeAll(includedIds);
for (String id : artifactIds) {
logger.debug(" Excluded: " + id);
}
}
return classRealm;
}
public ClassRealm getCoreRealm() {
return containerRealm;
}
public ClassRealm createProjectRealm(Model model, List<Artifact> artifacts) {
Objects.requireNonNull(model, "model cannot be null");
ClassLoader parent = getMavenApiRealm();
return createRealm(getKey(model), ClassRealmRequest.RealmType.Project, parent, null, null, artifacts);
}
private static String getKey(Model model) {
return "project>" + model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion();
}
public ClassRealm createExtensionRealm(Plugin plugin, List<Artifact> artifacts) {
Objects.requireNonNull(plugin, "plugin cannot be null");
ClassLoader parent = PARENT_CLASSLOADER;
Map<String, ClassLoader> foreignImports = Collections.<String, ClassLoader>singletonMap("", getMavenApiRealm());
return createRealm(
getKey(plugin, true), ClassRealmRequest.RealmType.Extension, parent, null, foreignImports, artifacts);
}
private boolean isProvidedArtifact(Artifact artifact) {
return providedArtifacts.contains(artifact.getGroupId() + ":" + artifact.getArtifactId());
}
public ClassRealm createPluginRealm(
Plugin plugin,
ClassLoader parent,
List<String> parentImports,
Map<String, ClassLoader> foreignImports,
List<Artifact> artifacts) {
Objects.requireNonNull(plugin, "plugin cannot be null");
if (parent == null) {
parent = PARENT_CLASSLOADER;
}
return createRealm(
getKey(plugin, false),
ClassRealmRequest.RealmType.Plugin,
parent,
parentImports,
foreignImports,
artifacts);
}
private static String getKey(Plugin plugin, boolean extension) {
String version = ArtifactUtils.toSnapshotVersion(plugin.getVersion());
return (extension ? "extension>" : "plugin>") + plugin.getGroupId() + ":" + plugin.getArtifactId() + ":"
+ version;
}
private static String getId(Artifact artifact) {
return getId(
artifact.getGroupId(),
artifact.getArtifactId(),
artifact.getExtension(),
artifact.getClassifier(),
artifact.getBaseVersion());
}
private static String getId(ClassRealmConstituent constituent) {
return getId(
constituent.getGroupId(),
constituent.getArtifactId(),
constituent.getType(),
constituent.getClassifier(),
constituent.getVersion());
}
private static String getId(String gid, String aid, String type, String cls, String ver) {
return gid + ':' + aid + ':' + type + (StringUtils.isNotEmpty(cls) ? ':' + cls : "") + ':' + ver;
}
private void callDelegates(
ClassRealm classRealm,
ClassRealmRequest.RealmType type,
ClassLoader parent,
List<String> parentImports,
Map<String, ClassLoader> foreignImports,
List<ClassRealmConstituent> constituents) {
List<ClassRealmManagerDelegate> delegates = new ArrayList<>(this.delegates);
if (!delegates.isEmpty()) {
ClassRealmRequest request =
new DefaultClassRealmRequest(type, parent, parentImports, foreignImports, constituents);
for (ClassRealmManagerDelegate delegate : delegates) {
try {
delegate.setupRealm(classRealm, request);
} catch (Exception e) {
logger.error(
delegate.getClass().getName() + " failed to setup class realm " + classRealm + ": "
+ e.getMessage(),
e);
}
}
}
}
private Set<String> populateRealm(ClassRealm classRealm, List<ClassRealmConstituent> constituents) {
Set<String> includedIds = new LinkedHashSet<>();
if (logger.isDebugEnabled()) {
logger.debug("Populating class realm " + classRealm.getId());
}
for (ClassRealmConstituent constituent : constituents) {
File file = constituent.getFile();
String id = getId(constituent);
includedIds.add(id);
if (logger.isDebugEnabled()) {
logger.debug(" Included: " + id);
}
try {
classRealm.addURL(file.toURI().toURL());
} catch (MalformedURLException e) {
// Not going to happen
logger.error(e.getMessage(), e);
}
}
return includedIds;
}
private void wireRealm(ClassRealm classRealm, List<String> parentImports, Map<String, ClassLoader> foreignImports) {
if (foreignImports != null && !foreignImports.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug("Importing foreign packages into class realm " + classRealm.getId());
}
for (Map.Entry<String, ClassLoader> entry : foreignImports.entrySet()) {
ClassLoader importedRealm = entry.getValue();
String imp = entry.getKey();
if (logger.isDebugEnabled()) {
logger.debug(" Imported: " + imp + " < " + getId(importedRealm));
}
classRealm.importFrom(importedRealm, imp);
}
}
if (parentImports != null && !parentImports.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug("Importing parent packages into class realm " + classRealm.getId());
}
for (String imp : parentImports) {
if (logger.isDebugEnabled()) {
logger.debug(" Imported: " + imp + " < " + getId(classRealm.getParentClassLoader()));
}
classRealm.importFromParent(imp);
}
}
}
private String getId(ClassLoader classLoader) {
if (classLoader instanceof ClassRealm) {
return ((ClassRealm) classLoader).getId();
}
return String.valueOf(classLoader);
}
}

View File

@@ -179,8 +179,7 @@ public class DaemonMavenCli {
slf4jLogger = slf4jLoggerFactory.getLogger(this.getClass().getName());
plexusLoggerManager = new Slf4jLoggerManager();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
classWorld = new ClassWorld("plexus.core", cl);
this.classWorld = ((ClassRealm) Thread.currentThread().getContextClassLoader()).getWorld();
container = container();
@@ -473,6 +472,7 @@ public class DaemonMavenCli {
List<File> extClassPath = Stream.of(
Environment.MVND_EXT_CLASSPATH.asString().split(","))
.filter(s -> s != null && !s.isEmpty())
.map(File::new)
.collect(Collectors.toList());

View File

@@ -99,7 +99,13 @@ public class Server implements AutoCloseable, Runnable {
private final DaemonMemoryStatus memoryStatus;
private final long keepAliveMs;
public Server() throws IOException {
public static void main(String[] args) {
try (Server server = new Server()) {
server.run();
}
}
public Server() {
// When spawning a new process, the child process is create within
// the same process group. This means that a few signals are sent
// to the whole group. This is the case for SIGINT (Ctrl-C) and

View File

@@ -0,0 +1,25 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
main is org.mvndaemon.mvnd.daemon.Server from plexus.core
set maven.home default ${mvnd.home}
set maven.conf default ${maven.home}/conf
[plexus.core]
load ${maven.conf}/logging
optionally ${maven.home}/lib/ext/*.jar
load ${maven.home}/lib/*.jar

View File

@@ -320,7 +320,6 @@
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>