99
1010package org .elasticsearch .plugins ;
1111
12+ import org .apache .lucene .tests .util .LuceneTestCase ;
13+ import org .elasticsearch .Version ;
14+ import org .elasticsearch .common .settings .Settings ;
15+ import org .elasticsearch .env .Environment ;
16+ import org .elasticsearch .env .TestEnvironment ;
17+ import org .elasticsearch .logging .LogManager ;
18+ import org .elasticsearch .logging .Logger ;
19+ import org .elasticsearch .plugin .analysis .CharFilterFactory ;
1220import org .elasticsearch .test .ESTestCase ;
21+ import org .elasticsearch .test .PrivilegedOperations ;
22+ import org .elasticsearch .test .compiler .InMemoryJavaCompiler ;
23+ import org .elasticsearch .test .jar .JarUtils ;
1324
25+ import java .io .IOException ;
26+ import java .io .UncheckedIOException ;
27+ import java .net .URLClassLoader ;
28+ import java .nio .file .Files ;
29+ import java .nio .file .Path ;
30+ import java .util .Map ;
31+
32+ import static java .util .Map .entry ;
33+ import static org .elasticsearch .test .LambdaMatchers .transformedMatch ;
34+ import static org .hamcrest .Matchers .contains ;
1435import static org .hamcrest .Matchers .equalTo ;
36+ import static org .hamcrest .Matchers .hasSize ;
37+ import static org .hamcrest .Matchers .instanceOf ;
38+ import static org .hamcrest .Matchers .is ;
39+ import static org .hamcrest .Matchers .not ;
1540
41+ @ ESTestCase .WithoutSecurityManager
42+ @ LuceneTestCase .SuppressFileSystems (value = "ExtrasFS" )
1643public class PluginsLoaderTests extends ESTestCase {
1744
45+ private static final Logger logger = LogManager .getLogger (PluginsLoaderTests .class );
46+
47+ static PluginsLoader newPluginsLoader (Settings settings ) {
48+ return PluginsLoader .createPluginsLoader (null , TestEnvironment .newEnvironment (settings ).pluginsFile (), false );
49+ }
50+
1851 public void testToModuleName () {
1952 assertThat (PluginsLoader .toModuleName ("module.name" ), equalTo ("module.name" ));
2053 assertThat (PluginsLoader .toModuleName ("module-name" ), equalTo ("module.name" ));
@@ -28,4 +61,220 @@ public void testToModuleName() {
2861 assertThat (PluginsLoader .toModuleName ("_module_name" ), equalTo ("_module_name" ));
2962 assertThat (PluginsLoader .toModuleName ("_" ), equalTo ("_" ));
3063 }
64+
65+ public void testStablePluginLoading () throws Exception {
66+ final Path home = createTempDir ();
67+ final Settings settings = Settings .builder ().put (Environment .PATH_HOME_SETTING .getKey (), home ).build ();
68+ final Path plugins = home .resolve ("plugins" );
69+ final Path plugin = plugins .resolve ("stable-plugin" );
70+ Files .createDirectories (plugin );
71+ PluginTestUtil .writeStablePluginProperties (
72+ plugin ,
73+ "description" ,
74+ "description" ,
75+ "name" ,
76+ "stable-plugin" ,
77+ "version" ,
78+ "1.0.0" ,
79+ "elasticsearch.version" ,
80+ Version .CURRENT .toString (),
81+ "java.version" ,
82+ System .getProperty ("java.specification.version" )
83+ );
84+
85+ Path jar = plugin .resolve ("impl.jar" );
86+ JarUtils .createJarWithEntries (jar , Map .of ("p/A.class" , InMemoryJavaCompiler .compile ("p.A" , """
87+ package p;
88+ import java.util.Map;
89+ import org.elasticsearch.plugin.analysis.CharFilterFactory;
90+ import org.elasticsearch.plugin.NamedComponent;
91+ import java.io.Reader;
92+ @NamedComponent( "a_name")
93+ public class A implements CharFilterFactory {
94+ @Override
95+ public Reader create(Reader reader) {
96+ return reader;
97+ }
98+ }
99+ """ )));
100+ Path namedComponentFile = plugin .resolve ("named_components.json" );
101+ Files .writeString (namedComponentFile , """
102+ {
103+ "org.elasticsearch.plugin.analysis.CharFilterFactory": {
104+ "a_name": "p.A"
105+ }
106+ }
107+ """ );
108+
109+ var pluginsLoader = newPluginsLoader (settings );
110+ try {
111+ var loadedLayers = pluginsLoader .pluginLayers ().toList ();
112+
113+ assertThat (loadedLayers , hasSize (1 ));
114+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().getName (), equalTo ("stable-plugin" ));
115+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().isStable (), is (true ));
116+
117+ assertThat (pluginsLoader .pluginDescriptors (), hasSize (1 ));
118+ assertThat (pluginsLoader .pluginDescriptors ().get (0 ).getName (), equalTo ("stable-plugin" ));
119+ assertThat (pluginsLoader .pluginDescriptors ().get (0 ).isStable (), is (true ));
120+
121+ var pluginClassLoader = loadedLayers .get (0 ).pluginClassLoader ();
122+ var pluginModuleLayer = loadedLayers .get (0 ).pluginModuleLayer ();
123+ assertThat (pluginClassLoader , instanceOf (UberModuleClassLoader .class ));
124+ assertThat (pluginModuleLayer , is (not (ModuleLayer .boot ())));
125+ assertThat (pluginModuleLayer .modules (), contains (transformedMatch (Module ::getName , equalTo ("synthetic.stable.plugin" ))));
126+
127+ if (CharFilterFactory .class .getModule ().isNamed () == false ) {
128+ // test frameworks run with stable api classes on classpath, so we
129+ // have no choice but to let our class read the unnamed module that
130+ // owns the stable api classes
131+ ((UberModuleClassLoader ) pluginClassLoader ).addReadsSystemClassLoaderUnnamedModule ();
132+ }
133+
134+ Class <?> stableClass = pluginClassLoader .loadClass ("p.A" );
135+ assertThat (stableClass .getModule ().getName (), equalTo ("synthetic.stable.plugin" ));
136+ } finally {
137+ closePluginLoaders (pluginsLoader );
138+ }
139+ }
140+
141+ public void testModularPluginLoading () throws Exception {
142+ final Path home = createTempDir ();
143+ final Settings settings = Settings .builder ().put (Environment .PATH_HOME_SETTING .getKey (), home ).build ();
144+ final Path plugins = home .resolve ("plugins" );
145+ final Path plugin = plugins .resolve ("modular-plugin" );
146+ Files .createDirectories (plugin );
147+ PluginTestUtil .writePluginProperties (
148+ plugin ,
149+ "description" ,
150+ "description" ,
151+ "name" ,
152+ "modular-plugin" ,
153+ "classname" ,
154+ "p.A" ,
155+ "modulename" ,
156+ "modular.plugin" ,
157+ "version" ,
158+ "1.0.0" ,
159+ "elasticsearch.version" ,
160+ Version .CURRENT .toString (),
161+ "java.version" ,
162+ System .getProperty ("java.specification.version" )
163+ );
164+
165+ Path jar = plugin .resolve ("impl.jar" );
166+ Map <String , CharSequence > sources = Map .ofEntries (entry ("module-info" , "module modular.plugin { exports p; }" ), entry ("p.A" , """
167+ package p;
168+ import org.elasticsearch.plugins.Plugin;
169+
170+ public class A extends Plugin {
171+ }
172+ """ ));
173+
174+ // Usually org.elasticsearch.plugins.Plugin would be in the org.elasticsearch.server module.
175+ // Unfortunately, as tests run non-modular, it will be in the unnamed module, so we need to add a read for it.
176+ var classToBytes = InMemoryJavaCompiler .compile (sources , "--add-reads" , "modular.plugin=ALL-UNNAMED" );
177+
178+ JarUtils .createJarWithEntries (
179+ jar ,
180+ Map .ofEntries (entry ("module-info.class" , classToBytes .get ("module-info" )), entry ("p/A.class" , classToBytes .get ("p.A" )))
181+ );
182+
183+ var pluginsLoader = newPluginsLoader (settings );
184+ try {
185+ var loadedLayers = pluginsLoader .pluginLayers ().toList ();
186+
187+ assertThat (loadedLayers , hasSize (1 ));
188+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().getName (), equalTo ("modular-plugin" ));
189+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().isStable (), is (false ));
190+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().isModular (), is (true ));
191+
192+ assertThat (pluginsLoader .pluginDescriptors (), hasSize (1 ));
193+ assertThat (pluginsLoader .pluginDescriptors ().get (0 ).getName (), equalTo ("modular-plugin" ));
194+ assertThat (pluginsLoader .pluginDescriptors ().get (0 ).isModular (), is (true ));
195+
196+ var pluginModuleLayer = loadedLayers .get (0 ).pluginModuleLayer ();
197+ assertThat (pluginModuleLayer , is (not (ModuleLayer .boot ())));
198+ assertThat (pluginModuleLayer .modules (), contains (transformedMatch (Module ::getName , equalTo ("modular.plugin" ))));
199+ } finally {
200+ closePluginLoaders (pluginsLoader );
201+ }
202+ }
203+
204+ public void testNonModularPluginLoading () throws Exception {
205+ final Path home = createTempDir ();
206+ final Settings settings = Settings .builder ().put (Environment .PATH_HOME_SETTING .getKey (), home ).build ();
207+ final Path plugins = home .resolve ("plugins" );
208+ final Path plugin = plugins .resolve ("non-modular-plugin" );
209+ Files .createDirectories (plugin );
210+ PluginTestUtil .writePluginProperties (
211+ plugin ,
212+ "description" ,
213+ "description" ,
214+ "name" ,
215+ "non-modular-plugin" ,
216+ "classname" ,
217+ "p.A" ,
218+ "version" ,
219+ "1.0.0" ,
220+ "elasticsearch.version" ,
221+ Version .CURRENT .toString (),
222+ "java.version" ,
223+ System .getProperty ("java.specification.version" )
224+ );
225+
226+ Path jar = plugin .resolve ("impl.jar" );
227+ Map <String , CharSequence > sources = Map .ofEntries (entry ("p.A" , """
228+ package p;
229+ import org.elasticsearch.plugins.Plugin;
230+
231+ public class A extends Plugin {
232+ }
233+ """ ));
234+
235+ var classToBytes = InMemoryJavaCompiler .compile (sources );
236+
237+ JarUtils .createJarWithEntries (jar , Map .ofEntries (entry ("p/A.class" , classToBytes .get ("p.A" ))));
238+
239+ var pluginsLoader = newPluginsLoader (settings );
240+ try {
241+ var loadedLayers = pluginsLoader .pluginLayers ().toList ();
242+
243+ assertThat (loadedLayers , hasSize (1 ));
244+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().getName (), equalTo ("non-modular-plugin" ));
245+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().isStable (), is (false ));
246+ assertThat (loadedLayers .get (0 ).pluginBundle ().pluginDescriptor ().isModular (), is (false ));
247+
248+ assertThat (pluginsLoader .pluginDescriptors (), hasSize (1 ));
249+ assertThat (pluginsLoader .pluginDescriptors ().get (0 ).getName (), equalTo ("non-modular-plugin" ));
250+ assertThat (pluginsLoader .pluginDescriptors ().get (0 ).isModular (), is (false ));
251+
252+ var pluginModuleLayer = loadedLayers .get (0 ).pluginModuleLayer ();
253+ assertThat (pluginModuleLayer , is (ModuleLayer .boot ()));
254+ } finally {
255+ closePluginLoaders (pluginsLoader );
256+ }
257+ }
258+
259+ // Closes the URLClassLoaders and UberModuleClassloaders created by the given plugin loader.
260+ // We can use the direct ClassLoader from the plugin because tests do not use any parent SPI ClassLoaders.
261+ static void closePluginLoaders (PluginsLoader pluginsLoader ) {
262+ pluginsLoader .pluginLayers ().forEach (lp -> {
263+ if (lp .pluginClassLoader () instanceof URLClassLoader urlClassLoader ) {
264+ try {
265+ PrivilegedOperations .closeURLClassLoader (urlClassLoader );
266+ } catch (IOException unexpected ) {
267+ throw new UncheckedIOException (unexpected );
268+ }
269+ } else if (lp .pluginClassLoader () instanceof UberModuleClassLoader loader ) {
270+ try {
271+ PrivilegedOperations .closeURLClassLoader (loader .getInternalLoader ());
272+ } catch (Exception e ) {
273+ throw new RuntimeException (e );
274+ }
275+ } else {
276+ logger .info ("Cannot close unexpected classloader " + lp .pluginClassLoader ());
277+ }
278+ });
279+ }
31280}
0 commit comments