From: Mathieu Baudier Date: Mon, 8 May 2023 05:41:25 +0000 (+0200) Subject: Can specify JShell bundle X-Git-Tag: v2.3.16~9 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=c31e21e07586e1a3937ac3c79d71941f047234ae;p=lgpl%2Fargeo-commons.git Can specify JShell bundle --- diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java index aec3da1f6..4df9b819e 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java @@ -1,6 +1,7 @@ package org.argeo.cms.jshell; import java.io.IOException; +import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; @@ -16,6 +17,8 @@ import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsState; import org.argeo.api.uuid.UuidFactory; import org.argeo.cms.util.OS; +import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider; +import org.osgi.framework.Bundle; public class CmsJShell { private final static CmsLog log = CmsLog.getLog(CmsJShell.class); @@ -26,17 +29,24 @@ public class CmsJShell { private CmsState cmsState; private Map localSessions = new HashMap<>(); + private Map bundleDirs = new HashMap<>(); + private Path stateRunDir; private Path localBase; private Path linkedDir; +// private String defaultBundle = "org.argeo.cms.cli"; + public void start() throws Exception { // Path localBase = cmsState.getStatePath("org.argeo.cms.jshell/local"); - UUID stateUuid = cmsState.getUuid(); +// UUID stateUuid = cmsState.getUuid(); + + // TODO better define application id, make it configurable + String applicationID = cmsState.getStatePath("").getFileName().toString(); // TODO centralise state run dir - Path stateRunDir = OS.getRunDir().resolve(stateUuid.toString()); + stateRunDir = OS.getRunDir().resolve(applicationID); localBase = stateRunDir.resolve("jsh"); Files.createDirectories(localBase); @@ -50,14 +60,34 @@ public class CmsJShell { localBase.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + try (DirectoryStream bundleSns = Files.newDirectoryStream(localBase)) { + for (Path bundleSnDir : bundleSns) { + addBundleSnDir(bundleSnDir); + if (bundleDirs.containsKey(bundleSnDir)) { + bundleSnDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE); + } + } + } WatchKey key; while ((key = watchService.take()) != null) { events: for (WatchEvent event : key.pollEvents()) { // System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context() + "."); - Path path = localBase.resolve((Path) event.context()); + Path parent = (Path) key.watchable(); // sessions - if (Files.isSameFile(localBase, path.getParent())) { + if (Files.isSameFile(localBase, parent)) { + Path bundleSnDir = localBase.resolve((Path) event.context()); + if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) { + addBundleSnDir(bundleSnDir); + if (bundleDirs.containsKey(bundleSnDir)) { + bundleSnDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE); + } + } else if (StandardWatchEventKinds.ENTRY_DELETE.equals(event.kind())) { + } + } else { + Path path = parent.resolve((Path) event.context()); if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) { if (!Files.isDirectory(path)) { log.warn("Ignoring " + path + " as it is not a directory"); @@ -70,32 +100,14 @@ public class CmsJShell { continue events; } - LocalJShellSession localSession = new LocalJShellSession(path); + Path bundleIdDir = bundleDirs.get(parent); + LocalJShellSession localSession = new LocalJShellSession(path, bundleIdDir); localSessions.put(path, localSession); } else if (StandardWatchEventKinds.ENTRY_DELETE.equals(event.kind())) { // TODO clean up session LocalJShellSession localSession = localSessions.remove(path); localSession.cleanUp(); } - } else { -// if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) { -// Path sessionDir = path.getParent(); -// LocalSession session = localSessions.get(sessionDir); -// if (session == null) { -// sessions: for (Path p : localSessions.keySet()) { -// if (Files.isSameFile(sessionDir, p)) { -// session = localSessions.get(p); -// break sessions; -// } -// } -// } -// if (session == null) { -// log.warn("Ignoring " + path + " as its parent is not a registered session"); -// continue events; -// } -// session.addChild(path); -// } - } } key.reset(); @@ -103,7 +115,7 @@ public class CmsJShell { } catch (IOException | InterruptedException e) { e.printStackTrace(); } - }, "JSChell local sessions watcher").start(); + }, "JShell local sessions watcher").start(); // thread context class loader should be where the service is defined // Thread.currentThread().setContextClassLoader(loader); @@ -113,6 +125,19 @@ public class CmsJShell { } + private void addBundleSnDir(Path bundleSnDir) throws IOException { + String symbolicName = bundleSnDir.getFileName().toString(); + Bundle fromBundle = OsgiExecutionControlProvider.getBundleFromSn(symbolicName); + if (fromBundle == null) { + log.error("Ignoring bundle " + symbolicName + " because it was not found"); + return; + } + Long bundleId = fromBundle.getBundleId(); + Path bundleIdDir = stateRunDir.resolve(bundleId.toString()); + Files.createDirectories(bundleIdDir); + bundleDirs.put(bundleSnDir, bundleIdDir); + } + // public void startX(BundleContext bc) { // uuidFactory = new NoOpUuidFactory(); // diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java index c86049881..f458c5c29 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java @@ -5,24 +5,23 @@ import java.io.Console; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.System.Logger; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.Channels; +import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardWatchEventKinds; -import java.nio.file.WatchEvent; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; import java.util.UUID; public class JShellClient { + private final static Logger logger = System.getLogger(JShellClient.class.getName()); + public final static String STDIO = "stdio"; public final static String STDERR = "stderr"; public final static String CMDIO = "cmdio"; @@ -32,6 +31,7 @@ public class JShellClient { public static void main(String[] args) throws IOException, InterruptedException { try { Path targetStateDirectory = Paths.get(args[0]); + String symbolicName = args[1]; Path localBase = targetStateDirectory.resolve("jsh"); if (Files.isSymbolicLink(localBase)) { localBase = localBase.toRealPath(); @@ -42,44 +42,60 @@ public class JShellClient { toRawTerminal(); } - SocketPipeSource std = new SocketPipeSource(); - std.setInputStream(System.in); - std.setOutputStream(System.out); + SocketPipeSource stdio = new SocketPipeSource(); + stdio.setInputStream(System.in); + stdio.setOutputStream(System.out); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + // logger.log(Logger.Level.INFO, "Shutting down..."); + System.out.println("\nShutting down..."); + stdio.shutdown(); + }, "Shut down JShell client")); + Path bundleSnDir = localBase.resolve(symbolicName); + if (!Files.exists(bundleSnDir)) + Files.createDirectory(bundleSnDir); UUID uuid = UUID.randomUUID(); - Path sessionDir = localBase.resolve(uuid.toString()); + Path sessionDir = bundleSnDir.resolve(uuid.toString()); Files.createDirectory(sessionDir); - Path stdPath = sessionDir.resolve(JShellClient.STDIO); - - // wait for sockets to be available - WatchService watchService = FileSystems.getDefault().newWatchService(); - sessionDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE); - WatchKey key; - watch: while ((key = watchService.take()) != null) { - for (WatchEvent event : key.pollEvents()) { - Path path = sessionDir.resolve((Path) event.context()); - if (Files.isSameFile(stdPath, path)) { - break watch; - } - } + Path stdioPath = sessionDir.resolve(JShellClient.STDIO); + + while (!(Files.exists(stdioPath))) { + // TODO timeout + Thread.sleep(50); + +// // wait for sockets to be available +// WatchService watchService = FileSystems.getDefault().newWatchService(); +// sessionDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE); +// WatchKey key; +// watch: while ((key = watchService.take()) != null) { +// for (WatchEvent event : key.pollEvents()) { +// Path path = sessionDir.resolve((Path) event.context()); +// if (Files.isSameFile(stdioPath, path)) { +// break watch; +// } +// } +// } +// watchService.close(); } - watchService.close(); - UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdPath); + UnixDomainSocketAddress stdioSocketAddress = UnixDomainSocketAddress.of(stdioPath.toRealPath()); - SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX); - channel.connect(stdSocketAddress); + try (SocketChannel stdioChannel = SocketChannel.open(StandardProtocolFamily.UNIX)) { + stdioChannel.connect(stdioSocketAddress); + stdio.forward(stdioChannel); + } - std.forward(channel); } catch (IOException | InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { - try { - stty(ttyConfig.trim()); - } catch (Exception e) { - System.err.println("Exception restoring tty config"); - } + if (ttyConfig != null) + try { + stty(ttyConfig.trim()); + } catch (Exception e) { + System.err.println("Exception restoring tty config"); + } } } @@ -309,8 +325,10 @@ class SocketPipeSource { private WritableByteChannel outChannel; private Thread readOutThread; + private Thread forwardThread; public void forward(SocketChannel channel) throws IOException { + forwardThread = Thread.currentThread(); readOutThread = new Thread(() -> { try { @@ -323,6 +341,8 @@ class SocketPipeSource { buffer.rewind(); } System.exit(0); + } catch (ClosedByInterruptException e) { + // silent } catch (IOException e) { e.printStackTrace(); } @@ -341,6 +361,11 @@ class SocketPipeSource { while (channel.isConnected()) { if (inChannel.read(buffer) < 0) break; +// int b = (int) buffer.get(0); +// if (b == 0x1B) { +// System.out.println("Ctrl+C"); +// } + buffer.flip(); channel.write(buffer); buffer.rewind(); @@ -356,6 +381,21 @@ class SocketPipeSource { } } + public void shutdown() { + try { + inChannel.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + outChannel.close(); + } catch (IOException e) { + e.printStackTrace(); + } + forwardThread.interrupt(); + readOutThread.interrupt(); + } + public void setInputStream(InputStream in) { inChannel = Channels.newChannel(in); } diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java index 786ee272d..196ee0468 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java @@ -1,17 +1,13 @@ package org.argeo.cms.jshell; -import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.net.StandardProtocolFamily; -import java.net.URI; import java.net.UnixDomainSocketAddress; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.StringJoiner; import java.util.UUID; import java.util.concurrent.Executors; @@ -21,6 +17,7 @@ import javax.security.auth.login.LoginException; import org.argeo.api.cms.CmsAuth; import org.argeo.api.cms.CmsLog; import org.argeo.cms.util.CurrentSubject; +import org.argeo.cms.util.FsUtils; import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider; import jdk.jshell.tool.JavaShellToolBuilder; @@ -30,8 +27,7 @@ class LocalJShellSession implements Runnable { private UUID uuid; private Path sessionDir; - - private String fromBundle = "eu.netiket.on.apaf.project.togo2023"; + private Path socketsDir; private Path stdioPath; private Path stderrPath; @@ -41,21 +37,32 @@ class LocalJShellSession implements Runnable { private LoginContext loginContext; - LocalJShellSession(Path sessionDir) { - this.sessionDir = sessionDir; - this.uuid = UUID.fromString(sessionDir.getFileName().toString()); - - stdioPath = sessionDir.resolve(JShellClient.STDIO); + private Long bundleId; - // TODO proper login + LocalJShellSession(Path sessionDir, Path bundleIdDir) { try { - loginContext = new LoginContext(CmsAuth.DATA_ADMIN.getLoginContextName()); - loginContext.login(); - } catch (LoginException e1) { - throw new RuntimeException("Could not login as data admin", e1); - } finally { - } + this.sessionDir = sessionDir; + this.uuid = UUID.fromString(sessionDir.getFileName().toString()); + bundleId = Long.parseLong(bundleIdDir.getFileName().toString()); + socketsDir = bundleIdDir.resolve(uuid.toString()); + Files.createDirectories(socketsDir); + + stdioPath = socketsDir.resolve(JShellClient.STDIO); + Files.createSymbolicLink(sessionDir.resolve(JShellClient.STDIO), stdioPath); + + // TODO proper login + try { + loginContext = new LoginContext(CmsAuth.DATA_ADMIN.getLoginContextName()); + loginContext.login(); + } catch (LoginException e1) { + throw new RuntimeException("Could not login as data admin", e1); + } finally { + } + } catch (IOException e) { + log.error("Cannot initiate local session " + uuid, e); + cleanUp(); + } replThread = new Thread(() -> CurrentSubject.callAs(loginContext.getSubject(), Executors.callable(this)), "JShell " + sessionDir); replThread.start(); @@ -80,9 +87,9 @@ class LocalJShellSession implements Runnable { try (SocketChannel channel = stdServerChannel.accept()) { std.open(channel); - String frameworkLocation = System.getProperty("osgi.framework"); - StringJoiner classpath = new StringJoiner(File.pathSeparator); - classpath.add(Paths.get(URI.create(frameworkLocation)).toAbsolutePath().toString()); +// StringJoiner classpath = new StringJoiner(File.pathSeparator); +// String frameworkLocation = System.getProperty("osgi.framework"); +// classpath.add(Paths.get(URI.create(frameworkLocation)).toAbsolutePath().toString()); ClassLoader cmsJShellBundleCL = OsgiExecutionControlProvider.class.getClassLoader(); ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); @@ -93,8 +100,9 @@ class LocalJShellSession implements Runnable { // // START JSHELL // - int exitCode = builder.start("--execution", "osgi:bundle(" + fromBundle + ")", "--class-path", - classpath.toString()); + int exitCode = builder.start("--execution", "osgi:bundle(" + bundleId + ")", "--class-path", + OsgiExecutionControlProvider.getBundleClasspath(bundleId), "--startup", + OsgiExecutionControlProvider.getBundleStartupScript(bundleId).toString()); // log.debug("JShell " + sessionDir + " completed with exit code " + exitCode); } finally { @@ -111,10 +119,10 @@ class LocalJShellSession implements Runnable { void cleanUp() { try { - if (Files.exists(stdioPath)) - Files.delete(stdioPath); + if (Files.exists(socketsDir)) + FsUtils.delete(socketsDir); if (Files.exists(sessionDir)) - Files.delete(sessionDir); + FsUtils.delete(sessionDir); } catch (IOException e) { log.error("Cannot clean up JShell " + sessionDir, e); } diff --git a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java index 673233cde..f785919b2 100644 --- a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java +++ b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java @@ -1,11 +1,20 @@ package org.argeo.internal.cms.jshell.osgi; +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.Objects; import java.util.Set; +import java.util.StringJoiner; import java.util.TreeMap; import java.util.TreeSet; @@ -46,45 +55,119 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider { public ExecutionControl generate(ExecutionEnv env, Map parameters) throws Throwable { // TODO find a better way to get a default bundle context // NOTE: the related default bundle has to be started - BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext(); - String symbolicName = parameters.get(BUNDLE_PARAMETER); +// String symbolicName = parameters.get(BUNDLE_PARAMETER); +// Bundle fromBundle = getBundleFromSn(symbolicName); + + Long bundleId = Long.parseLong(parameters.get(BUNDLE_PARAMETER)); + Bundle fromBundle = getBundleFromId(bundleId); + + BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class); + ClassLoader fromBundleClassLoader = fromBundleWiring.getClassLoader(); + + // use the bundle classloade as context classloader + Thread.currentThread().setContextClassLoader(fromBundleClassLoader); + + ExecutionControl executionControl = new DirectExecutionControl( + new WrappingLoaderDelegate(fromBundleClassLoader)); + log.debug("JShell from " + fromBundle.getSymbolicName() + "_" + fromBundle.getVersion() + " [" + + fromBundle.getBundleId() + "]"); + return executionControl; + } + + public static Bundle getBundleFromSn(String symbolicName) { + BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext(); Objects.requireNonNull(symbolicName); NavigableMap bundles = new TreeMap(); for (Bundle b : bc.getBundles()) { if (symbolicName.equals(b.getSymbolicName())) bundles.put(b.getVersion(), b); } + if (bundles.isEmpty()) + return null; Bundle fromBundle = bundles.lastEntry().getValue(); + return fromBundle; + } + + public static Bundle getBundleFromId(Long bundleId) { + BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext(); + Bundle fromBundle = bc.getBundle(bundleId); + return fromBundle; + } + + public static Path getBundleStartupScript(Long bundleId) { + BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext(); + Bundle fromBundle = bc.getBundle(bundleId); + Path bundleStartupScript = fromBundle.getDataFile("BUNDLE.jsh").toPath(); BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class); ClassLoader fromBundleClassLoader = fromBundleWiring.getClassLoader(); Set packagesToImport = new TreeSet<>(); - Set bundlesToAddToCompileClasspath = new TreeSet<>(); - // from bundle - bundlesToAddToCompileClasspath.add(fromBundle); // from bundle packages for (Package pkg : fromBundleClassLoader.getDefinedPackages()) { packagesToImport.add(pkg.getName()); } -// System.out.println(Arrays.asList(fromBundleClassLoader.getDefinedPackages())); List bundleWires = fromBundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE); for (BundleWire bw : bundleWires) { -// System.out.println(bw.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE)); - bundlesToAddToCompileClasspath.add(bw.getProviderWiring().getBundle()); packagesToImport.add(bw.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE).toString()); } - log.debug("JShell from " + fromBundle.getSymbolicName() + "_" + fromBundle.getVersion() + " [" - + fromBundle.getBundleId() + "]"); - log.debug(" required packages " + packagesToImport); - log.debug(" required bundles " + bundlesToAddToCompileClasspath); - ExecutionControl executionControl = new DirectExecutionControl( - new WrappingLoaderDelegate(fromBundleClassLoader)); - return executionControl; + try (Writer writer = Files.newBufferedWriter(bundleStartupScript, StandardCharsets.UTF_8)) { + for (String p : packagesToImport) { + writer.write("import " + p + ".*;\n"); + } + } catch (IOException e) { + throw new RuntimeException("Cannot writer bundle startup script to " + bundleStartupScript, e); + } + + return bundleStartupScript; } + public static String getBundleClasspath(Long bundleId) throws IOException { + String framework = System.getProperty("osgi.framework"); + Path frameworkLocation = Paths.get(URI.create(framework)).toAbsolutePath(); + BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext(); + Bundle fromBundle = bc.getBundle(bundleId); + + BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class); + + Set bundlesToAddToCompileClasspath = new TreeSet<>(); + + // from bundle + bundlesToAddToCompileClasspath.add(fromBundle); + + List bundleWires = fromBundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE); + for (BundleWire bw : bundleWires) { + bundlesToAddToCompileClasspath.add(bw.getProviderWiring().getBundle()); + } + + StringJoiner classpath = new StringJoiner(File.pathSeparator); + bundles: for (Bundle b : bundlesToAddToCompileClasspath) { + if (b.getBundleId() == 0) {// system bundle + classpath.add(frameworkLocation.toString()); + continue bundles; + } + Path p = bundleToPath(frameworkLocation, b); + classpath.add(p.toString()); + } + + return classpath.toString(); + } + + static Path bundleToPath(Path frameworkLocation, Bundle bundle) throws IOException { + String location = bundle.getLocation(); + if (location.startsWith("initial@reference:file:")) { + location = location.substring("initial@reference:file:".length()); + Path p = frameworkLocation.getParent().resolve(location).toRealPath(); + // TODO load dev.properties from OSGi configuration directory + if (Files.isDirectory(p)) + p = p.resolve("bin"); + return p; + } + Path p = Paths.get(location); + return p; + } }