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 \
List<String> getDeployProperties(String property);
Path getDataPath(String relativePath);
+
+ Path getStatePath(String relativePath);
}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+Import-Package: \
+org.osgi.framework.namespace, \
+*
+
+Service-Component:\
+OSGI-INF/cmsJShell.xml
--- /dev/null
+bin.includes = META-INF/,\
+ .,\
+ OSGI-INF/cmsJShell.xml
+source.. = src/
+output.. = bin/
--- /dev/null
+org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider
\ No newline at end of file
--- /dev/null
+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");
+// }
+// }
+
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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();
+// }
+// }
+
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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;
+ }
+ }
+
+}
return KernelUtils.getOsgiInstancePath(relativePath);
}
+ @Override
+ public Path getStatePath(String relativePath) {
+ return KernelUtils.getOsgiConfigurationPath(relativePath);
+ }
+
@Override
public Long getAvailableSince() {
return availableSince;
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
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)
org.argeo.cms.lib.equinox,\
org.argeo.cms.lib.jetty,\
+#argeo.osgi.start.5=\
+#org.argeo.cms.jshell
# Local
argeo.node.repo.type=h2