Introduce CMS JShell
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 2 May 2023 10:06:56 +0000 (12:06 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 2 May 2023 10:06:56 +0000 (12:06 +0200)
17 files changed:
Makefile
org.argeo.api.cms/src/org/argeo/api/cms/CmsState.java
org.argeo.cms.jshell/.classpath [new file with mode: 0644]
org.argeo.cms.jshell/.project [new file with mode: 0644]
org.argeo.cms.jshell/OSGI-INF/cmsJShell.xml [new file with mode: 0644]
org.argeo.cms.jshell/bnd.bnd [new file with mode: 0644]
org.argeo.cms.jshell/build.properties [new file with mode: 0644]
org.argeo.cms.jshell/src/META-INF/services/jdk.jshell.spi.ExecutionControlProvider [new file with mode: 0644]
org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java [new file with mode: 0644]
org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java [new file with mode: 0644]
org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java [new file with mode: 0644]
org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java [new file with mode: 0644]
org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java [new file with mode: 0644]
org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/KernelUtils.java
sdk/cms-e4-rap.properties

index 74920833fa431ecad193c2a1d78104420eea4846..3920b644aac6fa2b0b63f5941778026da2eb8029 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -19,6 +19,7 @@ org.argeo.cms.ee \
 org.argeo.cms.lib.jetty \
 org.argeo.cms.lib.dbus \
 org.argeo.cms.lib.sshd \
+org.argeo.cms.jshell \
 org.argeo.cms.cli \
 osgi/equinox/org.argeo.cms.lib.equinox \
 swt/org.argeo.swt.minidesktop \
index 181e4b9c661f716424f0d183bf97514431680b51..8703b6b62d795d1d7e1cbb86aee0a707dbe7f4d2 100644 (file)
@@ -21,4 +21,6 @@ public interface CmsState {
        List<String> getDeployProperties(String property);
 
        Path getDataPath(String relativePath);
+
+       Path getStatePath(String relativePath);
 }
diff --git a/org.argeo.cms.jshell/.classpath b/org.argeo.cms.jshell/.classpath
new file mode 100644 (file)
index 0000000..81fe078
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.jshell/.project b/org.argeo.cms.jshell/.project
new file mode 100644 (file)
index 0000000..99b8eee
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.jshell</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ds.core.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.jshell/OSGI-INF/cmsJShell.xml b/org.argeo.cms.jshell/OSGI-INF/cmsJShell.xml
new file mode 100644 (file)
index 0000000..05f74a6
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="stop" name="org.argeo.cms.cmsJShell">
+   <implementation class="org.argeo.cms.jshell.CmsJShell"/>
+   <reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
+</scr:component>
diff --git a/org.argeo.cms.jshell/bnd.bnd b/org.argeo.cms.jshell/bnd.bnd
new file mode 100644 (file)
index 0000000..e575175
--- /dev/null
@@ -0,0 +1,6 @@
+Import-Package: \
+org.osgi.framework.namespace, \
+*
+
+Service-Component:\
+OSGI-INF/cmsJShell.xml
diff --git a/org.argeo.cms.jshell/build.properties b/org.argeo.cms.jshell/build.properties
new file mode 100644 (file)
index 0000000..9bd0cca
--- /dev/null
@@ -0,0 +1,5 @@
+bin.includes = META-INF/,\
+               .,\
+               OSGI-INF/cmsJShell.xml
+source.. = src/
+output.. = bin/
diff --git a/org.argeo.cms.jshell/src/META-INF/services/jdk.jshell.spi.ExecutionControlProvider b/org.argeo.cms.jshell/src/META-INF/services/jdk.jshell.spi.ExecutionControlProvider
new file mode 100644 (file)
index 0000000..d21f5ba
--- /dev/null
@@ -0,0 +1 @@
+org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider
\ No newline at end of file
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
new file mode 100644 (file)
index 0000000..aec3da1
--- /dev/null
@@ -0,0 +1,278 @@
+package org.argeo.cms.jshell;
+
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.CmsState;
+import org.argeo.api.uuid.UuidFactory;
+import org.argeo.cms.util.OS;
+
+public class CmsJShell {
+       private final static CmsLog log = CmsLog.getLog(CmsJShell.class);
+       static ClassLoader loader = CmsJShell.class.getClassLoader();
+
+       public static UuidFactory uuidFactory = null;
+
+       private CmsState cmsState;
+
+       private Map<Path, LocalJShellSession> localSessions = new HashMap<>();
+
+       private Path localBase;
+       private Path linkedDir;
+
+       public void start() throws Exception {
+
+               // Path localBase = cmsState.getStatePath("org.argeo.cms.jshell/local");
+               UUID stateUuid = cmsState.getUuid();
+
+               // TODO centralise state run dir
+               Path stateRunDir = OS.getRunDir().resolve(stateUuid.toString());
+               localBase = stateRunDir.resolve("jsh");
+               Files.createDirectories(localBase);
+
+               linkedDir = Files.createSymbolicLink(cmsState.getStatePath("jsh"), localBase);
+
+               log.info("Local JShell on " + localBase + ", linked to " + linkedDir);
+
+               new Thread(() -> {
+                       try {
+                               WatchService watchService = FileSystems.getDefault().newWatchService();
+
+                               localBase.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());
+                                               // sessions
+                                               if (Files.isSameFile(localBase, path.getParent())) {
+                                                       if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) {
+                                                               if (!Files.isDirectory(path)) {
+                                                                       log.warn("Ignoring " + path + " as it is not a directory");
+                                                                       continue events;
+                                                               }
+                                                               try {
+                                                                       UUID.fromString(path.getFileName().toString());
+                                                               } catch (IllegalArgumentException e) {
+                                                                       log.warn("Ignoring " + path + " as it is not named as UUID");
+                                                                       continue events;
+                                                               }
+
+                                                               LocalJShellSession localSession = new LocalJShellSession(path);
+                                                               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();
+                               }
+                       } catch (IOException | InterruptedException e) {
+                               e.printStackTrace();
+                       }
+               }, "JSChell local sessions watcher").start();
+
+               // thread context class loader should be where the service is defined
+//             Thread.currentThread().setContextClassLoader(loader);
+//             JavaShellToolBuilder builder = JavaShellToolBuilder.builder();
+//
+//             builder.start("--execution", "osgi:bundle(org.argeo.cms.jshell)");
+
+       }
+
+//     public void startX(BundleContext bc) {
+//             uuidFactory = new NoOpUuidFactory();
+//
+//             List<String> locations = new ArrayList<>();
+//             for (Bundle bundle : bc.getBundles()) {
+//                     locations.add(bundle.getLocation());
+////                   System.out.println(bundle.getLocation());
+//             }
+//
+//             CmsState cmsState = (CmsState) bc.getService(bc.getServiceReference("org.argeo.api.cms.CmsState"));
+//             System.out.println(cmsState.getDeployProperties(CmsDeployProperty.HTTP_PORT.getProperty()));
+//             System.out.println(cmsState.getUuid());
+//
+//             ExecutionControlProvider executionControlProvider = new ExecutionControlProvider() {
+//                     @Override
+//                     public String name() {
+//                             return "name";
+//                     }
+//
+//                     @Override
+//                     public ExecutionControl generate(ExecutionEnv ee, Map<String, String> map) throws Throwable {
+//                             return new LocalExecutionControl(new WrappingLoaderDelegate(loader));
+////                           Thread.currentThread().setContextClassLoader(loader);
+////                           return new DirectExecutionControl();
+//                     }
+//             };
+//
+////           Thread.currentThread().setContextClassLoader(loader);
+//
+//             try (JShell js = JShell.builder().executionEngine(executionControlProvider, null).build()) {
+//                     js.addToClasspath("/home/mbaudier/dev/git/unstable/output/a2/org.argeo.cms/org.argeo.api.cms.2.3.jar");
+//                     js.addToClasspath("/home/mbaudier/dev/git/unstable/output/a2/org.argeo.cms/org.argeo.cms.2.3.jar");
+//                     js.addToClasspath(
+//                                     "/home/mbaudier/dev/git/unstable/output/a2/osgi/equinox/org.argeo.tp.osgi/org.eclipse.osgi.3.18.jar");
+////                   do {
+//                     System.out.print("Enter some Java code: ");
+//                     // String input = console.readLine();
+//                     String imports = """
+//                                     import org.argeo.api.cms.*;
+//                                     import org.argeo.cms.*;
+//                                     import org.argeo.slc.jshell.*;
+//                                     """;
+//                     js.eval(imports);
+//                     String input = """
+//                                     var bc = org.osgi.framework.FrameworkUtil.getBundle(org.argeo.cms.CmsDeployProperty.class).getBundleContext();
+//                                     var cmsState =(org.argeo.api.cms.CmsState) bc.getService(bc.getServiceReference("org.argeo.api.cms.CmsState"));
+//                                     System.out.println(cmsState.getDeployProperties(org.argeo.cms.CmsDeployProperty.HTTP_PORT.getProperty()));
+//                                     cmsState.getUuid();
+//                                             """;
+////                           if (input == null) {
+////                                   break;
+////                           }
+//
+//                     input.lines().forEach((l) -> {
+//
+//                             List<SnippetEvent> events = js.eval(l);
+//                             for (SnippetEvent e : events) {
+//                                     StringBuilder sb = new StringBuilder();
+//                                     if (e.causeSnippet() == null) {
+//                                             // We have a snippet creation event
+//                                             switch (e.status()) {
+//                                             case VALID:
+//                                                     sb.append("Successful ");
+//                                                     break;
+//                                             case RECOVERABLE_DEFINED:
+//                                                     sb.append("With unresolved references ");
+//                                                     break;
+//                                             case RECOVERABLE_NOT_DEFINED:
+//                                                     sb.append("Possibly reparable, failed  ");
+//                                                     break;
+//                                             case REJECTED:
+//                                                     sb.append("Failed ");
+//                                                     break;
+//                                             }
+//                                             if (e.previousStatus() == Status.NONEXISTENT) {
+//                                                     sb.append("addition");
+//                                             } else {
+//                                                     sb.append("modification");
+//                                             }
+//                                             sb.append(" of ");
+//                                             sb.append(e.snippet().source());
+//                                             System.out.println(sb);
+//                                             if (e.value() != null) {
+//                                                     System.out.printf("Value is: %s\n", e.value());
+//                                             }
+//                                             System.out.flush();
+//                                     }
+//                             }
+//                     });
+////                   } while (true);
+//             }
+//     }
+
+       public void stop() {
+               try {
+                       Files.delete(linkedDir);
+               } catch (IOException e) {
+                       log.error("Cannot remove " + linkedDir);
+               }
+       }
+
+       public void setCmsState(CmsState cmsState) {
+               this.cmsState = cmsState;
+       }
+
+//     public static void main(String[] args) throws Exception {
+//             Pipe inPipe = Pipe.open();
+//             Pipe outPipe = Pipe.open();
+//
+//             InputStream in = Channels.newInputStream(inPipe.source());
+//             OutputStream out = Channels.newOutputStream(outPipe.sink());
+//             JavaShellToolBuilder builder = JavaShellToolBuilder.builder();
+//             builder.in(in, null);
+//             builder.interactiveTerminal(true);
+//             builder.out(new PrintStream(out));
+//
+//             UnixDomainSocketAddress ioSocketAddress = JShellClient.ioSocketAddress();
+//             Files.deleteIfExists(ioSocketAddress.getPath());
+//
+//             try (ServerSocketChannel serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
+//                     serverChannel.bind(ioSocketAddress);
+//
+//                     try (SocketChannel channel = serverChannel.accept()) {
+//                             new Thread(() -> {
+//
+//                                     try {
+//                                             ByteBuffer buffer = ByteBuffer.allocate(1024);
+//                                             while (true) {
+//                                                     if (channel.read(buffer) < 0)
+//                                                             break;
+//                                                     buffer.flip();
+//                                                     inPipe.sink().write(buffer);
+//                                                     buffer.rewind();
+//                                             }
+//                                     } catch (IOException e) {
+//                                             e.printStackTrace();
+//                                     }
+//                             }, "Read in").start();
+//
+//                             new Thread(() -> {
+//
+//                                     try {
+//                                             ByteBuffer buffer = ByteBuffer.allocate(1024);
+//                                             while (true) {
+//                                                     if (outPipe.source().read(buffer) < 0)
+//                                                             break;
+//                                                     buffer.flip();
+//                                                     channel.write(buffer);
+//                                                     buffer.rewind();
+//                                             }
+//                                     } catch (IOException e) {
+//                                             e.printStackTrace();
+//                                     }
+//                             }, "Write out").start();
+//
+//                             builder.start();
+//                     }
+//             } finally {
+//                     System.out.println("Completed");
+//             }
+//     }
+
+}
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
new file mode 100644 (file)
index 0000000..c860498
--- /dev/null
@@ -0,0 +1,366 @@
+package org.argeo.cms.jshell;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Console;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.StandardProtocolFamily;
+import java.net.UnixDomainSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+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 {
+       public final static String STDIO = "stdio";
+       public final static String STDERR = "stderr";
+       public final static String CMDIO = "cmdio";
+
+       private static String ttyConfig;
+
+       public static void main(String[] args) throws IOException, InterruptedException {
+               try {
+                       Path targetStateDirectory = Paths.get(args[0]);
+                       Path localBase = targetStateDirectory.resolve("jsh");
+                       if (Files.isSymbolicLink(localBase)) {
+                               localBase = localBase.toRealPath();
+                       }
+
+                       Console console = System.console();
+                       if (console != null) {
+                               toRawTerminal();
+                       }
+
+                       SocketPipeSource std = new SocketPipeSource();
+                       std.setInputStream(System.in);
+                       std.setOutputStream(System.out);
+
+                       UUID uuid = UUID.randomUUID();
+                       Path sessionDir = localBase.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;
+                                       }
+                               }
+                       }
+                       watchService.close();
+
+                       UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdPath);
+
+                       SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX);
+                       channel.connect(stdSocketAddress);
+
+                       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");
+                       }
+               }
+
+       }
+
+       private static void toRawTerminal() throws IOException, InterruptedException {
+
+               ttyConfig = stty("-g");
+
+               // set the console to be character-buffered instead of line-buffered
+               stty("-icanon min 1");
+
+               // disable character echoing
+               stty("-echo");
+
+               Runtime.getRuntime().addShutdownHook(new Thread(() -> toOriginalTerminal(), "Reset terminal"));
+       }
+
+       private static void toOriginalTerminal() {
+               if (ttyConfig != null)
+                       try {
+                               stty(ttyConfig.trim());
+                       } catch (Exception e) {
+                               System.err.println("Exception restoring tty config");
+                       }
+       }
+
+       /**
+        * Execute the stty command with the specified arguments against the current
+        * active terminal.
+        */
+       private static String stty(final String args) throws IOException, InterruptedException {
+               String cmd = "stty " + args + " < /dev/tty";
+
+               return exec(new String[] { "sh", "-c", cmd });
+       }
+
+       /**
+        * Execute the specified command and return the output (both stdout and stderr).
+        */
+       private static String exec(final String[] cmd) throws IOException, InterruptedException {
+               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+
+               Process p = Runtime.getRuntime().exec(cmd);
+               int c;
+               InputStream in = p.getInputStream();
+
+               while ((c = in.read()) != -1) {
+                       bout.write(c);
+               }
+
+               in = p.getErrorStream();
+
+               while ((c = in.read()) != -1) {
+                       bout.write(c);
+               }
+
+               p.waitFor();
+
+               String result = new String(bout.toByteArray());
+               return result;
+       }
+
+//     void pipe() throws IOException {
+//             // Set up Server Socket and bind to the port 8000
+//             ServerSocketChannel server = ServerSocketChannel.open();
+//             SocketAddress endpoint = new InetSocketAddress(8000);
+//             server.socket().bind(endpoint);
+//
+//             server.configureBlocking(false);
+//
+//             // Set up selector so we can run with a single thread but multiplex between 2
+//             // channels
+//             Selector selector = Selector.open();
+//             server.register(selector, SelectionKey.OP_ACCEPT);
+//
+//             ByteBuffer buffer = ByteBuffer.allocate(1024);
+//
+//             while (true) {
+//                     // block until data comes in
+//                     selector.select();
+//
+//                     Set<SelectionKey> keys = selector.selectedKeys();
+//
+//                     for (SelectionKey key : keys) {
+//                             if (!key.isValid()) {
+//                                     // not valid or writable so skip
+//                                     continue;
+//                             }
+//
+//                             if (key.isAcceptable()) {
+//                                     // Accept socket channel for client connection
+//                                     ServerSocketChannel channel = (ServerSocketChannel) key.channel();
+//                                     SocketChannel accept = channel.accept();
+//                                     setupConnection(selector, accept);
+//                             } else if (key.isReadable()) {
+//                                     try {
+//                                             // Read into the buffer from the socket and then write the buffer into the
+//                                             // attached socket.
+//                                             SocketChannel recv = (SocketChannel) key.channel();
+//                                             SocketChannel send = (SocketChannel) key.attachment();
+//                                             recv.read(buffer);
+//                                             buffer.flip();
+//                                             send.write(buffer);
+//                                             buffer.rewind();
+//                                     } catch (IOException e) {
+//                                             e.printStackTrace();
+//
+//                                             // Close sockets
+//                                             if (key.channel() != null)
+//                                                     key.channel().close();
+//                                             if (key.attachment() != null)
+//                                                     ((SocketChannel) key.attachment()).close();
+//                                     }
+//                             } else if (key.isWritable()) {
+//
+//                             }
+//                     }
+//
+//                     // Clear keys for next select
+//                     keys.clear();
+//             }
+//
+//     }
+
+//     public static void mainX(String[] args) throws IOException, InterruptedException {
+//             toRawTerminal();
+//             try {
+//                     boolean client = true;
+//                     if (client) {
+//                             ReadableByteChannel inChannel;
+//                             WritableByteChannel outChannel;
+//                             inChannel = Channels.newChannel(System.in);
+//                             outChannel = Channels.newChannel(System.out);
+//
+//                             SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX);
+//                             channel.connect(ioSocketAddress());
+//
+//                             new Thread(() -> {
+//
+//                                     try {
+//                                             ByteBuffer buffer = ByteBuffer.allocate(1024);
+//                                             while (true) {
+//                                                     if (channel.read(buffer) < 0)
+//                                                             break;
+//                                                     buffer.flip();
+//                                                     outChannel.write(buffer);
+//                                                     buffer.rewind();
+//                                             }
+//                                             System.exit(0);
+//                                     } catch (IOException e) {
+//                                             e.printStackTrace();
+//                                     }
+//                             }, "Read out").start();
+//
+//                             ByteBuffer buffer = ByteBuffer.allocate(1);
+//                             while (channel.isConnected()) {
+//                                     if (inChannel.read(buffer) < 0)
+//                                             break;
+//                                     buffer.flip();
+//                                     channel.write(buffer);
+//                                     buffer.rewind();
+//                             }
+//
+//                     } else {
+//                             ServerSocketChannel serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
+//                             serverChannel.bind(ioSocketAddress());
+//
+//                             SocketChannel channel = serverChannel.accept();
+//
+//                             while (true) {
+//                                     readSocketMessage(channel).ifPresent(message -> System.out.printf("[Client message] %s", message));
+//                                     Thread.sleep(100);
+//                             }
+//                     }
+//             } finally {
+//                     toOriginalTerminal();
+//             }
+//     }
+//
+//     private static Optional<String> readSocketMessage(SocketChannel channel) throws IOException {
+//             ByteBuffer buffer = ByteBuffer.allocate(1024);
+//             int bytesRead = channel.read(buffer);
+//             if (bytesRead < 0)
+//                     return Optional.empty();
+//
+//             byte[] bytes = new byte[bytesRead];
+//             buffer.flip();
+//             buffer.get(bytes);
+//             String message = new String(bytes);
+//             return Optional.of(message);
+//     }
+//
+//     public static void setupConnection(Selector selector, SocketChannel client) throws IOException {
+//             // Connect to the remote server
+//             SocketAddress address = new InetSocketAddress("192.168.1.74", 8000);
+//             SocketChannel remote = SocketChannel.open(address);
+//
+//             // Make sockets non-blocking (should be better performance)
+//             client.configureBlocking(false);
+//             remote.configureBlocking(false);
+//
+//             client.register(selector, SelectionKey.OP_READ, remote);
+//             remote.register(selector, SelectionKey.OP_READ, client);
+//     }
+//
+//     static UnixDomainSocketAddress ioSocketAddress() throws IOException {
+//             String system = "default";
+//             String bundleSn = "org.argeo.slc.jshell";
+//
+//             String xdgRunDir = System.getenv("XDG_RUNTIME_DIR");
+//             Path baseRunDir = Paths.get(xdgRunDir);
+//             Path jshellSocketBase = baseRunDir.resolve("jshell").resolve(system).resolve(bundleSn);
+//
+//             Files.createDirectories(jshellSocketBase);
+//
+//             Path ioSocketPath = jshellSocketBase.resolve("io");
+//
+//             UnixDomainSocketAddress ioSocketAddress = UnixDomainSocketAddress.of(ioSocketPath);
+//             System.out.println(ioSocketAddress);
+//             return ioSocketAddress;
+//     }
+
+}
+
+class SocketPipeSource {
+       private ReadableByteChannel inChannel;
+       private WritableByteChannel outChannel;
+
+       private Thread readOutThread;
+
+       public void forward(SocketChannel channel) throws IOException {
+               readOutThread = new Thread(() -> {
+
+                       try {
+                               ByteBuffer buffer = ByteBuffer.allocate(1024);
+                               while (true) {
+                                       if (channel.read(buffer) < 0)
+                                               break;
+                                       buffer.flip();
+                                       outChannel.write(buffer);
+                                       buffer.rewind();
+                               }
+                               System.exit(0);
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                       }
+               }, "Read out");
+               readOutThread.start();
+
+               // TODO make it smarter than a 1 byte buffer
+               // we should recognize control characters
+               // e.g ^C
+//             int c = System.in.read();
+//             if (c == 0x1B) {
+//                     break;
+//             }
+
+               ByteBuffer buffer = ByteBuffer.allocate(1);
+               while (channel.isConnected()) {
+                       if (inChannel.read(buffer) < 0)
+                               break;
+                       buffer.flip();
+                       channel.write(buffer);
+                       buffer.rewind();
+               }
+
+               // end
+               // TODO make it more robust
+               try {
+                       // TODO add timeout
+                       readOutThread.join();
+               } catch (InterruptedException e) {
+                       e.printStackTrace();
+               }
+       }
+
+       public void setInputStream(InputStream in) {
+               inChannel = Channels.newChannel(in);
+       }
+
+       public void setOutputStream(OutputStream out) {
+               outChannel = Channels.newChannel(out);
+       }
+}
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
new file mode 100644 (file)
index 0000000..fce330d
--- /dev/null
@@ -0,0 +1,126 @@
+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 org.argeo.api.cms.CmsLog;
+import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider;
+
+import jdk.jshell.tool.JavaShellToolBuilder;
+
+class LocalJShellSession implements Runnable {
+       private final static CmsLog log = CmsLog.getLog(LocalJShellSession.class);
+
+       private UUID uuid;
+       private Path sessionDir;
+
+       private String fromBundle = "org.argeo.cms.jshell";
+
+       private Path stdioPath;
+       private Path stderrPath;
+       private Path cmdioPath;
+
+       private Thread replThread;
+
+       LocalJShellSession(Path sessionDir) {
+               this.sessionDir = sessionDir;
+               this.uuid = UUID.fromString(sessionDir.getFileName().toString());
+
+               stdioPath = sessionDir.resolve(JShellClient.STDIO);
+
+               replThread = new Thread(this, "JShell " + sessionDir);
+               replThread.start();
+       }
+
+       public void run() {
+               log.debug(() -> "Started JShell session " + sessionDir);
+               try (SocketPipeMirror std = new SocketPipeMirror()) {
+                       // prepare jshell tool builder
+                       JavaShellToolBuilder builder = JavaShellToolBuilder.builder();
+                       builder.in(std.getInputStream(), null);
+                       builder.interactiveTerminal(true);
+                       builder.out(new PrintStream(std.getOutputStream()));
+
+                       // UnixDomainSocketAddress ioSocketAddress = JSchellClient.ioSocketAddress();
+                       // Files.deleteIfExists(ioSocketAddress.getPath());
+                       UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdioPath);
+
+                       try (ServerSocketChannel stdServerChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
+                               stdServerChannel.bind(stdSocketAddress);
+                               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());
+
+                                       ClassLoader cmsJShellBundleCL = OsgiExecutionControlProvider.class.getClassLoader();
+                                       ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
+                                       try {
+                                               // we need our own class loader so that Java service loader
+                                               // finds our ExecutionControlProvider implementation
+                                               Thread.currentThread().setContextClassLoader(cmsJShellBundleCL);
+                                               //
+                                               // START JSHELL
+                                               //
+                                               int exitCode = builder.start("--execution", "osgi:bundle(" + fromBundle + ")", "--class-path",
+                                                               classpath.toString());
+                                               //
+                                               log.debug("JShell " + sessionDir + " completed with exit code " + exitCode);
+                                       } finally {
+                                               Thread.currentThread().setContextClassLoader(currentContextClassLoader);
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new RuntimeException("JShell " + sessionDir + " failed", e);
+               } finally {
+                       cleanUp();
+               }
+       }
+
+       void cleanUp() {
+               try {
+                       if (Files.exists(stdioPath))
+                               Files.delete(stdioPath);
+                       if (Files.exists(sessionDir))
+                               Files.delete(sessionDir);
+               } catch (IOException e) {
+                       log.error("Cannot clean up JShell " + sessionDir, e);
+               }
+       }
+
+//             void addChild(Path p) throws IOException {
+//                     if (replThread != null)
+//                             throw new IllegalStateException("JShell " + sessionDir + " is already started");
+//
+//                     if (STDIO.equals(p.getFileName().toString())) {
+//                             stdioPath = p;
+//                     } else if (STDERR.equals(p.getFileName().toString())) {
+//                             stderrPath = p;
+//                     } else if (CMDIO.equals(p.getFileName().toString())) {
+//                             cmdioPath = p;
+//                     } else {
+//                             log.warn("Unkown file name " + p.getFileName() + " in " + sessionDir);
+//                     }
+//
+//                     // check that all paths are available
+//                     // if (stdioPath != null && stderrPath != null && cmdioPath != null) {
+//                     if (stdioPath != null) {
+//                             replThread = new Thread(this, "JShell " + sessionDir);
+//                             replThread.start();
+//                     }
+//             }
+
+}
diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java
new file mode 100644 (file)
index 0000000..f763d54
--- /dev/null
@@ -0,0 +1,86 @@
+package org.argeo.cms.jshell;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.Channels;
+import java.nio.channels.Pipe;
+import java.nio.channels.SocketChannel;
+
+class SocketPipeMirror implements Closeable {
+       private final Pipe inPipe;
+       private final Pipe outPipe;
+
+       private final InputStream in;
+       private final OutputStream out;
+
+       private Thread readInThread;
+       private Thread writeOutThread;
+
+       public SocketPipeMirror() throws IOException {
+               inPipe = Pipe.open();
+               outPipe = Pipe.open();
+               in = Channels.newInputStream(inPipe.source());
+               out = Channels.newOutputStream(outPipe.sink());
+       }
+
+       public void open(SocketChannel channel) {
+               readInThread = new Thread(() -> {
+
+                       try {
+                               ByteBuffer buffer = ByteBuffer.allocate(1024);
+                               while (!readInThread.isInterrupted() && channel.isConnected()) {
+                                       if (channel.read(buffer) < 0)
+                                               break;
+                                       buffer.flip();
+                                       inPipe.sink().write(buffer);
+                                       buffer.rewind();
+                               }
+                       } catch (AsynchronousCloseException e) {
+                               // ignore
+                               // TODO make it cleaner
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                       }
+               }, "Read in");
+               readInThread.start();
+
+               writeOutThread = new Thread(() -> {
+
+                       try {
+                               ByteBuffer buffer = ByteBuffer.allocate(1024);
+                               while (!writeOutThread.isInterrupted() && channel.isConnected()) {
+                                       if (outPipe.source().read(buffer) < 0)
+                                               break;
+                                       buffer.flip();
+                                       channel.write(buffer);
+                                       buffer.rewind();
+                               }
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                       }
+               }, "Write out");
+               writeOutThread.start();
+
+       }
+
+       @Override
+       public void close() throws IOException {
+               // TODO make it more robust
+               readInThread.interrupt();
+               writeOutThread.interrupt();
+               in.close();
+               out.close();
+       }
+
+       public InputStream getInputStream() {
+               return in;
+       }
+
+       public OutputStream getOutputStream() {
+               return out;
+       }
+}
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
new file mode 100644 (file)
index 0000000..673233c
--- /dev/null
@@ -0,0 +1,90 @@
+package org.argeo.internal.cms.jshell.osgi;
+
+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.TreeMap;
+import java.util.TreeSet;
+
+import org.argeo.api.cms.CmsLog;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+
+import jdk.jshell.execution.DirectExecutionControl;
+import jdk.jshell.spi.ExecutionControl;
+import jdk.jshell.spi.ExecutionControlProvider;
+import jdk.jshell.spi.ExecutionEnv;
+
+public class OsgiExecutionControlProvider implements ExecutionControlProvider {
+       private final static CmsLog log = CmsLog.getLog(OsgiExecutionControlProvider.class);
+
+       public final static String PROVIDER_NAME = "osgi";
+       public final static String BUNDLE_PARAMETER = "bundle";
+
+       @Override
+       public String name() {
+               return PROVIDER_NAME;
+       }
+
+       @Override
+       public Map<String, String> defaultParameters() {
+               Map<String, String> defaultParameters = new HashMap<>();
+               defaultParameters.put(BUNDLE_PARAMETER, null);
+               return defaultParameters;
+       }
+
+       @Override
+       public ExecutionControl generate(ExecutionEnv env, Map<String, String> 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);
+               Objects.requireNonNull(symbolicName);
+               NavigableMap<Version, Bundle> bundles = new TreeMap<Version, Bundle>();
+               for (Bundle b : bc.getBundles()) {
+                       if (symbolicName.equals(b.getSymbolicName()))
+                               bundles.put(b.getVersion(), b);
+               }
+               Bundle fromBundle = bundles.lastEntry().getValue();
+
+               BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class);
+               ClassLoader fromBundleClassLoader = fromBundleWiring.getClassLoader();
+
+               Set<String> packagesToImport = new TreeSet<>();
+               Set<Bundle> 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<BundleWire> 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;
+       }
+
+}
diff --git a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java
new file mode 100644 (file)
index 0000000..f013a19
--- /dev/null
@@ -0,0 +1,228 @@
+package org.argeo.internal.cms.jshell.osgi;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.security.CodeSource;
+import java.security.SecureClassLoader;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import jdk.jshell.execution.LoaderDelegate;
+import jdk.jshell.spi.ExecutionControl.ClassBytecodes;
+import jdk.jshell.spi.ExecutionControl.ClassInstallException;
+import jdk.jshell.spi.ExecutionControl.EngineTerminationException;
+
+/** A {@link LoaderDelegate} using a parent {@link ClassLoader}. */
+class WrappingLoaderDelegate implements LoaderDelegate {
+       private final WrappingClassloader loader;
+       private final Map<String, Class<?>> klasses = new HashMap<>();
+
+       private static class WrappingClassloader extends SecureClassLoader {
+
+               private final Map<String, ClassFile> classFiles = new HashMap<>();
+
+               public WrappingClassloader(ClassLoader parent) {
+                       super(parent);
+               }
+
+               private class ResourceURLStreamHandler extends URLStreamHandler {
+
+                       private final String name;
+
+                       ResourceURLStreamHandler(String name) {
+                               this.name = name;
+                       }
+
+                       @Override
+                       protected URLConnection openConnection(URL u) throws IOException {
+                               return new URLConnection(u) {
+                                       private InputStream in;
+                                       private Map<String, List<String>> fields;
+                                       private List<String> fieldNames;
+
+                                       @Override
+                                       public void connect() {
+                                               if (connected) {
+                                                       return;
+                                               }
+                                               connected = true;
+                                               ClassFile file = classFiles.get(name);
+                                               in = new ByteArrayInputStream(file.data);
+                                               fields = new LinkedHashMap<>();
+                                               fields.put("content-length", List.of(Integer.toString(file.data.length)));
+                                               Instant instant = new Date(file.timestamp).toInstant();
+                                               ZonedDateTime time = ZonedDateTime.ofInstant(instant, ZoneId.of("GMT"));
+                                               String timeStamp = DateTimeFormatter.RFC_1123_DATE_TIME.format(time);
+                                               fields.put("date", List.of(timeStamp));
+                                               fields.put("last-modified", List.of(timeStamp));
+                                               fieldNames = new ArrayList<>(fields.keySet());
+                                       }
+
+                                       @Override
+                                       public InputStream getInputStream() throws IOException {
+                                               connect();
+                                               return in;
+                                       }
+
+                                       @Override
+                                       public String getHeaderField(String name) {
+                                               connect();
+                                               return fields.getOrDefault(name, List.of()).stream().findFirst().orElse(null);
+                                       }
+
+                                       @Override
+                                       public Map<String, List<String>> getHeaderFields() {
+                                               connect();
+                                               return fields;
+                                       }
+
+                                       @Override
+                                       public String getHeaderFieldKey(int n) {
+                                               return n < fieldNames.size() ? fieldNames.get(n) : null;
+                                       }
+
+                                       @Override
+                                       public String getHeaderField(int n) {
+                                               String name = getHeaderFieldKey(n);
+
+                                               return name != null ? getHeaderField(name) : null;
+                                       }
+
+                               };
+                       }
+               }
+
+               void declare(String name, byte[] bytes) {
+                       classFiles.put(toResourceString(name), new ClassFile(bytes, System.currentTimeMillis()));
+               }
+
+               @Override
+               protected Class<?> findClass(String name) throws ClassNotFoundException {
+                       ClassFile file = classFiles.get(toResourceString(name));
+                       if (file == null) {
+                               return super.findClass(name);
+                       }
+                       return super.defineClass(name, file.data, 0, file.data.length, (CodeSource) null);
+               }
+
+               @Override
+               public URL findResource(String name) {
+                       URL u = doFindResource(name);
+                       return u != null ? u : super.findResource(name);
+               }
+
+               @Override
+               public Enumeration<URL> findResources(String name) throws IOException {
+                       URL u = doFindResource(name);
+                       Enumeration<URL> sup = super.findResources(name);
+
+                       if (u == null) {
+                               return sup;
+                       }
+
+                       List<URL> result = new ArrayList<>();
+
+                       while (sup.hasMoreElements()) {
+                               result.add(sup.nextElement());
+                       }
+
+                       result.add(u);
+
+                       return Collections.enumeration(result);
+               }
+
+               private URL doFindResource(String name) {
+                       if (classFiles.containsKey(name)) {
+                               try {
+                                       return new URL(null, new URI("jshell", null, "/" + name, null).toString(),
+                                                       new ResourceURLStreamHandler(name));
+                               } catch (MalformedURLException | URISyntaxException ex) {
+                                       throw new InternalError(ex);
+                               }
+                       }
+
+                       return null;
+               }
+
+               private String toResourceString(String className) {
+                       return className.replace('.', '/') + ".class";
+               }
+
+               private static class ClassFile {
+                       public final byte[] data;
+                       public final long timestamp;
+
+                       ClassFile(byte[] data, long timestamp) {
+                               this.data = data;
+                               this.timestamp = timestamp;
+                       }
+
+               }
+       }
+
+       public WrappingLoaderDelegate(ClassLoader parentClassLoader) {
+               this.loader = new WrappingClassloader(parentClassLoader);
+
+               Thread.currentThread().setContextClassLoader(loader);
+       }
+
+       @Override
+       public void load(ClassBytecodes[] cbcs) throws ClassInstallException, EngineTerminationException {
+               boolean[] loaded = new boolean[cbcs.length];
+               try {
+                       for (ClassBytecodes cbc : cbcs) {
+                               loader.declare(cbc.name(), cbc.bytecodes());
+                       }
+                       for (int i = 0; i < cbcs.length; ++i) {
+                               ClassBytecodes cbc = cbcs[i];
+                               Class<?> klass = loader.loadClass(cbc.name());
+                               klasses.put(cbc.name(), klass);
+                               loaded[i] = true;
+                               // Get class loaded to the point of, at least, preparation
+                               klass.getDeclaredMethods();
+                       }
+               } catch (Throwable ex) {
+                       throw new ClassInstallException("load: " + ex.getMessage(), loaded);
+               }
+       }
+
+       @Override
+       public void classesRedefined(ClassBytecodes[] cbcs) {
+               for (ClassBytecodes cbc : cbcs) {
+                       loader.declare(cbc.name(), cbc.bytecodes());
+               }
+       }
+
+       @Override
+       public void addToClasspath(String cp) {
+               // ignore
+       }
+
+       @Override
+       public Class<?> findClass(String name) throws ClassNotFoundException {
+               Class<?> klass = klasses.get(name);
+               if (klass == null) {
+                       throw new ClassNotFoundException(name + " not found");
+               } else {
+                       return klass;
+               }
+       }
+
+}
index c1f92deb40b3ea7743098bcb0c8e4bbdacc028af..5dc8570099c608bae621e50b43b4f838e9ea092c 100644 (file)
@@ -373,6 +373,11 @@ public class CmsStateImpl implements CmsState {
                return KernelUtils.getOsgiInstancePath(relativePath);
        }
 
