-
Notifications
You must be signed in to change notification settings - Fork 25.6k
[WIP] Non-modular plugins with synthetic module #117570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ed6d1d8
e5e5ce5
4c3f232
bdcdb30
d188bec
374731a
2eeb099
a9a768c
241a5b5
c3c0127
2ad48c9
daf710c
7933ada
8707701
56c050e
298405e
a5594ea
a4d39de
0f56ab9
990b04d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ | |
| package org.elasticsearch.plugins; | ||
|
|
||
| import org.elasticsearch.core.SuppressForbidden; | ||
| import org.elasticsearch.logging.LogManager; | ||
| import org.elasticsearch.logging.Logger; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
|
|
@@ -42,6 +44,10 @@ | |
| */ | ||
| public class ModuleSupport { | ||
|
|
||
| private static final Logger logger = LogManager.getLogger(ModuleSupport.class); | ||
|
|
||
| private static final Set<String> FORBIDDEN_PACKAGE_NAMES = Set.of("org.apache.commons.lang.enum"); | ||
|
|
||
| private ModuleSupport() { | ||
| throw new AssertionError("Utility class, should not be instantiated"); | ||
| } | ||
|
|
@@ -82,31 +88,44 @@ static ModuleDescriptor createModuleDescriptor( | |
| Map<String, List<String>> allBundledProviders = new HashMap<>(); | ||
| Set<String> servicesUsedInBundle = new HashSet<>(); | ||
| for (Path path : jarPaths) { | ||
| logger.info("Scanning JAR " + path + " for services"); | ||
| assert path.getFileName().toString().endsWith(".jar") : "expected jars suffix, in path: " + path; | ||
| try (JarFile jf = new JarFile(path.toFile(), true, ZipFile.OPEN_READ, Runtime.version())) { | ||
| // if we have a module declaration, trust its uses/provides | ||
| JarEntry moduleInfo = jf.getJarEntry("module-info.class"); | ||
| if (moduleInfo != null) { | ||
| var descriptor = getDescriptorForModularJar(path); | ||
| pkgs.addAll(descriptor.packages()); | ||
| pkgs.addAll(descriptor.packages().stream().filter(Predicate.not(FORBIDDEN_PACKAGE_NAMES::contains)).toList()); | ||
| servicesUsedInBundle.addAll(descriptor.uses()); | ||
| logger.info("Adding modular uses " + String.join(";", descriptor.uses())); | ||
|
|
||
| for (ModuleDescriptor.Provides p : descriptor.provides()) { | ||
| String serviceName = p.service(); | ||
| List<String> providersInModule = p.providers(); | ||
| logger.info("Adding modular providers " + String.join(";", providersInModule)); | ||
|
|
||
| allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInModule)); | ||
| servicesUsedInBundle.add(serviceName); | ||
| } | ||
| } else { | ||
| var scan = scan(jf); | ||
| scan.classFiles().stream().map(cf -> toPackageName(cf, "/")).flatMap(Optional::stream).forEach(pkgs::add); | ||
| scan.classFiles() | ||
| .stream() | ||
| .map(cf -> toPackageName(cf, "/")) | ||
| .flatMap(Optional::stream) | ||
| .filter(Predicate.not(FORBIDDEN_PACKAGE_NAMES::contains)) | ||
| .forEach(pkgs::add); | ||
|
|
||
| // read providers from the list of service files | ||
| for (String serviceFileName : scan.serviceFiles()) { | ||
| String serviceName = getServiceName(serviceFileName); | ||
| List<String> providersInJar = getProvidersFromServiceFile(jf, serviceFileName); | ||
|
|
||
| allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInJar)); | ||
| if (providersInJar.isEmpty() == false) { | ||
| logger.info("Adding non-modular providers " + String.join(";", providersInJar)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit 2: it's best practice to use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. keep in mind this is WIP, very "debugging" PR - these |
||
| allBundledProviders.compute(serviceName, (k, v) -> createListOrAppend(v, providersInJar)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know you didn't write it, but this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, noticed the same, and there are also some addAll mixed with foreach add; if this ever turns into a real PR I'll fix them. |
||
| } | ||
| logger.info("Adding non-modular uses " + serviceName); | ||
| servicesUsedInBundle.add(serviceName); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,7 @@ | |
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory; | ||
|
|
@@ -100,8 +101,8 @@ public record LayerAndLoader(ModuleLayer layer, ClassLoader loader) { | |
| Objects.requireNonNull(loader); | ||
| } | ||
|
|
||
| public static LayerAndLoader ofLoader(ClassLoader loader) { | ||
| return new LayerAndLoader(ModuleLayer.boot(), loader); | ||
| public static LayerAndLoader ofLoader(UberModuleClassLoader loader) { | ||
| return new LayerAndLoader(loader.getLayer(), loader); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -273,7 +274,7 @@ static LayerAndLoader createSPI( | |
| ); | ||
| } else { | ||
| logger.debug(() -> "Loading bundle: " + plugin.getName() + ", creating spi, non-modular"); | ||
| return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.spiUrls.toArray(new URL[0]), parentLoader)); | ||
| return new LayerAndLoader(ModuleLayer.boot(), URLClassLoader.newInstance(bundle.spiUrls.toArray(new URL[0]), parentLoader)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -286,26 +287,39 @@ static LayerAndLoader createPlugin( | |
| ) { | ||
| final PluginDescriptor plugin = bundle.plugin; | ||
| if (plugin.getModuleName().isPresent()) { | ||
| logger.debug(() -> "Loading bundle: " + plugin.getName() + ", modular"); | ||
| logger.info(() -> "Loading bundle: " + plugin.getName() + ", modular"); | ||
| var parentLayers = Stream.concat( | ||
| Stream.ofNullable(spiLayerAndLoader != null ? spiLayerAndLoader.layer() : null), | ||
| extendedPlugins.stream().map(LoadedPluginLayer::spiModuleLayer) | ||
| ).toList(); | ||
| return createPluginModuleLayer(bundle, pluginParentLoader, parentLayers, qualifiedExports); | ||
| } else if (plugin.isStable()) { | ||
| logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular as synthetic module"); | ||
| logger.info(() -> "Loading bundle: " + plugin.getName() + ", non-modular as synthetic module"); | ||
| return LayerAndLoader.ofLoader( | ||
| UberModuleClassLoader.getInstance( | ||
| pluginParentLoader, | ||
| ModuleLayer.boot(), | ||
| List.of(ModuleLayer.boot()), | ||
| "synthetic." + toModuleName(plugin.getName()), | ||
| bundle.allUrls, | ||
| Set.of("org.elasticsearch.server") // TODO: instead of denying server, allow only jvm + stable API modules | ||
| ) | ||
| ); | ||
| } else { | ||
| logger.debug(() -> "Loading bundle: " + plugin.getName() + ", non-modular"); | ||
| return LayerAndLoader.ofLoader(URLClassLoader.newInstance(bundle.urls.toArray(URL[]::new), pluginParentLoader)); | ||
| var syntheticName = "synthetic." + toModuleName2(plugin.getName()); | ||
| logger.info(() -> "Loading bundle: " + plugin.getName() + ", non-modular, as synthetic module " + syntheticName); | ||
| var parentLayers = Stream.concat( | ||
| Stream.ofNullable(spiLayerAndLoader != null ? spiLayerAndLoader.layer() : null), | ||
| Stream.concat(extendedPlugins.stream().map(LoadedPluginLayer::spiModuleLayer), Stream.of(ModuleLayer.boot())) | ||
| ).toList(); | ||
| var parentModuleNames = parentLayers.stream() | ||
| .flatMap(l -> l.modules().stream()) | ||
| .map(Module::getName) | ||
| .distinct() | ||
| .collect(Collectors.joining(";")); | ||
| logger.info("Parent modules for {}: {}", syntheticName, parentModuleNames); | ||
| return LayerAndLoader.ofLoader( | ||
| UberModuleClassLoader.getInstance(pluginParentLoader, parentLayers, syntheticName, bundle.allUrls, Set.of()) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -343,7 +357,7 @@ static LayerAndLoader createPluginModuleLayer( | |
| ); | ||
| } | ||
|
|
||
| static LayerAndLoader createModuleLayer( | ||
| private static LayerAndLoader createModuleLayer( | ||
| String className, | ||
| String moduleName, | ||
| Path[] paths, | ||
|
|
@@ -389,6 +403,16 @@ static String toModuleName(String name) { | |
| return result; | ||
| } | ||
|
|
||
| static String toModuleName2(String name) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very hacky, but I did not want to break the existing name schema for stable plugins. |
||
| String result = name.replace('-', '_') | ||
| .replaceAll("\\W+", ".") // replace non-alphanumeric character strings with dots | ||
| .replaceAll("(^[^A-Za-z_]*)", "") // trim non-alpha or underscore characters from start | ||
| .replaceAll("\\.$", "") // trim trailing dot | ||
| .toLowerCase(Locale.getDefault()); | ||
| assert ModuleSupport.isPackageName(result); | ||
| return result; | ||
| } | ||
|
|
||
| static final String toPackageName(String className) { | ||
| assert className.endsWith(".") == false; | ||
| int index = className.lastIndexOf('.'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,7 +30,8 @@ public class RestJvmCrashAction implements RestHandler { | |
| static { | ||
| try { | ||
| AccessController.doPrivileged((PrivilegedExceptionAction<?>) () -> { | ||
| Class<?> unsafe = Class.forName("sun.misc.Unsafe"); | ||
| // Bypass UberModuleClassLoader, as server does not have our very dangerous permissions | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shows an interesting side effect of using UberModuleClassLoader: for class loading, the server module/ProtectionDomain gets in the way. So if it does not have permissions (like |
||
| Class<?> unsafe = ClassLoader.getSystemClassLoader().loadClass("sun.misc.Unsafe"); | ||
|
|
||
| FREE_MEMORY = unsafe.getMethod("freeMemory", long.class); | ||
| Field f = unsafe.getDeclaredField("theUnsafe"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: potentially long strings are often best placed at the end of a log message to make the message easier to read: