Improve JShell client CLI
[lgpl/argeo-commons.git] / org.argeo.cms.jshell / src / org / argeo / cms / jshell / JShellClient.java
index f6846f196c0192204e54acc5e3e0ec895461c9bc..f090ed0684a85b3be957b9da3af309f46f0ec67b 100644 (file)
@@ -11,11 +11,13 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.io.PrintStream;
 import java.lang.System.Logger;
 import java.lang.management.ManagementFactory;
 import java.net.StandardSocketOptions;
 import java.net.UnixDomainSocketAddress;
 import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
 import java.nio.channels.Channels;
 import java.nio.channels.ClosedByInterruptException;
 import java.nio.channels.ReadableByteChannel;
@@ -30,6 +32,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+/** A JShell client to a local CMS node. */
 public class JShellClient {
        private final static Logger logger = System.getLogger(JShellClient.class.getName());
 
@@ -97,7 +100,7 @@ public class JShellClient {
                        ctl.setOutputStream(System.err);
 
                        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
-                               System.out.println("\nShutting down...");
+//                             System.out.println("\nShutting down...");
                                toOriginalTerminal();
                                std.shutdown();
                                ctl.shutdown();
@@ -153,34 +156,101 @@ public class JShellClient {
 
        }
 
-       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]);
+       public static void main(String[] args) {
+               try {
+                       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 ("-h".equals(currentOption) || "--help".equals(currentOption)) {
+                                               printHelp(System.out);
+                                               return;
+                                       }
+                                       if (!options.containsKey(currentOption))
+                                               options.put(currentOption, new ArrayList<>());
+                                       i++;
+                                       options.get(currentOption).add(args[i]);
+                               } else {
+                                       plainArgs.add(args[i]);
+                               }
+                       }
+
+                       List<String> dir = opt(options, "-d", "--sockets-dir");
+                       if (dir.size() > 1)
+                               throw new IllegalArgumentException("Only one run directory can be specified");
+                       Path targetStateDirectory;
+                       if (dir.isEmpty())
+                               targetStateDirectory = Paths.get(System.getProperty("user.dir"));
+                       else {
+                               targetStateDirectory = Paths.get(dir.get(0));
+                               if (!Files.exists(targetStateDirectory)) {
+                                       // we assume argument is the application id
+                                       targetStateDirectory = getRunDir().resolve(dir.get(0));
+                               }
                        }
+
+                       List<String> bundle = opt(options, "-b", "--bundle");
+                       if (bundle.size() > 1)
+                               throw new IllegalArgumentException("Only one bundle can be specified");
+                       String symbolicName = bundle.isEmpty() ? "org.argeo.cms.cli" : bundle.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();
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       printHelp(System.err);
                }
+       }
 
-               Path targetStateDirectory = Paths.get(options.get("-d").get(0));
-               String symbolicName = options.get("-b").get(0);
+       /** Guaranteed to return a non-null list (which may be empty). */
+       private static List<String> opt(Map<String, List<String>> options, String shortOpt, String longOpt) {
+               List<String> res = new ArrayList<>();
+               if (options.get(shortOpt) != null)
+                       res.addAll(options.get(shortOpt));
+               if (options.get(longOpt) != null)
+                       res.addAll(options.get(longOpt));
+               return res;
+       }
 
-               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));
+       public static void printHelp(PrintStream out) {
+               out.println("Start a JShell terminal or execute a JShell script in a local Argeo CMS instance");
+               out.println("Usage: jshc -d <sockets directory> -b <bundle> [JShell script] [script arguments...]");
+               out.println("  -d, --sockets-dir  app directory with UNIX sockets (default to current dir)");
+               out.println("  -b, --bundle       bundle to activate and use as context (default to org.argeo.cms.cli)");
+               out.println("  -h, --help         this help message");
+       }
 
-               JShellClient client = new JShellClient(targetStateDirectory, symbolicName, script, scriptArgs);
-               client.run();
+       // Copied from org.argeo.cms.util.OS
+       private static Path getRunDir() {
+               Path runDir;
+               String xdgRunDir = System.getenv("XDG_RUNTIME_DIR");
+               if (xdgRunDir != null) {
+                       // TODO support multiple names
+                       runDir = Paths.get(xdgRunDir);
+               } else {
+                       String username = System.getProperty("user.name");
+                       if (username.equals("root")) {
+                               runDir = Paths.get("/run");
+                       } else {
+                               Path homeDir = Paths.get(System.getProperty("user.home"));
+                               if (!Files.isWritable(homeDir)) {
+                                       // typically, dameon's home (/usr/sbin) is not writable
+                                       runDir = Paths.get("/tmp/" + username + "/run");
+                               } else {
+                                       runDir = homeDir.resolve(".cache/argeo");
+                               }
+                       }
+               }
+               return runDir;
        }
 
        /*
@@ -258,7 +328,6 @@ public class JShellClient {
                                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)
@@ -267,30 +336,15 @@ public class JShellClient {
                                if (sb.length() > 0)
                                        writeLine(sb);
 
-                               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();
+                                               writeLine(line);
                                        }
                                }
 
-//                             ByteBuffer buffer = ByteBuffer.allocate(1024);
-//                             try (SeekableByteChannel scriptChannel = Files.newByteChannel(script, StandardOpenOption.READ)) {
-//                                     while (channel.isConnected()) {
-//                                             if (scriptChannel.read(buffer) < 0)
-//                                                     break;
-//                                             buffer.flip();
-//                                             channel.write(buffer);
-//                                             buffer.rewind();
-//                                     }
-//                             }
-
                                // exit
                                if (channel.isConnected())
                                        writeLine("/exit");
@@ -299,6 +353,7 @@ public class JShellClient {
                        }
                }
 
+               /** Not optimal, but performance is not critical here. */
                private void writeLine(Object obj) throws IOException {
                        channel.write(ByteBuffer.wrap((obj + "\n").getBytes(UTF_8)));
                }
@@ -346,6 +401,8 @@ class SocketPipeSource {
                                }
                        } catch (ClosedByInterruptException e) {
                                // silent
+                       } catch (AsynchronousCloseException e) {
+                               // silent
                        } catch (IOException e) {
                                e.printStackTrace();
                        }
@@ -366,8 +423,11 @@ class SocketPipeSource {
                                try {
                                        ByteBuffer buffer = ByteBuffer.allocate(inBufferSize);
                                        while (channel.isConnected()) {
-                                               if (inChannel.read(buffer) < 0)
+                                               if (inChannel.read(buffer) < 0) {
+                                                       System.err.println("in EOF");
+                                                       channel.shutdownOutput();
                                                        break;
+                                               }
 //                     int b = (int) buffer.get(0);
 //                     if (b == 0x1B) {
 //                             System.out.println("Ctrl+C");
@@ -423,9 +483,9 @@ class SocketPipeSource {
                } catch (IOException e) {
                        e.printStackTrace();
                }
-               if (inChannel != null)
-                       forwardThread.interrupt();
-               readThread.interrupt();
+//             if (inChannel != null)
+//                     forwardThread.interrupt();
+//             readThread.interrupt();
        }
 
        public void setInputStream(InputStream in) {