1212import java .lang .module .Configuration ;
1313import java .lang .module .ModuleDescriptor ;
1414import java .lang .module .ModuleFinder ;
15+ import java .lang .reflect .Constructor ;
1516import java .net .MalformedURLException ;
1617import java .net .URL ;
1718import java .net .URLClassLoader ;
1819import java .nio .file .Files ;
1920import java .nio .file .Path ;
2021import java .nio .file .Paths ;
22+ import java .nio .charset .StandardCharsets ;
2123import java .time .Instant ;
2224import java .util .*;
2325import java .util .concurrent .ConcurrentHashMap ;
@@ -78,6 +80,7 @@ public PluginLoaderService(
7880 */
7981 public LoadedPlugin loadPlugin (Plugin dbPlugin ) throws PluginLoadException {
8082 String pluginId = dbPlugin .getId ();
83+ URLClassLoader classLoader = null ;
8184
8285 // Prevent double-load
8386 if (loadedPlugins .containsKey (pluginId )) {
@@ -132,65 +135,21 @@ public LoadedPlugin loadPlugin(Plugin dbPlugin) throws PluginLoadException {
132135 );
133136 }
134137
135- // Layer 1: Create URLClassLoader (parent = platform class loader)
136- URLClassLoader classLoader = new URLClassLoader (
138+ // Use the application plugin API loader as parent so plugin implementations
139+ // share the same SynapsePlugin/Channel/ModelProvider types.
140+ ClassLoader parentClassLoader = SynapsePlugin .class .getClassLoader ();
141+ classLoader = new URLClassLoader (
137142 new URL [] { jarUrl },
138- ClassLoader . getPlatformClassLoader ()
143+ parentClassLoader
139144 );
140145
141- // Layer 2: Create JPMS ModuleLayer
142- ModuleFinder pluginFinder = ModuleFinder .of (realJarPath );
143- Set <ModuleDescriptor > descriptors = pluginFinder
144- .findAll ()
145- .stream ()
146- .map (java .lang .module .ModuleReference ::descriptor )
147- .collect (Collectors .toSet ());
148-
149- if (descriptors .isEmpty ()) {
150- classLoader .close ();
151- throw new PluginLoadException (
152- pluginId ,
153- "JAR contains no module descriptor (module-info.class). " +
154- "Plugins must declare a JPMS module."
155- );
156- }
157-
158- Set <String > moduleNames = descriptors
159- .stream ()
160- .map (ModuleDescriptor ::name )
161- .collect (Collectors .toSet ());
162-
163- Configuration parentConfig = ModuleLayer .boot ().configuration ();
164- Configuration pluginConfig = parentConfig .resolve (
165- pluginFinder ,
166- ModuleFinder .of (),
167- moduleNames
168- );
169-
170- ModuleLayer pluginLayer = ModuleLayer .defineModulesWithOneLoader (
171- pluginConfig ,
172- List .of (ModuleLayer .boot ()),
173- classLoader
174- ).layer ();
175-
176- // Layer 3: Discover plugin implementation via ServiceLoader
177- ServiceLoader <SynapsePlugin > loader = ServiceLoader .load (
146+ ModuleLayer pluginLayer = tryCreatePluginLayer (realJarPath , classLoader );
147+ SynapsePlugin instance = discoverPluginInstance (
148+ dbPlugin ,
178149 pluginLayer ,
179- SynapsePlugin . class
150+ classLoader
180151 );
181152
182- Optional <SynapsePlugin > instanceOpt = loader .findFirst ();
183- if (instanceOpt .isEmpty ()) {
184- classLoader .close ();
185- throw new PluginLoadException (
186- pluginId ,
187- "No SynapsePlugin implementation found in JAR. " +
188- "Ensure META-INF/services/dev.synapse.plugin.api.SynapsePlugin is present."
189- );
190- }
191-
192- SynapsePlugin instance = instanceOpt .get ();
193-
194153 // Validate id matches
195154 if (!pluginId .equals (instance .getId ())) {
196155 classLoader .close ();
@@ -289,15 +248,138 @@ public LoadedPlugin loadPlugin(Plugin dbPlugin) throws PluginLoadException {
289248 Plugin .LoaderState .ERROR ,
290249 e .getMessage ()
291250 );
251+ closeQuietly (classLoader );
292252 throw e ;
293253 } catch (Exception e ) {
294254 String msg =
295255 "Unexpected error during plugin load: " + e .getMessage ();
296256 updateLoaderState (pluginId , Plugin .LoaderState .ERROR , msg );
257+ closeQuietly (classLoader );
297258 throw new PluginLoadException (pluginId , msg , e );
298259 }
299260 }
300261
262+ private ModuleLayer tryCreatePluginLayer (
263+ Path jarPath ,
264+ URLClassLoader classLoader
265+ ) throws Exception {
266+ ModuleFinder pluginFinder = ModuleFinder .of (jarPath );
267+ Set <ModuleDescriptor > descriptors = pluginFinder
268+ .findAll ()
269+ .stream ()
270+ .map (java .lang .module .ModuleReference ::descriptor )
271+ .collect (Collectors .toSet ());
272+
273+ if (descriptors .isEmpty () || !SynapsePlugin .class .getModule ().isNamed ()) {
274+ return null ;
275+ }
276+
277+ Set <String > moduleNames = descriptors
278+ .stream ()
279+ .map (ModuleDescriptor ::name )
280+ .collect (Collectors .toSet ());
281+
282+ Configuration parentConfig = ModuleLayer .boot ().configuration ();
283+ Configuration pluginConfig = parentConfig .resolve (
284+ pluginFinder ,
285+ ModuleFinder .of (),
286+ moduleNames
287+ );
288+
289+ return ModuleLayer .defineModulesWithOneLoader (
290+ pluginConfig ,
291+ List .of (ModuleLayer .boot ()),
292+ classLoader
293+ ).layer ();
294+ }
295+
296+ private SynapsePlugin discoverPluginInstance (
297+ Plugin dbPlugin ,
298+ ModuleLayer pluginLayer ,
299+ URLClassLoader classLoader
300+ ) throws Exception {
301+ if (pluginLayer != null ) {
302+ ServiceLoader <SynapsePlugin > layerLoader = ServiceLoader .load (
303+ pluginLayer ,
304+ SynapsePlugin .class
305+ );
306+ Optional <SynapsePlugin > instanceOpt = layerLoader .findFirst ();
307+ if (instanceOpt .isPresent ()) {
308+ return instanceOpt .get ();
309+ }
310+ }
311+
312+ ServiceLoader <SynapsePlugin > classPathLoader = ServiceLoader .load (
313+ SynapsePlugin .class ,
314+ classLoader
315+ );
316+ Optional <SynapsePlugin > serviceInstance = classPathLoader .findFirst ();
317+ if (serviceInstance .isPresent ()) {
318+ return serviceInstance .get ();
319+ }
320+
321+ String entryPoint = extractEntryPoint (dbPlugin , classLoader );
322+ if (entryPoint == null || entryPoint .isBlank ()) {
323+ throw new PluginLoadException (
324+ dbPlugin .getId (),
325+ "No SynapsePlugin implementation found in JAR and manifest entry_point is missing."
326+ );
327+ }
328+
329+ Class <?> pluginClass = Class .forName (entryPoint , true , classLoader );
330+ if (!SynapsePlugin .class .isAssignableFrom (pluginClass )) {
331+ throw new PluginLoadException (
332+ dbPlugin .getId (),
333+ "Manifest entry_point does not implement SynapsePlugin: " + entryPoint
334+ );
335+ }
336+
337+ @ SuppressWarnings ("unchecked" )
338+ Class <? extends SynapsePlugin > typedClass =
339+ (Class <? extends SynapsePlugin >) pluginClass ;
340+ Constructor <? extends SynapsePlugin > constructor =
341+ typedClass .getDeclaredConstructor ();
342+ constructor .setAccessible (true );
343+ return constructor .newInstance ();
344+ }
345+
346+ private String extractEntryPoint (Plugin dbPlugin , URLClassLoader classLoader ) {
347+ Map <String , Object > manifest = dbPlugin .getManifest ();
348+ if (manifest != null ) {
349+ Object entryPoint = manifest .get ("entry_point" );
350+ if (entryPoint != null ) {
351+ return entryPoint .toString ();
352+ }
353+ }
354+
355+ URL manifestUrl = classLoader .getResource ("manifest.yml" );
356+ if (manifestUrl == null ) {
357+ return null ;
358+ }
359+ try (var input = manifestUrl .openStream ()) {
360+ String content = new String (
361+ input .readAllBytes (),
362+ StandardCharsets .UTF_8
363+ );
364+ for (String line : content .split ("\\ R" )) {
365+ String trimmed = line .trim ();
366+ if (trimmed .startsWith ("entry_point:" )) {
367+ return trimmed .substring ("entry_point:" .length ()).trim ();
368+ }
369+ }
370+ } catch (Exception ignored ) {}
371+ return null ;
372+ }
373+
374+ private void closeQuietly (URLClassLoader classLoader ) {
375+ if (classLoader == null ) {
376+ return ;
377+ }
378+ try {
379+ classLoader .close ();
380+ } catch (Exception ignored ) {}
381+ }
382+
301383 /**
302384 * Unloads a plugin by id. Calls onUnload(), closes ClassLoader, removes from memory.
303385 *
@@ -310,23 +392,32 @@ public boolean unloadPlugin(String pluginId) {
310392 return false ;
311393 }
312394
313- Plugin dbPlugin = pluginRepository .findById (pluginId ).orElse (null );
314- if (dbPlugin != null ) {
315- sandboxService .runLifecycleHookWithTimeout (
316- pluginId ,
317- () -> {
318- try {
319- loaded .instance ().onUnload ();
320- } catch (Exception e ) {
321- // Logged by sandbox service; do not block unload
322- }
323- },
324- false ,
325- dbPlugin
326- );
327- } else {
328- loaded .unload ();
395+ try {
396+ Plugin dbPlugin = pluginRepository .findById (pluginId ).orElse (null );
397+ if (dbPlugin != null ) {
398+ sandboxService .runLifecycleHookWithTimeout (
399+ pluginId ,
400+ () -> {
401+ try {
402+ loaded .instance ().onUnload ();
403+ } catch (Exception e ) {
404+ // Logged by sandbox service; do not block unload
405+ }
406+ },
407+ false ,
408+ dbPlugin
409+ );
410+ } else {
411+ try {
412+ loaded .instance ().onUnload ();
413+ } catch (Exception e ) {
414+ // Best-effort unload without database metadata
415+ }
416+ }
417+ } finally {
418+ closeQuietly (loaded .classLoader ());
329419 }
420+
330421 updateLoaderState (pluginId , Plugin .LoaderState .UNLOADED , null );
331422
332423 logService .log (
0 commit comments