package org.argeo.cms.jshell; import java.io.ByteArrayOutputStream; import java.io.Console; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.UUID; public class JShellClient { public final static String STDIO = "stdio"; public final static String STDERR = "stderr"; public final static String CMDIO = "cmdio"; private static String ttyConfig; public static void main(String[] args) throws IOException, InterruptedException { try { Path targetStateDirectory = Paths.get(args[0]); Path localBase = targetStateDirectory.resolve("jsh"); if (Files.isSymbolicLink(localBase)) { localBase = localBase.toRealPath(); } Console console = System.console(); if (console != null) { toRawTerminal(); } SocketPipeSource std = new SocketPipeSource(); std.setInputStream(System.in); std.setOutputStream(System.out); UUID uuid = UUID.randomUUID(); Path sessionDir = localBase.resolve(uuid.toString()); Files.createDirectory(sessionDir); Path stdPath = sessionDir.resolve(JShellClient.STDIO); // wait for sockets to be available WatchService watchService = FileSystems.getDefault().newWatchService(); sessionDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE); WatchKey key; watch: while ((key = watchService.take()) != null) { for (WatchEvent event : key.pollEvents()) { Path path = sessionDir.resolve((Path) event.context()); if (Files.isSameFile(stdPath, path)) { break watch; } } } watchService.close(); UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdPath); SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX); channel.connect(stdSocketAddress); std.forward(channel); } catch (IOException | InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { stty(ttyConfig.trim()); } catch (Exception e) { System.err.println("Exception restoring tty config"); } } } private static void toRawTerminal() throws IOException, InterruptedException { ttyConfig = stty("-g"); // set the console to be character-buffered instead of line-buffered stty("-icanon min 1"); // disable character echoing stty("-echo"); Runtime.getRuntime().addShutdownHook(new Thread(() -> toOriginalTerminal(), "Reset terminal")); } private static void toOriginalTerminal() { if (ttyConfig != null) try { stty(ttyConfig.trim()); } catch (Exception e) { System.err.println("Exception restoring tty config"); } } /** * Execute the stty command with the specified arguments against the current * active terminal. */ private static String stty(final String args) throws IOException, InterruptedException { String cmd = "stty " + args + " < /dev/tty"; return exec(new String[] { "sh", "-c", cmd }); } /** * Execute the specified command and return the output (both stdout and stderr). */ private static String exec(final String[] cmd) throws IOException, InterruptedException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); Process p = Runtime.getRuntime().exec(cmd); int c; InputStream in = p.getInputStream(); while ((c = in.read()) != -1) { bout.write(c); } in = p.getErrorStream(); while ((c = in.read()) != -1) { bout.write(c); } p.waitFor(); String result = new String(bout.toByteArray()); return result; } // void pipe() throws IOException { // // Set up Server Socket and bind to the port 8000 // ServerSocketChannel server = ServerSocketChannel.open(); // SocketAddress endpoint = new InetSocketAddress(8000); // server.socket().bind(endpoint); // // server.configureBlocking(false); // // // Set up selector so we can run with a single thread but multiplex between 2 // // channels // Selector selector = Selector.open(); // server.register(selector, SelectionKey.OP_ACCEPT); // // ByteBuffer buffer = ByteBuffer.allocate(1024); // // while (true) { // // block until data comes in // selector.select(); // // Set keys = selector.selectedKeys(); // // for (SelectionKey key : keys) { // if (!key.isValid()) { // // not valid or writable so skip // continue; // } // // if (key.isAcceptable()) { // // Accept socket channel for client connection // ServerSocketChannel channel = (ServerSocketChannel) key.channel(); // SocketChannel accept = channel.accept(); // setupConnection(selector, accept); // } else if (key.isReadable()) { // try { // // Read into the buffer from the socket and then write the buffer into the // // attached socket. // SocketChannel recv = (SocketChannel) key.channel(); // SocketChannel send = (SocketChannel) key.attachment(); // recv.read(buffer); // buffer.flip(); // send.write(buffer); // buffer.rewind(); // } catch (IOException e) { // e.printStackTrace(); // // // Close sockets // if (key.channel() != null) // key.channel().close(); // if (key.attachment() != null) // ((SocketChannel) key.attachment()).close(); // } // } else if (key.isWritable()) { // // } // } // // // Clear keys for next select // keys.clear(); // } // // } // public static void mainX(String[] args) throws IOException, InterruptedException { // toRawTerminal(); // try { // boolean client = true; // if (client) { // ReadableByteChannel inChannel; // WritableByteChannel outChannel; // inChannel = Channels.newChannel(System.in); // outChannel = Channels.newChannel(System.out); // // SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX); // channel.connect(ioSocketAddress()); // // new Thread(() -> { // // try { // ByteBuffer buffer = ByteBuffer.allocate(1024); // while (true) { // if (channel.read(buffer) < 0) // break; // buffer.flip(); // outChannel.write(buffer); // buffer.rewind(); // } // System.exit(0); // } catch (IOException e) { // e.printStackTrace(); // } // }, "Read out").start(); // // ByteBuffer buffer = ByteBuffer.allocate(1); // while (channel.isConnected()) { // if (inChannel.read(buffer) < 0) // break; // buffer.flip(); // channel.write(buffer); // buffer.rewind(); // } // // } else { // ServerSocketChannel serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); // serverChannel.bind(ioSocketAddress()); // // SocketChannel channel = serverChannel.accept(); // // while (true) { // readSocketMessage(channel).ifPresent(message -> System.out.printf("[Client message] %s", message)); // Thread.sleep(100); // } // } // } finally { // toOriginalTerminal(); // } // } // // private static Optional readSocketMessage(SocketChannel channel) throws IOException { // ByteBuffer buffer = ByteBuffer.allocate(1024); // int bytesRead = channel.read(buffer); // if (bytesRead < 0) // return Optional.empty(); // // byte[] bytes = new byte[bytesRead]; // buffer.flip(); // buffer.get(bytes); // String message = new String(bytes); // return Optional.of(message); // } // // public static void setupConnection(Selector selector, SocketChannel client) throws IOException { // // Connect to the remote server // SocketAddress address = new InetSocketAddress("192.168.1.74", 8000); // SocketChannel remote = SocketChannel.open(address); // // // Make sockets non-blocking (should be better performance) // client.configureBlocking(false); // remote.configureBlocking(false); // // client.register(selector, SelectionKey.OP_READ, remote); // remote.register(selector, SelectionKey.OP_READ, client); // } // // static UnixDomainSocketAddress ioSocketAddress() throws IOException { // String system = "default"; // String bundleSn = "org.argeo.slc.jshell"; // // String xdgRunDir = System.getenv("XDG_RUNTIME_DIR"); // Path baseRunDir = Paths.get(xdgRunDir); // Path jshellSocketBase = baseRunDir.resolve("jshell").resolve(system).resolve(bundleSn); // // Files.createDirectories(jshellSocketBase); // // Path ioSocketPath = jshellSocketBase.resolve("io"); // // UnixDomainSocketAddress ioSocketAddress = UnixDomainSocketAddress.of(ioSocketPath); // System.out.println(ioSocketAddress); // return ioSocketAddress; // } } class SocketPipeSource { private ReadableByteChannel inChannel; private WritableByteChannel outChannel; private Thread readOutThread; public void forward(SocketChannel channel) throws IOException { readOutThread = new Thread(() -> { try { ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { if (channel.read(buffer) < 0) break; buffer.flip(); outChannel.write(buffer); buffer.rewind(); } System.exit(0); } catch (IOException e) { e.printStackTrace(); } }, "Read out"); readOutThread.start(); // TODO make it smarter than a 1 byte buffer // we should recognize control characters // e.g ^C // int c = System.in.read(); // if (c == 0x1B) { // break; // } ByteBuffer buffer = ByteBuffer.allocate(1); while (channel.isConnected()) { if (inChannel.read(buffer) < 0) break; buffer.flip(); channel.write(buffer); buffer.rewind(); } // end // TODO make it more robust try { // TODO add timeout readOutThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } public void setInputStream(InputStream in) { inChannel = Channels.newChannel(in); } public void setOutputStream(OutputStream out) { outChannel = Channels.newChannel(out); } }