Functionally complete JShell
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 9 May 2023 07:18:11 +0000 (09:18 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 9 May 2023 07:18:11 +0000 (09:18 +0200)
org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java
org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java
org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java
org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java
org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java
org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java

index 4df9b819e95591fdb8df809e0d2b7426351d6f6b..e3718ae73119f693d99c78441e194d9bf7199255 100644 (file)
@@ -20,6 +20,7 @@ import org.argeo.cms.util.OS;
 import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider;
 import org.osgi.framework.Bundle;
 
+/** A factory for JShell sessions. */
 public class CmsJShell {
        private final static CmsLog log = CmsLog.getLog(CmsJShell.class);
        static ClassLoader loader = CmsJShell.class.getClassLoader();
@@ -32,41 +33,45 @@ public class CmsJShell {
        private Map<Path, Path> bundleDirs = new HashMap<>();
 
        private Path stateRunDir;
-       private Path localBase;
-       private Path linkedDir;
-
-//     private String defaultBundle = "org.argeo.cms.cli";
+       private Path jshBase;
+       private Path jshLinkedDir;
+       private Path jtermBase;
+       private Path jtermLinkedDir;
 
        public void start() throws Exception {
-
-               // Path localBase = cmsState.getStatePath("org.argeo.cms.jshell/local");
-//             UUID stateUuid = cmsState.getUuid();
-
                // TODO better define application id, make it configurable
                String applicationID = cmsState.getStatePath("").getFileName().toString();
 
                // TODO centralise state run dir
                stateRunDir = OS.getRunDir().resolve(applicationID);
-               localBase = stateRunDir.resolve("jsh");
-               Files.createDirectories(localBase);
 
-               linkedDir = Files.createSymbolicLink(cmsState.getStatePath("jsh"), localBase);
+               jshBase = stateRunDir.resolve(JShellClient.JSH);
+               Files.createDirectories(jshBase);
+               jshLinkedDir = Files.createSymbolicLink(cmsState.getStatePath(JShellClient.JSH), jshBase);
 
-               log.info("Local JShell on " + localBase + ", linked to " + linkedDir);
+               jtermBase = stateRunDir.resolve(JShellClient.JTERM);
+               Files.createDirectories(jtermBase);
+               jtermLinkedDir = Files.createSymbolicLink(cmsState.getStatePath(JShellClient.JTERM), jtermBase);
+
+               log.info("Local JShell on " + jshBase + ", linked to " + jshLinkedDir);
+               log.info("Local JTerml on " + jtermBase + ", linked to " + jtermLinkedDir);
 
                new Thread(() -> {
                        try {
                                WatchService watchService = FileSystems.getDefault().newWatchService();
 
-                               localBase.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
+                               jshBase.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                                                StandardWatchEventKinds.ENTRY_DELETE);
-                               try (DirectoryStream<Path> bundleSns = Files.newDirectoryStream(localBase)) {
+                               try (DirectoryStream<Path> bundleSns = Files.newDirectoryStream(jshBase)) {
                                        for (Path bundleSnDir : bundleSns) {
-                                               addBundleSnDir(bundleSnDir);
-                                               if (bundleDirs.containsKey(bundleSnDir)) {
-                                                       bundleSnDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
-                                                                       StandardWatchEventKinds.ENTRY_DELETE);
-                                               }
+                                               addBundleSnDir(bundleSnDir, watchService);
+                                       }
+                               }
+                               jtermBase.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
+                                               StandardWatchEventKinds.ENTRY_DELETE);
+                               try (DirectoryStream<Path> bundleSns = Files.newDirectoryStream(jtermBase)) {
+                                       for (Path bundleSnDir : bundleSns) {
+                                               addBundleSnDir(bundleSnDir, watchService);
                                        }
                                }
 
@@ -76,14 +81,16 @@ public class CmsJShell {
 //                                             System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context() + ".");
                                                Path parent = (Path) key.watchable();
                                                // sessions
-                                               if (Files.isSameFile(localBase, parent)) {
-                                                       Path bundleSnDir = localBase.resolve((Path) event.context());
+                                               if (Files.isSameFile(jshBase, parent)) {
+                                                       Path bundleSnDir = jshBase.resolve((Path) event.context());
                                                        if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) {
-                                                               addBundleSnDir(bundleSnDir);
-                                                               if (bundleDirs.containsKey(bundleSnDir)) {
-                                                                       bundleSnDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
-                                                                                       StandardWatchEventKinds.ENTRY_DELETE);
-                                                               }
+                                                               addBundleSnDir(bundleSnDir, watchService);
+                                                       } else if (StandardWatchEventKinds.ENTRY_DELETE.equals(event.kind())) {
+                                                       }
+                                               } else if (Files.isSameFile(jtermBase, parent)) {
+                                                       Path bundleSnDir = jtermBase.resolve((Path) event.context());
+                                                       if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) {
+                                                               addBundleSnDir(bundleSnDir, watchService);
                                                        } else if (StandardWatchEventKinds.ENTRY_DELETE.equals(event.kind())) {
                                                        }
                                                } else {
@@ -100,13 +107,21 @@ public class CmsJShell {
                                                                        continue events;
                                                                }
 
+                                                               boolean interactive;
+                                                               if (Files.isSameFile(jshBase, parent.getParent())) {
+                                                                       interactive = false;
+                                                               } else if (Files.isSameFile(jtermBase, parent.getParent())) {
+                                                                       interactive = true;
+                                                               } else {
+                                                                       log.warn("Ignoring " + path + " as we don't know whether it is interactive or not");
+                                                                       continue events;
+                                                               }
                                                                Path bundleIdDir = bundleDirs.get(parent);
-                                                               LocalJShellSession localSession = new LocalJShellSession(path, bundleIdDir);
+                                                               LocalJShellSession localSession = new LocalJShellSession(path, bundleIdDir,
+                                                                               interactive);
                                                                localSessions.put(path, localSession);
                                                        } else if (StandardWatchEventKinds.ENTRY_DELETE.equals(event.kind())) {
-                                                               // TODO clean up session
-                                                               LocalJShellSession localSession = localSessions.remove(path);
-                                                               localSession.cleanUp();
+                                                               localSessions.remove(path);
                                                        }
                                                }
                                        }
@@ -116,16 +131,9 @@ public class CmsJShell {
                                e.printStackTrace();
                        }
                }, "JShell 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)");
