From 641e3f3c535f540afd122d2a7b53389342f77002 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 8 Mar 2024 18:13:41 +0100 Subject: [PATCH] Introduce OSGi sub framework with shared class loaders --- .../org/argeo/api/init/RuntimeManager.java | 65 ++-- .../org/argeo/init/RuntimeManagerMain.java | 11 +- .../src/org/argeo/init/osgi/OsgiBoot.java | 145 +++++---- .../src/org/argeo/init/osgi/OsgiBuilder.java | 8 +- .../argeo/init/osgi/OsgiRuntimeContext.java | 2 +- .../init/osgi/SubFrameworkActivator.java | 295 ++++++++++++++++++ 6 files changed, 425 insertions(+), 101 deletions(-) create mode 100644 org.argeo.init/src/org/argeo/init/osgi/SubFrameworkActivator.java diff --git a/org.argeo.init/src/org/argeo/api/init/RuntimeManager.java b/org.argeo.init/src/org/argeo/api/init/RuntimeManager.java index da15b1ebc..cb8caeda9 100644 --- a/org.argeo.init/src/org/argeo/api/init/RuntimeManager.java +++ b/org.argeo.init/src/org/argeo/api/init/RuntimeManager.java @@ -19,27 +19,54 @@ public interface RuntimeManager { public void closeRuntime(String relPath, boolean async); + /** + * Load configs recursively starting with the parent directories, until a + * jvm.args file is found. + */ static void loadConfig(Path dir, Map config) { - try { - // System.out.println("Load from " + dir); - Path jvmArgsPath = dir.resolve(RuntimeManager.JVM_ARGS); - if (!Files.exists(jvmArgsPath)) { - // load from parent directory - loadConfig(dir.getParent(), config); - } - - if (Files.exists(dir)) - for (Path p : Files.newDirectoryStream(dir, "*.ini")) { - Properties props = new Properties(); - try (InputStream in = Files.newInputStream(p)) { - props.load(in); - } - for (Object key : props.keySet()) { - config.put(key.toString(), props.getProperty(key.toString())); - } + try { + Path jvmArgsPath = dir.resolve(RuntimeManager.JVM_ARGS); + if (!Files.exists(jvmArgsPath)) { + // load from parent directory + loadConfig(dir.getParent(), config); + } + + if (Files.exists(dir)) + for (Path p : Files.newDirectoryStream(dir, "*.ini")) { + try (InputStream in = Files.newInputStream(p)) { + loadConfig(in, config); } - } catch (IOException e) { - throw new UncheckedIOException("Cannot load configuration from " + dir, e); + } + } catch (IOException e) { + throw new UncheckedIOException("Cannot load configuration from " + dir, e); + } + } + + /** + * Load config from a {@link Properties} formatted stream. If a property value + * starts with a '+' character, itis expected that the last character is a + * separator and it will be prepended to the existing value. + */ + static void loadConfig(InputStream in, Map config) throws IOException { + Properties props = new Properties(); + props.load(in); + for (Object k : props.keySet()) { + String key = k.toString(); + String value = props.getProperty(key); + if (value.length() > 1 && '+' == value.charAt(0)) { + String currentValue = config.get(key); + if (currentValue == null || "".equals(currentValue)) { + // remove the + and the trailing separator + value = value.substring(1, value.length() - 1); + config.put(key, value); + } else { + // remove the + but keep the trailing separator + value = value.substring(1); + config.put(key, value + currentValue); + } + } else { + config.put(key, value); } } + } } diff --git a/org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java b/org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java index 74560185d..f4ed507c0 100644 --- a/org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java +++ b/org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java @@ -17,11 +17,10 @@ import org.argeo.api.init.InitConstants; import org.argeo.api.init.RuntimeContext; import org.argeo.api.init.RuntimeManager; import org.argeo.init.logging.ThinLoggerFinder; +import org.argeo.init.osgi.OsgiBoot; import org.argeo.init.osgi.OsgiRuntimeContext; import org.argeo.internal.init.InternalState; -import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; import org.osgi.framework.FrameworkEvent; import org.osgi.framework.launch.Framework; @@ -69,16 +68,12 @@ public class RuntimeManagerMain implements RuntimeManager { BundleContext bc = managerRuntimeContext.getFramework().getBundleContext(); // uninstall init as a bundle since it will be available via OSGi system - for (Bundle b : bc.getBundles()) { - if (b.getSymbolicName().equals(SYMBOLIC_NAME_INIT)) { - b.uninstall(); - } - } + OsgiBoot.uninstallBundles(bc, SYMBOLIC_NAME_INIT); bc.registerService(RuntimeManager.class, this, new Hashtable<>(configuration)); logger.log(Level.DEBUG, "Registered runtime manager"); managerRuntimeContext.waitForStop(0); - } catch (InterruptedException | BundleException e) { + } catch (InterruptedException e) { e.printStackTrace(); System.exit(1); } diff --git a/org.argeo.init/src/org/argeo/init/osgi/OsgiBoot.java b/org.argeo.init/src/org/argeo/init/osgi/OsgiBoot.java index 9b9ed6a7a..963bab4d4 100644 --- a/org.argeo.init/src/org/argeo/init/osgi/OsgiBoot.java +++ b/org.argeo.init/src/org/argeo/init/osgi/OsgiBoot.java @@ -14,12 +14,12 @@ import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Properties; import java.util.ServiceLoader; import java.util.Set; import java.util.SortedMap; @@ -131,37 +131,15 @@ public class OsgiBoot { * with {@link BundleContext#getProperty(String)}. If these properties are * null, system properties are used instead. */ - public void bootstrap(Map properties) { + public void bootstrap() { try { long begin = System.currentTimeMillis(); - - // notify start - String osgiInstancePath = getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA); - String osgiConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA); - String osgiSharedConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA); - logger.log(DEBUG, () -> "OSGi bootstrap starting" // - + (osgiInstancePath != null ? " data: " + osgiInstancePath + "" : "") // - + (osgiConfigurationPath != null ? " state: " + osgiConfigurationPath + "" : "") // - + (osgiSharedConfigurationPath != null ? " config: " + osgiSharedConfigurationPath + "" : "") // - ); - - // legacy install bundles - installUrls(getBundlesUrls()); - installUrls(getDistributionUrls()); - - // A2 install bundles - provisioningManager.install(null); - + // Install bundles + install(); // Make sure fragments are properly considered by refreshing - refreshFramework(); - - // start bundles -// if (properties != null && !Boolean.parseBoolean(properties.get(PROP_OSGI_USE_SYSTEM_PROPERTIES))) - startBundles(properties); -// else -// startBundles(); - - // complete + refresh(); + // Start bundles + startBundles(); long duration = System.currentTimeMillis() - begin; logger.log(DEBUG, () -> "OSGi bootstrap completed in " + Math.round(((double) duration) / 1000) + "s (" + duration + "ms), " + bundleContext.getBundles().length + " bundles"); @@ -191,14 +169,23 @@ public class OsgiBoot { System.out.println(); } - /** - * Calls {@link #bootstrap(Map)} with null. - * - * @see #bootstrap(Map) - */ - @Deprecated - public void bootstrap() { - bootstrap(null); + public void install() { + String osgiInstancePath = getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA); + String osgiConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA); + String osgiSharedConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA); + logger.log(DEBUG, () -> "OSGi bootstrap starting" // + + (osgiInstancePath != null ? " data: " + osgiInstancePath + "" : "") // + + (osgiConfigurationPath != null ? " state: " + osgiConfigurationPath + "" : "") // + + (osgiSharedConfigurationPath != null ? " config: " + osgiSharedConfigurationPath + "" : "") // + ); + + // legacy install bundles + installUrls(getBundlesUrls()); + installUrls(getDistributionUrls()); + + // A2 install bundles + provisioningManager.install(null); + } public void update() { @@ -305,17 +292,17 @@ public class OsgiBoot { * * @see OsgiBoot#doStartBundles(Map) */ - public void startBundles(Map properties) { + public void startBundles() { Map map = new TreeMap<>(); // first use properties - if (properties != null) { - for (String key : properties.keySet()) { - String property = key; - if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) { - map.put(property, properties.get(property)); - } - } - } +// if (properties != null) { +// for (String key : properties.keySet()) { +// String property = key; +// if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) { +// map.put(property, properties.get(property)); +// } +// } +// } // then try all start level until a maximum int maxStartLevel = Integer .parseInt(getProperty(InitConstants.PROP_ARGEO_OSGI_MAX_START_LEVEL, DEFAULT_MAX_START_LEVEL)); @@ -327,28 +314,28 @@ public class OsgiBoot { } // finally, override with system properties - for (Object key : System.getProperties().keySet()) { - if (key.toString().startsWith(InitConstants.PROP_ARGEO_OSGI_START)) { - map.put(key.toString(), System.getProperty(key.toString())); - } - } +// for (Object key : System.getProperties().keySet()) { +// if (key.toString().startsWith(InitConstants.PROP_ARGEO_OSGI_START)) { +// map.put(key.toString(), System.getProperty(key.toString())); +// } +// } // start doStartBundles(map); } - void startBundles(Properties properties) { - Map map = new TreeMap<>(); - // first use properties - if (properties != null) { - for (Object key : properties.keySet()) { - String property = key.toString(); - if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) { - map.put(property, properties.get(property).toString()); - } - } - } - startBundles(map); - } +// void startBundles(Properties properties) { +// Map map = new TreeMap<>(); +// // first use properties +// if (properties != null) { +// for (Object key : properties.keySet()) { +// String property = key.toString(); +// if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) { +// map.put(property, properties.get(property).toString()); +// } +// } +// } +// startBundles(map); +// } /** * Start bundle based on keys starting with @@ -717,7 +704,7 @@ public class OsgiBoot { return (basePath + '/' + relativePath).replace('/', File.separatorChar); } - private void refreshFramework() { + public void refresh() { Bundle systemBundle = bundleContext.getBundle(0); FrameworkWiring frameworkWiring = systemBundle.adapt(FrameworkWiring.class); // TODO deal with refresh breaking native loading (e.g SWT) @@ -741,6 +728,14 @@ public class OsgiBoot { return getProperty(name, null); } + /* + * BEAN METHODS + */ + + public BundleContext getBundleContext() { + return bundleContext; + } + /* * PLAIN OSGI LAUNCHER */ @@ -765,11 +760,23 @@ public class OsgiBoot { } /* - * BEAN METHODS + * OSGI UTILITIES */ + /** Uninstall all bundles with these symbolic names */ + public static void uninstallBundles(BundleContext bc, String... symbolicNames) { + List lst = Arrays.asList(symbolicNames); + for (Bundle b : bc.getBundles()) { + String sn = b.getSymbolicName(); + if (sn == null) + continue; + if (lst.contains(sn)) { + try { + b.uninstall(); + } catch (BundleException e) { + logger.log(ERROR, "Cannot uninstall " + sn, e); + } + } + } - public BundleContext getBundleContext() { - return bundleContext; } - } diff --git a/org.argeo.init/src/org/argeo/init/osgi/OsgiBuilder.java b/org.argeo.init/src/org/argeo/init/osgi/OsgiBuilder.java index d89535e03..f1c16af91 100644 --- a/org.argeo.init/src/org/argeo/init/osgi/OsgiBuilder.java +++ b/org.argeo.init/src/org/argeo/init/osgi/OsgiBuilder.java @@ -12,7 +12,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; import java.util.TreeMap; @@ -53,6 +52,7 @@ public class OsgiBuilder { } public Framework launch() { + configuration.putAll(startLevelsToProperties()); // start OSGi framework = OsgiBoot.defaultOsgiLaunch(configuration); @@ -73,7 +73,7 @@ public class OsgiBuilder { } } // start bundles - osgiBoot.startBundles(startLevelsToProperties()); + osgiBoot.startBundles(); // if (OsgiBootUtils.isDebug()) // for (Bundle bundle : bc.getBundles()) { @@ -283,8 +283,8 @@ public class OsgiBuilder { // // UTILITIES // - private Properties startLevelsToProperties() { - Properties properties = new Properties(); + private Map startLevelsToProperties() { + Map properties = new HashMap<>(); for (Integer startLevel : startLevels.keySet()) { String property = InitConstants.PROP_ARGEO_OSGI_START + "." + startLevel; StringBuilder value = new StringBuilder(); diff --git a/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java b/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java index 4ec5a0473..2914be38a 100644 --- a/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java +++ b/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java @@ -100,7 +100,7 @@ public class OsgiRuntimeContext implements RuntimeContext, AutoCloseable { Thread osgiBootThread = new Thread("OSGi boot framework " + frameworkUuuid) { @Override public void run() { - osgiBoot.bootstrap(config); + osgiBoot.bootstrap(); } }; osgiBootThread.start(); diff --git a/org.argeo.init/src/org/argeo/init/osgi/SubFrameworkActivator.java b/org.argeo.init/src/org/argeo/init/osgi/SubFrameworkActivator.java new file mode 100644 index 000000000..3d6469df8 --- /dev/null +++ b/org.argeo.init/src/org/argeo/init/osgi/SubFrameworkActivator.java @@ -0,0 +1,295 @@ +package org.argeo.init.osgi; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.argeo.api.init.InitConstants; +import org.argeo.api.init.RuntimeManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleReference; +import org.osgi.framework.connect.ConnectContent; +import org.osgi.framework.connect.ConnectFrameworkFactory; +import org.osgi.framework.connect.ConnectModule; +import org.osgi.framework.connect.ModuleConnector; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.wiring.BundleWiring; + +public class SubFrameworkActivator implements BundleActivator { +// private final static String EQUINOX_FRAMEWORK_CLASS = "org.eclipse.osgi.launch.Equinox"; + private final static String EQUINOX_FRAMEWORK_FACTORY_CLASS = "org.eclipse.osgi.launch.EquinoxFactory"; + +// private ClassLoader bundleClassLoader; +// private ClassLoader subFrameworkClassLoader; + private BundleContext bundleContext; + + private ConnectFrameworkFactory frameworkFactory; + + @Override + public void start(BundleContext context) throws Exception { + this.bundleContext = context; + + try { + Bundle bundle = context.getBundle(); + ClassLoader bundleClassLoader = bundle.adapt(BundleWiring.class).getClassLoader(); +// subFrameworkClassLoader = new URLClassLoader(new URL[0], bundleClassLoader); + + @SuppressWarnings("unchecked") + Class frameworkFactoryClass = (Class) bundleClassLoader + .loadClass(EQUINOX_FRAMEWORK_FACTORY_CLASS); + frameworkFactory = frameworkFactoryClass.getConstructor().newInstance(); + + new Thread() { + + @Override + public void run() { + for (int i = 0; i < 5; i++) { + Map config = new HashMap<>(); + Path basePase = Paths.get(System.getProperty("user.home"), ".config/argeo/test/", "test" + i); + config.put(InitConstants.PROP_OSGI_CONFIGURATION_AREA, + basePase.resolve(RuntimeManager.STATE).toString()); + config.put(InitConstants.PROP_OSGI_INSTANCE_AREA, + basePase.resolve(RuntimeManager.DATA).toString()); + config.put("argeo.host", "host" + i); + config.put("osgi.console", "host" + i + ":2023"); + startFramework(config); + } + } + + }.start(); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + Framework startFramework(Map config) { + try { + URL bundleConfigUrl = bundleContext.getBundle().getEntry("config.ini"); + try (InputStream in = bundleConfigUrl.openStream()) { + RuntimeManager.loadConfig(in, config); + } + + // Equinox +// config.put("osgi.frameworkParentClassloader", "current"); +// config.put("osgi.parentClassLoader", "app"); +// config.put("osgi.contextClassLoaderParent", "app"); + + ModuleConnector moduleConnector = new ParentBundleModuleConnector(bundleContext); + +// URL frameworkUrl = URI.create(bundleContext.getProperty("osgi.framework")).toURL(); +// URLClassLoader frameworkClassLoader = new URLClassLoader(new URL[] { frameworkUrl, }); +// Class frameworkClass = (Class) frameworkClassLoader +// .loadClass(EQUINOX_FRAMEWORK_CLASS); +// Framework framework = frameworkClass.getConstructor(Map.class, ModuleConnector.class).newInstance(config, +// moduleConnector); + + Framework framework = frameworkFactory.newFramework(config, moduleConnector); + + framework.init(); + + for (Bundle b : bundleContext.getBundles()) { + if (b.getBundleId() == 0) + continue; + String location = b.getLocation(); + if (location.contains("/org.argeo.tp/") // + || location.contains("/org.argeo.tp.sys/") // + || location.contains("/org.argeo.tp.httpd/") // + || location.contains("/org.argeo.tp.sshd/") // + || location.contains("/org.argeo.cms/org.argeo.init") // + ) { + framework.getBundleContext().installBundle(b.getLocation()); + } + } + + OsgiBoot osgiBoot = new OsgiBoot(framework.getBundleContext()); + osgiBoot.install(); +// OsgiBoot.uninstallBundles(osgiBoot.getBundleContext(), "org.argeo.api.cms"); +// OsgiBoot.uninstallBundles(osgiBoot.getBundleContext(), "org.osgi.service.useradmin"); +// osgiBoot.getBundleContext() +// .installBundle("initial@reference:file:../../../../../argeo-commons/org.argeo.api.cms/"); +// osgiBoot.getBundleContext().installBundle( +// "reference:file:/usr/local/share/a2/osgi/equinox/org.argeo.tp.osgi/org.osgi.service.useradmin.1.1.jar"); + osgiBoot.refresh(); + framework.start(); + osgiBoot.startBundles(); + +// for (Bundle b : framework.getBundleContext().getBundles()) { +// BundleContext bc = b.getBundleContext(); +// if (bc == null) +// System.err.println(b.getSymbolicName() + " BC null"); +// } + return framework; + } catch (Exception e) { + throw new IllegalStateException("Cannot start framework", e); + } + } + + @Override + public void stop(BundleContext context) throws Exception { + bundleContext = null; + frameworkFactory = null; + } + + class ParentBundleModuleConnector implements ModuleConnector { + private final BundleContext foreignBundleContext; + private BundleContext localBundleContext; + + public ParentBundleModuleConnector(BundleContext foreignBundleContext) { + this.foreignBundleContext = foreignBundleContext; + } + + @Override + public Optional newBundleActivator() { + return Optional.of(new BundleActivator() { + @Override + public void start(BundleContext context) throws Exception { + ParentBundleModuleConnector.this.localBundleContext = context; + } + + @Override + public void stop(BundleContext context) throws Exception { + ParentBundleModuleConnector.this.localBundleContext = null; + } + + }); + } + + @Override + public void initialize(File storage, Map configuration) { + } + + @Override + public Optional connect(String location) throws BundleException { + Bundle bundle = foreignBundleContext.getBundle(location); + if (bundle != null && bundle.getBundleId() != 0) { + System.out.println("Foreign Bundle: " + bundle.getSymbolicName() + " " + location); + ConnectModule module = new ConnectModule() { + + @Override + public ConnectContent getContent() throws IOException { + return new ForeignBundleConnectContent(localBundleContext, bundle); + } + }; + return Optional.of(module); + } + return Optional.empty(); + } + } + + class ForeignBundleClassLoader extends ClassLoader implements BundleReference { + private BundleContext localBundleContext; + private Bundle foreignBundle; + + public ForeignBundleClassLoader(BundleContext localBundleContext, Bundle foreignBundle) { + super("Foreign bundle " + foreignBundle.toString(), Optional + .ofNullable(foreignBundle.adapt(BundleWiring.class)).map((bw) -> bw.getClassLoader()).orElse(null)); + this.localBundleContext = localBundleContext; + this.foreignBundle = foreignBundle; + } + + @Override + public Bundle getBundle() { + return localBundleContext.getBundle(foreignBundle.getLocation()); + } + } + + class ForeignBundleConnectContent implements ConnectContent { + private final Bundle bundle; + private final ClassLoader classLoader; + + public ForeignBundleConnectContent(BundleContext localBundleContext, Bundle bundle) { + this.bundle = bundle; + this.classLoader = new ForeignBundleClassLoader(localBundleContext, bundle); + } + + @Override + public Optional> getHeaders() { + Dictionary dict = bundle.getHeaders(); + List keys = Collections.list(dict.keys()); + Map dictCopy = keys.stream().collect(Collectors.toMap(Function.identity(), dict::get)); + return Optional.of(dictCopy); + } + + @Override + public Iterable getEntries() throws IOException { + List lst = Collections.list(bundle.findEntries("", "*", true)).stream().map((u) -> u.getPath()) + .toList(); + return lst; + } + + @Override + public Optional getEntry(String path) { + URL u = bundle.getEntry(path); + if (u == null) { + u = bundle.getEntry("bin/" + path); + // System.err.println(u2); + } + if (u == null) { + if ("plugin.xml".equals(path)) + return Optional.empty(); + System.err.println(bundle.getSymbolicName() + " " + path + " not found"); + return Optional.empty(); + } + URL url = u; + ConnectEntry urlConnectEntry = new ConnectEntry() { + + @Override + public String getName() { + return path; + } + + @Override + public long getLastModified() { + // FIXME + return System.currentTimeMillis(); + } + + @Override + public InputStream getInputStream() throws IOException { + return url.openStream(); + } + + @Override + public long getContentLength() { + return -1; + } + }; + return Optional.of(urlConnectEntry); + } + + @Override + public Optional getClassLoader() { + ClassLoader cl; + // cl = bundle.adapt(BundleWiring.class).getClassLoader(); + + // cl = subFrameworkClassLoader; + cl = classLoader; + return Optional.of(cl); +// return Optional.empty(); + } + + @Override + public void open() throws IOException { + } + + @Override + public void close() throws IOException { + } + + } +} -- 2.30.2