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();
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);
}
}
// 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 {
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);
}
}
}
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) {
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");
-// }
-// }
-
}
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;
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);
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;
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
// 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) {
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;
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);
private Path sessionDir;
private Path socketsDir;
- private Path stdioPath;
- private Path stderrPath;
- private Path cmdioPath;
+ private Path stdPath;
+ private Path ctlPath;
private Thread replThread;
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());
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 {
} 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);
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);
// 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) {
}
}
- void cleanUp() {
+ private void cleanUp() {
try {
if (Files.exists(socketsDir))
FsUtils.delete(socketsDir);
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();
-// }
-// }
-
}
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());
} catch (IOException e) {
e.printStackTrace();
}
- }, "Read in");
+ }, "JShell read " + id);
readInThread.start();
writeOutThread = new Thread(() -> {
} catch (IOException e) {
e.printStackTrace();
}
- }, "Write out");
+ }, "JShell write " + id);
writeOutThread.start();
}
@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() {
// 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;
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);
}
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;
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<>();
}
}
- }
-
- 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();
}
+
}
}