+       @Override
+       public Path getStatePath(String relativePath) {
+               return KernelUtils.getOsgiConfigurationPath(relativePath);
+       }
+
        @Override
        public Long getAvailableSince() {
                return availableSince;
index 6e47873b35557f772ac7d702433652bdb342a3fa..943c06f4ec44a6914e73a41e0530c7e61aed5381 100644 (file)
@@ -65,6 +65,13 @@ class KernelUtils implements KernelConstants {
                return Paths.get(uri);
        }
 
+       public static Path getOsgiConfigurationPath(String relativePath) {
+               URI uri = getOsgiConfigurationUri(relativePath);
+               if (uri == null) // no data area available
+                       return null;
+               return Paths.get(uri);
+       }
+
        public static URI getOsgiInstanceUri(String relativePath) {
                String osgiInstanceBaseUri = getFrameworkProp(OSGI_INSTANCE_AREA);
                if (osgiInstanceBaseUri == null) // no data area available
@@ -75,6 +82,16 @@ class KernelUtils implements KernelConstants {
                return safeUri(osgiInstanceBaseUri + (relativePath != null ? relativePath : ""));
        }
 
+       public static URI getOsgiConfigurationUri(String relativePath) {
+               String osgiInstanceBaseUri = getFrameworkProp(OSGI_CONFIGURATION_AREA);
+               if (osgiInstanceBaseUri == null) // no data area available
+                       return null;
+
+               if (!osgiInstanceBaseUri.endsWith("/"))
+                       osgiInstanceBaseUri = osgiInstanceBaseUri + "/";
+               return safeUri(osgiInstanceBaseUri + (relativePath != null ? relativePath : ""));
+       }
+
        static String getFrameworkProp(String key, String def) {
                String value;
                if (CmsActivator.getBundleContext() != null)
index e83bc24d4d333f47ca0dcdb50f5c1d0129da4766..708ab5340dda65fe79f15f7a75529926d4e02e5d 100644 (file)
@@ -13,6 +13,8 @@ org.argeo.cms.lib.sshd,\
 org.argeo.cms.lib.equinox,\
 org.argeo.cms.lib.jetty,\
 
+#argeo.osgi.start.5=\
+#org.argeo.cms.jshell
 
 # Local
 argeo.node.repo.type=h2