From: Mathieu Baudier Date: Tue, 9 May 2023 07:18:11 +0000 (+0200) Subject: Functionally complete JShell X-Git-Tag: v2.3.16~8 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=9a975983b2f3509a287dfb5751799544ec97ce70;p=lgpl%2Fargeo-commons.git Functionally complete JShell --- diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java index 4df9b819e..e3718ae73 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java @@ -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 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 bundleSns = Files.newDirectoryStream(localBase)) { + try (DirectoryStream 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 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 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 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 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"); -// } -// } - } diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java index f458c5c29..f6846f196 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java @@ -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 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 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 plainArgs = new ArrayList<>(); + Map> 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 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 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 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 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) { diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java index 196ee0468..e77f653b7 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java @@ -1,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(); -// } -// } - } diff --git a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java index f763d54b6..7a2332bed 100644 --- a/org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java +++ b/org.argeo.cms.jshell/src/org/argeo/cms/jshell/SocketPipeMirror.java @@ -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() { diff --git a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java index f785919b2..80acd5551 100644 --- a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java +++ b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java @@ -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() { + + @Override + public InputStream get() { + return ((ExecutionEnv) getClass().getClassLoader()).userIn(); + } + + }.get(); + PrintStream STDOUT = new Supplier() { + + @Override + public PrintStream get() { + return ((ExecutionEnv) getClass().getClassLoader()).userOut(); + } + + }.get(); + PrintStream STDERR = new Supplier() { + + @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); } diff --git a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java index f013a19cb..428d71722 100644 --- a/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java +++ b/org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/WrappingLoaderDelegate.java @@ -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> 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 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 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(); } + } }