-
        }
 
-       private void addBundleSnDir(Path bundleSnDir) throws IOException {
+       private void addBundleSnDir(Path bundleSnDir, WatchService watchService) throws IOException {
                String symbolicName = bundleSnDir.getFileName().toString();
                Bundle fromBundle = OsgiExecutionControlProvider.getBundleFromSn(symbolicName);
                if (fromBundle == null) {
@@ -136,168 +144,19 @@ public class CmsJShell {
                Path bundleIdDir = stateRunDir.resolve(bundleId.toString());
                Files.createDirectories(bundleIdDir);
                bundleDirs.put(bundleSnDir, bundleIdDir);
-       }
 
-//     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);
-//             }
-//     }
+               bundleSnDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
+       }
 
        public void stop() {
                try {
-                       Files.delete(linkedDir);
+                       Files.delete(jshLinkedDir);
                } catch (IOException e) {
-                       log.error("Cannot remove " + linkedDir);
+                       log.error("Cannot remove " + jshLinkedDir);
                }
        }
 
        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");
-//             }
-//     }
-
 }
index f458c5c29d4e8559a3824a57596fbedf6c9861ef..f6846f196c0192204e54acc5e3e0ec895461c9bc 100644 (file)
@@ -1,12 +1,19 @@
 package org.argeo.cms.jshell;
 
-import java.io.ByteArrayOutputStream;
-import java.io.Console;
+import static java.lang.System.Logger.Level.ERROR;
+import static java.lang.System.Logger.Level.TRACE;
+import static java.net.StandardProtocolFamily.UNIX;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.lang.System.Logger;
-import java.net.StandardProtocolFamily;
+import java.lang.management.ManagementFactory;
+import java.net.StandardSocketOptions;
 import java.net.UnixDomainSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
@@ -17,39 +24,83 @@ import java.nio.channels.WritableByteChannel;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 public class JShellClient {
        private final static Logger logger = System.getLogger(JShellClient.class.getName());
 
-       public final static String STDIO = "stdio";
-       public final static String STDERR = "stderr";
-       public final static String CMDIO = "cmdio";
+       public final static String STD = "std";
+       public final static String CTL = "ctl";
 
-       private static String ttyConfig;
+       public final static String JSH = "jsh";
+       public final static String JTERM = "jterm";
 
-       public static void main(String[] args) throws IOException, InterruptedException {
+       private static String sttyExec = "/usr/bin/stty";
+
+       /** Benchmark based on uptime. */
+       private static boolean benchmark = false;
+
+       /**
+        * The real path (following symbolic links) to the directory were to create
+        * sessions.
+        */
+       private Path localBase;
+
+       /** The symbolic name of the bundle from which to run. */
+       private String symbolicName;
+
+       /** The script to run. */
+       private Path script;
+       /** Additional arguments of the script */
+       private List<String> scriptArgs;
+
+       private String ttyConfig;
+       private boolean terminal;
+
+       /** Workaround to be able to test in Eclipse console */
+       private boolean inEclipse = false;
+
+       public JShellClient(Path targetStateDirectory, String symbolicName, Path script, List<String> scriptArgs) {
                try {
-                       Path targetStateDirectory = Paths.get(args[0]);
-                       String symbolicName = args[1];
-                       Path localBase = targetStateDirectory.resolve("jsh");
+                       this.terminal = System.console() != null && script == null;
+                       if (inEclipse && script == null)
+                               terminal = true;
+                       if (terminal) {
+                               localBase = targetStateDirectory.resolve(JTERM);
+                       } else {
+                               localBase = targetStateDirectory.resolve(JSH);
+                       }
                        if (Files.isSymbolicLink(localBase)) {
                                localBase = localBase.toRealPath();
                        }
+                       this.symbolicName = symbolicName;
+                       this.script = script;
+                       this.scriptArgs = scriptArgs == null ? new ArrayList<>() : scriptArgs;
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot initialise client", e);
+               }
+       }
 
-                       Console console = System.console();
-                       if (console != null) {
+       public void run() {
+               try {
+                       if (terminal)
                                toRawTerminal();
-                       }
+                       SocketPipeSource std = new SocketPipeSource(STD, script != null);
+                       std.setInputStream(System.in);
+                       std.setOutputStream(System.out);
 
-                       SocketPipeSource stdio = new SocketPipeSource();
-                       stdio.setInputStream(System.in);
-                       stdio.setOutputStream(System.out);
+                       SocketPipeSource ctl = new SocketPipeSource(CTL, false);
+                       ctl.setOutputStream(System.err);
 
                        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
-                               // logger.log(Logger.Level.INFO, "Shutting down...");
                                System.out.println("\nShutting down...");
-                               stdio.shutdown();
+                               toOriginalTerminal();
+                               std.shutdown();
+                               ctl.shutdown();
                        }, "Shut down JShell client"));
 
                        Path bundleSnDir = localBase.resolve(symbolicName);
@@ -57,282 +108,235 @@ public class JShellClient {
                                Files.createDirectory(bundleSnDir);
                        UUID uuid = UUID.randomUUID();
                        Path sessionDir = bundleSnDir.resolve(uuid.toString());
+
+                       // creating the directory will trigger opening of the session on server side
                        Files.createDirectory(sessionDir);
-                       Path stdioPath = sessionDir.resolve(JShellClient.STDIO);
 
-                       while (!(Files.exists(stdioPath))) {
+                       Path stdPath = sessionDir.resolve(JShellClient.STD);
+                       Path ctlPath = sessionDir.resolve(JShellClient.CTL);
+
+                       while (!(Files.exists(stdPath) && Files.exists(ctlPath))) {
                                // TODO timeout
-                               Thread.sleep(50);
-
-//                             // wait for sockets to be available
-//                             WatchService watchService = FileSystems.getDefault().newWatchService();
-//                             sessionDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
-//                             WatchKey key;
-//                             watch: while ((key = watchService.take()) != null) {
-//                                     for (WatchEvent<?> event : key.pollEvents()) {
-//                                             Path path = sessionDir.resolve((Path) event.context());
-//                                             if (Files.isSameFile(stdioPath, path)) {
-//                                                     break watch;
-//                                             }
-//                                     }
-//                             }
-//                             watchService.close();
+                               try {
+                                       Thread.sleep(1);
+                               } catch (InterruptedException e) {
+                                       // silent
+                               }
                        }
 
-                       UnixDomainSocketAddress stdioSocketAddress = UnixDomainSocketAddress.of(stdioPath.toRealPath());
+                       UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdPath.toRealPath());
+                       UnixDomainSocketAddress ctlSocketAddress = UnixDomainSocketAddress.of(ctlPath.toRealPath());
 
-                       try (SocketChannel stdioChannel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
-                               stdioChannel.connect(stdioSocketAddress);
-                               stdio.forward(stdioChannel);
-                       }
+                       try (SocketChannel stdChannel = SocketChannel.open(UNIX);
+                                       SocketChannel ctlChannel = SocketChannel.open(UNIX);) {
+                               ctlChannel.connect(ctlSocketAddress);
+                               ctl.process(ctlChannel);
+                               if (script != null) {
+                                       new ScriptThread(ctlChannel).start();
+                               }
+                               stdChannel.connect(stdSocketAddress);
+                               std.process(stdChannel);
 
-               } catch (IOException | InterruptedException e) {
-                       // TODO Auto-generated catch block
+                               while (!std.isCompleted() && !ctl.isCompleted()) {
+                                       // isCompleted() will block
+                               }
+                       }
+                       if (benchmark)
+                               System.err.println(ManagementFactory.getRuntimeMXBean().getUptime());
+                       std.shutdown();
+                       ctl.shutdown();
+               } catch (IOException e) {
                        e.printStackTrace();
                } finally {
-                       if (ttyConfig != null)
-                               try {
-                                       stty(ttyConfig.trim());
-                               } catch (Exception e) {
-                                       System.err.println("Exception restoring tty config");
-                               }
+                       toOriginalTerminal();
                }
 
        }
 
-       private static void toRawTerminal() throws IOException, InterruptedException {
+       public static void main(String[] args) throws IOException, InterruptedException {
+               if (benchmark)
+                       System.err.println(ManagementFactory.getRuntimeMXBean().getUptime());
+               List<String> plainArgs = new ArrayList<>();
+               Map<String, List<String>> options = new HashMap<>();
+               String currentOption = null;
+               for (int i = 0; i < args.length; i++) {
+                       if (args[i].startsWith("-")) {
+                               currentOption = args[i];
+                               if (!options.containsKey(currentOption))
+                                       options.put(currentOption, new ArrayList<>());
+                               i++;
+                               options.get(currentOption).add(args[i]);
+                       } else {
+                               plainArgs.add(args[i]);
+                       }
+               }
 
-               ttyConfig = stty("-g");
+               Path targetStateDirectory = Paths.get(options.get("-d").get(0));
+               String symbolicName = options.get("-b").get(0);
+
+               Path script = plainArgs.isEmpty() ? null : Paths.get(plainArgs.get(0));
+               List<String> scriptArgs = new ArrayList<>();
+               for (int i = 1; i < plainArgs.size(); i++)
+                       scriptArgs.add(plainArgs.get(i));
+
+               JShellClient client = new JShellClient(targetStateDirectory, symbolicName, script, scriptArgs);
+               client.run();
+       }
 
+       /*
+        * TERMINAL
+        */
+       /** Set the terminal to raw mode. */
+       protected synchronized void toRawTerminal() {
+               boolean isWindows = File.separatorChar == '\\';
+               if (isWindows)
+                       return;
+               if (inEclipse)
+                       return;
+               // save current configuration
+               ttyConfig = stty("-g");
+               if (ttyConfig == null)
+                       return;
+               ttyConfig.trim();
                // 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");
-                       }
+       /** Restore original terminal configuration. */
+       protected synchronized void toOriginalTerminal() {
+               if (ttyConfig == null)
+                       return;
+               try {
+                       stty(ttyConfig);
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+               ttyConfig = null;
        }
 
        /**
         * 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";
+       protected String stty(String args) {
+               List<String> cmd = new ArrayList<>();
+               cmd.add("/bin/sh");
+               cmd.add("-c");
+               cmd.add(sttyExec + " " + 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();
+               logger.log(TRACE, () -> cmd.toString());
 
-               while ((c = in.read()) != -1) {
-                       bout.write(c);
+               try {
+                       ProcessBuilder pb = new ProcessBuilder(cmd);
+                       Process p = pb.start();
+                       String firstLine = new BufferedReader(new InputStreamReader(p.getInputStream())).readLine();
+                       p.waitFor();
+                       logger.log(TRACE, () -> firstLine);
+                       return firstLine;
+               } catch (IOException | InterruptedException e) {
+                       e.printStackTrace();
+                       return null;
                }
+       }
 
-               in = p.getErrorStream();
+       /*
+        * SCRIPT
+        */
+       private class ScriptThread extends Thread {
+               private SocketChannel channel;
 
-               while ((c = in.read()) != -1) {
-                       bout.write(c);
+               public ScriptThread(SocketChannel channel) {
+                       super("JShell script writer");
+                       this.channel = channel;
                }
 
-               p.waitFor();
+               @Override
+               public void run() {
+                       try {
+                               if (benchmark)
+                                       System.err.println(ManagementFactory.getRuntimeMXBean().getUptime());
+                               StringBuilder sb = new StringBuilder();
+//                             sb.append("/set feedback silent\n");
+                               if (!scriptArgs.isEmpty()) {
+                                       // additional arguments as $1, $2, etc.
+                                       for (String arg : scriptArgs)
+                                               sb.append('\"').append(arg).append('\"').append(";\n");
+                               }
+                               if (sb.length() > 0)
+                                       writeLine(sb);
 
-               String result = new String(bout.toByteArray());
-               return result;
-       }
+                               ByteBuffer buffer = ByteBuffer.allocate(1024);
+                               try (BufferedReader reader = Files.newBufferedReader(script)) {
+                                       String line;
+                                       lines: while ((line = reader.readLine()) != null) {
+                                               if (line.startsWith("#"))
+                                                       continue lines;
+                                               buffer.put((line + "\n").getBytes(UTF_8));
+                                               buffer.flip();
+                                               channel.write(buffer);
+                                               buffer.rewind();
+                                       }
+                               }
 
-//     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);
+//                             ByteBuffer buffer = ByteBuffer.allocate(1024);
+//                             try (SeekableByteChannel scriptChannel = Files.newByteChannel(script, StandardOpenOption.READ)) {
+//                                     while (channel.isConnected()) {
+//                                             if (scriptChannel.read(buffer) < 0)
+//                                                     break;
 //                                             buffer.flip();
-//                                             send.write(buffer);
+//                                             channel.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;
-//     }
 
+                               // exit
+                               if (channel.isConnected())
+                                       writeLine("/exit");
+                       } catch (IOException e) {
+                               logger.log(ERROR, "Cannot execute " + script, e);
+                       }
+               }
+
+               private void writeLine(Object obj) throws IOException {
+                       channel.write(ByteBuffer.wrap((obj + "\n").getBytes(UTF_8)));
+               }
+       }
 }
 
+/** Pipe streams to a channel. */
 class SocketPipeSource {
        private ReadableByteChannel inChannel;
        private WritableByteChannel outChannel;
 
-       private Thread readOutThread;
+       private Thread readThread;
        private Thread forwardThread;
 
-       public void forward(SocketChannel channel) throws IOException {
-               forwardThread = Thread.currentThread();
-               readOutThread = new Thread(() -> {
+       private int inBufferSize = 1;
+       private int outBufferSize = 1024;
+
+       private final String id;
+       private final boolean batch;
+
+       private boolean completed = false;
+
+       public SocketPipeSource(String id, boolean batch) {
+               this.id = id;
+               this.batch = batch;
+       }
+
+       public void process(SocketChannel channel) throws IOException {
+               if (batch) {
+                       Integer socketRcvBuf = channel.getOption(StandardSocketOptions.SO_RCVBUF);
+                       inBufferSize = socketRcvBuf;
+                       outBufferSize = socketRcvBuf;
+               }
+
+               readThread = new Thread(() -> {
 
                        try {
-                               ByteBuffer buffer = ByteBuffer.allocate(1024);
+                               ByteBuffer buffer = ByteBuffer.allocate(outBufferSize);
                                while (true) {
                                        if (channel.read(buffer) < 0)
                                                break;
@@ -340,14 +344,14 @@ class SocketPipeSource {
                                        outChannel.write(buffer);
                                        buffer.rewind();
                                }
-                               System.exit(0);
                        } catch (ClosedByInterruptException e) {
                                // silent
                        } catch (IOException e) {
                                e.printStackTrace();
                        }
-               }, "Read out");
-               readOutThread.start();
+                       markCompleted();
+               }, "JShell read " + id);
+               readThread.start();
 
                // TODO make it smarter than a 1 byte buffer
                // we should recognize control characters
@@ -357,43 +361,71 @@ class SocketPipeSource {
 //                     break;
 //             }
 
-               ByteBuffer buffer = ByteBuffer.allocate(1);
-               while (channel.isConnected()) {
-                       if (inChannel.read(buffer) < 0)
-                               break;
+               if (inChannel != null) {
+                       forwardThread = new Thread(() -> {
+                               try {
+                                       ByteBuffer buffer = ByteBuffer.allocate(inBufferSize);
+                                       while (channel.isConnected()) {
+                                               if (inChannel.read(buffer) < 0)
+                                                       break;
 //                     int b = (int) buffer.get(0);
 //                     if (b == 0x1B) {
 //                             System.out.println("Ctrl+C");
 //                     }
 
-                       buffer.flip();
-                       channel.write(buffer);
-                       buffer.rewind();
-               }
+                                               buffer.flip();
+                                               channel.write(buffer);
+                                               buffer.rewind();
+                                       }
+                               } catch (IOException e) {
+                                       e.printStackTrace();
+                               }
+                       }, "JShell write " + id);
+                       forwardThread.setDaemon(true);
+                       forwardThread.start();
+                       // end
+                       // TODO make it more robust
+                       // we want to be asynchronous when read only
+//                     try {
+//                             // TODO add timeout
+//                             readThread.join();
+//                     } catch (InterruptedException e) {
+//                             e.printStackTrace();
+//                     }
 
-               // end
-               // TODO make it more robust
-               try {
-                       // TODO add timeout
-                       readOutThread.join();
-               } catch (InterruptedException e) {
-                       e.printStackTrace();
                }
        }
 
+       public synchronized boolean isCompleted() {
+               if (!completed)
+                       try {
+                               wait();
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+               return completed;
+       }
+
+       protected synchronized void markCompleted() {
+               completed = true;
+               notifyAll();
+       }
+
        public void shutdown() {
-               try {
-                       inChannel.close();
-               } catch (IOException e) {
-                       e.printStackTrace();
-               }
+               if (inChannel != null)
+                       try {
+                               inChannel.close();
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                       }
                try {
                        outChannel.close();
                } catch (IOException e) {
                        e.printStackTrace();
                }
-               forwardThread.interrupt();
-               readOutThread.interrupt();
+               if (inChannel != null)
+                       forwardThread.interrupt();
+               readThread.interrupt();
        }
 
        public void setInputStream(InputStream in) {
index 196ee04689c9154e613e303b174c47c2624dda40..e77f653b7ce63a394a55546957461b498baa8119 100644 (file)
@@ -1,8 +1,10 @@
 package org.argeo.cms.jshell;
 
+import static java.net.StandardProtocolFamily.UNIX;
+
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.PrintStream;
-import java.net.StandardProtocolFamily;
 import java.net.UnixDomainSocketAddress;
 import java.nio.channels.ServerSocketChannel;
 import java.nio.channels.SocketChannel;
@@ -22,6 +24,7 @@ import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider;
 
 import jdk.jshell.tool.JavaShellToolBuilder;
 
+/** A JShell session based on local UNIX sockets. */
 class LocalJShellSession implements Runnable {
        private final static CmsLog log = CmsLog.getLog(LocalJShellSession.class);
 
@@ -29,9 +32,8 @@ class LocalJShellSession implements Runnable {
        private Path sessionDir;
        private Path socketsDir;
 
-       private Path stdioPath;
-       private Path stderrPath;
-       private Path cmdioPath;
+       private Path stdPath;
+       private Path ctlPath;
 
        private Thread replThread;
 
@@ -39,7 +41,10 @@ class LocalJShellSession implements Runnable {
 
        private Long bundleId;
 
-       LocalJShellSession(Path sessionDir, Path bundleIdDir) {
+       private final boolean interactive;
+
+       LocalJShellSession(Path sessionDir, Path bundleIdDir, boolean interactive) {
+               this.interactive = interactive;
                try {
                        this.sessionDir = sessionDir;
                        this.uuid = UUID.fromString(sessionDir.getFileName().toString());
@@ -47,8 +52,11 @@ class LocalJShellSession implements Runnable {
                        socketsDir = bundleIdDir.resolve(uuid.toString());
                        Files.createDirectories(socketsDir);
 
-                       stdioPath = socketsDir.resolve(JShellClient.STDIO);
-                       Files.createSymbolicLink(sessionDir.resolve(JShellClient.STDIO), stdioPath);
+                       stdPath = socketsDir.resolve(JShellClient.STD);
+                       Files.createSymbolicLink(sessionDir.resolve(JShellClient.STD), stdPath);
+
+                       ctlPath = socketsDir.resolve(JShellClient.CTL);
+                       Files.createSymbolicLink(sessionDir.resolve(JShellClient.CTL), ctlPath);
 
                        // TODO proper login
                        try {
@@ -62,6 +70,7 @@ class LocalJShellSession implements Runnable {
                } catch (IOException e) {
                        log.error("Cannot initiate local session " + uuid, e);
                        cleanUp();
+                       return;
                }
                replThread = new Thread(() -> CurrentSubject.callAs(loginContext.getSubject(), Executors.callable(this)),
                                "JShell " + sessionDir);
@@ -71,29 +80,44 @@ class LocalJShellSession implements Runnable {
        public void run() {
 
                log.debug(() -> "Started JShell session " + sessionDir);
-               try (SocketPipeMirror std = new SocketPipeMirror()) {
+               try (SocketPipeMirror std = new SocketPipeMirror(JShellClient.STD + " " + uuid); //
+                               SocketPipeMirror ctl = new SocketPipeMirror(JShellClient.CTL + " " + uuid);) {
                        // prepare jshell tool builder
+                       String feedbackMode;
                        JavaShellToolBuilder builder = JavaShellToolBuilder.builder();
-                       builder.in(std.getInputStream(), null);
-                       builder.interactiveTerminal(true);
-                       builder.out(new PrintStream(std.getOutputStream()));
+                       if (interactive) {
+                               builder.in(std.getInputStream(), null);
+                               builder.out(new PrintStream(std.getOutputStream()));
+                               builder.err(new PrintStream(ctl.getOutputStream()));
+                               builder.interactiveTerminal(true);
+                               feedbackMode = "concise";
+                       } else {
+                               builder.in(ctl.getInputStream(), std.getInputStream());
+                               PrintStream cmdOut = new PrintStream(ctl.getOutputStream());
+                               PrintStream discard = new PrintStream(OutputStream.nullOutputStream());
+                               builder.out(cmdOut, discard, new PrintStream(std.getOutputStream()));
+                               builder.err(cmdOut);
+                               builder.promptCapture(true);
+                               feedbackMode = "silent";
+                       }
 
-                       // UnixDomainSocketAddress ioSocketAddress = JSchellClient.ioSocketAddress();
-                       // Files.deleteIfExists(ioSocketAddress.getPath());
-                       UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdioPath);
+                       UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdPath);
+                       UnixDomainSocketAddress ctlSocketAddress = UnixDomainSocketAddress.of(ctlPath);
 
-                       try (ServerSocketChannel stdServerChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
+                       try (ServerSocketChannel stdServerChannel = ServerSocketChannel.open(UNIX);
+                                       ServerSocketChannel ctlServerChannel = ServerSocketChannel.open(UNIX);) {
                                stdServerChannel.bind(stdSocketAddress);
-                               try (SocketChannel channel = stdServerChannel.accept()) {
-                                       std.open(channel);
-
-//                                     StringJoiner classpath = new StringJoiner(File.pathSeparator);
-//                                     String frameworkLocation = System.getProperty("osgi.framework");
-//                                     classpath.add(Paths.get(URI.create(frameworkLocation)).toAbsolutePath().toString());
+                               ctlServerChannel.bind(ctlSocketAddress);
+                               try (SocketChannel stdChannel = stdServerChannel.accept();
+                                               SocketChannel ctlChannel = ctlServerChannel.accept();) {
+                                       std.open(stdChannel);
+                                       ctl.open(ctlChannel);
 
                                        ClassLoader cmsJShellBundleCL = OsgiExecutionControlProvider.class.getClassLoader();
                                        ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
                                        try {
+                                               String classpath = OsgiExecutionControlProvider.getBundleClasspath(bundleId);
+                                               Path bundleStartupScript = OsgiExecutionControlProvider.getBundleStartupScript(bundleId);
                                                // we need our own class loader so that Java service loader
                                                // finds our ExecutionControlProvider implementation
                                                Thread.currentThread().setContextClassLoader(cmsJShellBundleCL);
@@ -101,13 +125,13 @@ class LocalJShellSession implements Runnable {
                                                // START JSHELL
                                                //
                                                int exitCode = builder.start("--execution", "osgi:bundle(" + bundleId + ")", "--class-path",
-                                                               OsgiExecutionControlProvider.getBundleClasspath(bundleId), "--startup",
-                                                               OsgiExecutionControlProvider.getBundleStartupScript(bundleId).toString());
+                                                               classpath, "--startup", bundleStartupScript.toString(), "--feedback", feedbackMode);
                                                //
                                                log.debug("JShell " + sessionDir + " completed with exit code " + exitCode);
                                        } finally {
                                                Thread.currentThread().setContextClassLoader(currentContextClassLoader);
                                        }
+                               } finally {
                                }
                        }
                } catch (Exception e) {
@@ -117,7 +141,7 @@ class LocalJShellSession implements Runnable {
                }
        }
 
-       void cleanUp() {
+       private void cleanUp() {
                try {
                        if (Files.exists(socketsDir))
                                FsUtils.delete(socketsDir);
@@ -127,33 +151,11 @@ class LocalJShellSession implements Runnable {
                        log.error("Cannot clean up JShell " + sessionDir, e);
                }
 
-               try {
-                       loginContext.logout();
-               } catch (LoginException e) {
-                       log.error("Cannot log out JShell " + sessionDir, e);
-               }
+               if (loginContext != null)
+                       try {
+                               loginContext.logout();
+                       } catch (LoginException e) {
+                               log.error("Cannot log out 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();
-//                     }
-//             }
-
 }
index f763d54b6067cb4cfbf4015fc208911fa56cc461..7a2332bed1a38c6dbb202a349b5888493fe3db59 100644 (file)
@@ -20,7 +20,10 @@ class SocketPipeMirror implements Closeable {
        private Thread readInThread;
        private Thread writeOutThread;
 
-       public SocketPipeMirror() throws IOException {
+       private final String id;
+
+       public SocketPipeMirror(String id) throws IOException {
+               this.id = id;
                inPipe = Pipe.open();
                outPipe = Pipe.open();
                in = Channels.newInputStream(inPipe.source());
@@ -45,7 +48,7 @@ class SocketPipeMirror implements Closeable {
                        } catch (IOException e) {
                                e.printStackTrace();
                        }
-               }, "Read in");
+               }, "JShell read " + id);
                readInThread.start();
 
                writeOutThread = new Thread(() -> {
@@ -62,7 +65,7 @@ class SocketPipeMirror implements Closeable {
                        } catch (IOException e) {
                                e.printStackTrace();
                        }
-               }, "Write out");
+               }, "JShell write " + id);
                writeOutThread.start();
 
        }
@@ -70,10 +73,20 @@ class SocketPipeMirror implements Closeable {
        @Override
        public void close() throws IOException {
                // TODO make it more robust
-               readInThread.interrupt();
-               writeOutThread.interrupt();
+//             readInThread.interrupt();
+//             writeOutThread.interrupt();
                in.close();
                out.close();
+               try {
+                       readInThread.join();
+               } catch (InterruptedException e) {
+                       // silent
+               }
+               try {
+                       writeOutThread.join();
+               } catch (InterruptedException e) {
+                       // silent
+               }
        }
 
        public InputStream getInputStream() {
index f785919b2455c6d0ff7c6d00173b24603a750856..80acd55517e9af81e2b7a990584603458877397c 100644 (file)
@@ -67,9 +67,9 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
 
                // use the bundle classloade as context classloader
                Thread.currentThread().setContextClassLoader(fromBundleClassLoader);
-               
+
                ExecutionControl executionControl = new DirectExecutionControl(
-                               new WrappingLoaderDelegate(fromBundleClassLoader));
+                               new WrappingLoaderDelegate(env, fromBundleClassLoader));
                log.debug("JShell from " + fromBundle.getSymbolicName() + "_" + fromBundle.getVersion() + " ["
                                + fromBundle.getBundleId() + "]");
                return executionControl;
@@ -119,6 +119,36 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
                        for (String p : packagesToImport) {
                                writer.write("import " + p + ".*;\n");
                        }
+
+                       String std = """
+                                       import jdk.jshell.spi.ExecutionEnv;
+
+                                       InputStream STDIN = new Supplier<InputStream>() {
+
+                                                       @Override
+                                                       public InputStream get() {
+                                                               return ((ExecutionEnv) getClass().getClassLoader()).userIn();
+                                                       }
+
+                                               }.get();
+                                       PrintStream STDOUT = new Supplier<PrintStream>() {
+
+                                                       @Override
+                                                       public PrintStream get() {
+                                                               return ((ExecutionEnv) getClass().getClassLoader()).userOut();
+                                                       }
+
+                                               }.get();
+                                       PrintStream STDERR = new Supplier<PrintStream>() {
+
+                                                       @Override
+                                                       public PrintStream get() {
+                                                               return ((ExecutionEnv) getClass().getClassLoader()).userErr();
+                                                       }
+
+                                               }.get();
+                                                                               """;
+                       writer.write(std);
                } catch (IOException e) {
                        throw new RuntimeException("Cannot writer bundle startup script to " + bundleStartupScript, e);
                }
index f013a19cb2837fb1f8e0c13624405892761f99bf..428d717220efd7903600760e28eed56c2557bac2 100644 (file)
@@ -3,6 +3,7 @@ package org.argeo.internal.cms.jshell.osgi;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PrintStream;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -28,13 +29,65 @@ import jdk.jshell.execution.LoaderDelegate;
 import jdk.jshell.spi.ExecutionControl.ClassBytecodes;
 import jdk.jshell.spi.ExecutionControl.ClassInstallException;
 import jdk.jshell.spi.ExecutionControl.EngineTerminationException;
+import jdk.jshell.spi.ExecutionEnv;
 
 /** 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 ExecutionEnv env;
+
+       public WrappingLoaderDelegate(ExecutionEnv env, ClassLoader parentClassLoader) {
+               this.env = env;
+               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;
+               }
+       }
+
+       private class WrappingClassloader extends SecureClassLoader implements ExecutionEnv {
 
                private final Map<String, ClassFile> classFiles = new HashMap<>();
 
@@ -175,54 +228,32 @@ class WrappingLoaderDelegate implements LoaderDelegate {
                        }
 
                }
-       }
-
-       public WrappingLoaderDelegate(ClassLoader parentClassLoader) {
-               this.loader = new WrappingClassloader(parentClassLoader);
 
-               Thread.currentThread().setContextClassLoader(loader);
-       }
+               @Override
+               public InputStream userIn() {
+                       return env.userIn();
+               }
 
-       @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 PrintStream userOut() {
+                       return env.userOut();
                }
-       }
 
-       @Override
-       public void classesRedefined(ClassBytecodes[] cbcs) {
-               for (ClassBytecodes cbc : cbcs) {
-                       loader.declare(cbc.name(), cbc.bytecodes());
+               @Override
+               public PrintStream userErr() {
+                       return env.userErr();
                }
-       }
 
-       @Override
-       public void addToClasspath(String cp) {
-               // ignore
-       }
+               @Override
+               public List<String> extraRemoteVMOptions() {
+                       return env.extraRemoteVMOptions();
+               }
 
-       @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;
+               @Override
+               public void closeDown() {
+                       // env.closeDown();
                }
+
        }
 
 }