1 package org
.argeo
.cms
.jshell
;
3 import static java
.lang
.System
.Logger
.Level
.ERROR
;
4 import static java
.lang
.System
.Logger
.Level
.TRACE
;
5 import static java
.net
.StandardProtocolFamily
.UNIX
;
6 import static java
.nio
.charset
.StandardCharsets
.UTF_8
;
8 import java
.io
.BufferedReader
;
10 import java
.io
.IOException
;
11 import java
.io
.InputStream
;
12 import java
.io
.InputStreamReader
;
13 import java
.io
.OutputStream
;
14 import java
.io
.PrintStream
;
15 import java
.lang
.System
.Logger
;
16 import java
.lang
.management
.ManagementFactory
;
17 import java
.net
.StandardSocketOptions
;
18 import java
.net
.UnixDomainSocketAddress
;
19 import java
.nio
.ByteBuffer
;
20 import java
.nio
.channels
.AsynchronousCloseException
;
21 import java
.nio
.channels
.Channels
;
22 import java
.nio
.channels
.ClosedByInterruptException
;
23 import java
.nio
.channels
.ReadableByteChannel
;
24 import java
.nio
.channels
.SocketChannel
;
25 import java
.nio
.channels
.WritableByteChannel
;
26 import java
.nio
.file
.Files
;
27 import java
.nio
.file
.Path
;
28 import java
.nio
.file
.Paths
;
29 import java
.util
.ArrayList
;
30 import java
.util
.HashMap
;
31 import java
.util
.List
;
33 import java
.util
.UUID
;
35 /** A JShell client to a local CMS node. */
36 public class JShellClient
{
37 private final static Logger logger
= System
.getLogger(JShellClient
.class.getName());
39 public final static String STD
= "std";
40 public final static String CTL
= "ctl";
42 public final static String JSH
= "jsh";
43 public final static String JTERM
= "jterm";
45 private static String sttyExec
= "/usr/bin/stty";
47 /** Benchmark based on uptime. */
48 private static boolean benchmark
= false;
51 * The real path (following symbolic links) to the directory were to create
54 private Path localBase
;
56 /** The symbolic name of the bundle from which to run. */
57 private String symbolicName
;
59 /** The script to run. */
61 /** Additional arguments of the script */
62 private List
<String
> scriptArgs
;
64 private String ttyConfig
;
65 private boolean terminal
;
67 /** Workaround to be able to test in Eclipse console */
68 private boolean inEclipse
= false;
70 public JShellClient(Path targetStateDirectory
, String symbolicName
, Path script
, List
<String
> scriptArgs
) {
72 this.terminal
= System
.console() != null && script
== null;
73 if (inEclipse
&& script
== null)
76 localBase
= targetStateDirectory
.resolve(JTERM
);
78 localBase
= targetStateDirectory
.resolve(JSH
);
80 if (Files
.isSymbolicLink(localBase
)) {
81 localBase
= localBase
.toRealPath();
83 this.symbolicName
= symbolicName
;
85 this.scriptArgs
= scriptArgs
== null ?
new ArrayList
<>() : scriptArgs
;
86 } catch (IOException e
) {
87 throw new IllegalStateException("Cannot initialise client", e
);
95 SocketPipeSource std
= new SocketPipeSource(STD
, script
!= null);
96 std
.setInputStream(System
.in
);
97 std
.setOutputStream(System
.out
);
99 SocketPipeSource ctl
= new SocketPipeSource(CTL
, false);
100 ctl
.setOutputStream(System
.err
);
102 Runtime
.getRuntime().addShutdownHook(new Thread(() -> {
103 // System.out.println("\nShutting down...");
104 toOriginalTerminal();
107 }, "Shut down JShell client"));
109 Path bundleSnDir
= localBase
.resolve(symbolicName
);
110 if (!Files
.exists(bundleSnDir
))
111 Files
.createDirectory(bundleSnDir
);
112 UUID uuid
= UUID
.randomUUID();
113 Path sessionDir
= bundleSnDir
.resolve(uuid
.toString());
115 // creating the directory will trigger opening of the session on server side
116 Files
.createDirectory(sessionDir
);
118 Path stdPath
= sessionDir
.resolve(JShellClient
.STD
);
119 Path ctlPath
= sessionDir
.resolve(JShellClient
.CTL
);
121 while (!(Files
.exists(stdPath
) && Files
.exists(ctlPath
))) {
125 } catch (InterruptedException e
) {
130 UnixDomainSocketAddress stdSocketAddress
= UnixDomainSocketAddress
.of(stdPath
.toRealPath());
131 UnixDomainSocketAddress ctlSocketAddress
= UnixDomainSocketAddress
.of(ctlPath
.toRealPath());
133 try (SocketChannel stdChannel
= SocketChannel
.open(UNIX
);
134 SocketChannel ctlChannel
= SocketChannel
.open(UNIX
);) {
135 ctlChannel
.connect(ctlSocketAddress
);
136 ctl
.process(ctlChannel
);
137 if (script
!= null) {
138 new ScriptThread(ctlChannel
).start();
140 stdChannel
.connect(stdSocketAddress
);
141 std
.process(stdChannel
);
143 while (!std
.isCompleted() && !ctl
.isCompleted()) {
144 // isCompleted() will block
148 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
151 } catch (IOException e
) {
154 toOriginalTerminal();
159 public static void main(String
[] args
) {
162 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
163 List
<String
> plainArgs
= new ArrayList
<>();
164 Map
<String
, List
<String
>> options
= new HashMap
<>();
165 String currentOption
= null;
166 for (int i
= 0; i
< args
.length
; i
++) {
167 if (args
[i
].startsWith("-")) {
168 currentOption
= args
[i
];
169 if ("-h".equals(currentOption
) || "--help".equals(currentOption
)) {
170 printHelp(System
.out
);
173 if (!options
.containsKey(currentOption
))
174 options
.put(currentOption
, new ArrayList
<>());
176 options
.get(currentOption
).add(args
[i
]);
178 plainArgs
.add(args
[i
]);
182 List
<String
> dir
= opt(options
, "-d", "--sockets-dir");
184 throw new IllegalArgumentException("Only one run directory can be specified");
185 Path targetStateDirectory
;
187 targetStateDirectory
= Paths
.get(System
.getProperty("user.dir"));
189 targetStateDirectory
= Paths
.get(dir
.get(0));
190 if (!Files
.exists(targetStateDirectory
)) {
191 // we assume argument is the application id
192 targetStateDirectory
= getRunDir().resolve(dir
.get(0));
196 List
<String
> bundle
= opt(options
, "-b", "--bundle");
197 if (bundle
.size() > 1)
198 throw new IllegalArgumentException("Only one bundle can be specified");
199 String symbolicName
= bundle
.isEmpty() ?
"org.argeo.cms.cli" : bundle
.get(0);
201 Path script
= plainArgs
.isEmpty() ?
null : Paths
.get(plainArgs
.get(0));
202 List
<String
> scriptArgs
= new ArrayList
<>();
203 for (int i
= 1; i
< plainArgs
.size(); i
++)
204 scriptArgs
.add(plainArgs
.get(i
));
206 JShellClient client
= new JShellClient(targetStateDirectory
, symbolicName
, script
, scriptArgs
);
208 } catch (Exception e
) {
210 printHelp(System
.err
);
214 /** Guaranteed to return a non-null list (which may be empty). */
215 private static List
<String
> opt(Map
<String
, List
<String
>> options
, String shortOpt
, String longOpt
) {
216 List
<String
> res
= new ArrayList
<>();
217 if (options
.get(shortOpt
) != null)
218 res
.addAll(options
.get(shortOpt
));
219 if (options
.get(longOpt
) != null)
220 res
.addAll(options
.get(longOpt
));
224 public static void printHelp(PrintStream out
) {
225 out
.println("Start a JShell terminal or execute a JShell script in a local Argeo CMS instance");
226 out
.println("Usage: jshc -d <sockets directory> -b <bundle> [JShell script] [script arguments...]");
227 out
.println(" -d, --sockets-dir app directory with UNIX sockets (default to current dir)");
228 out
.println(" -b, --bundle bundle to activate and use as context (default to org.argeo.cms.cli)");
229 out
.println(" -h, --help this help message");
232 // Copied from org.argeo.cms.util.OS
233 private static Path
getRunDir() {
235 String xdgRunDir
= System
.getenv("XDG_RUNTIME_DIR");
236 if (xdgRunDir
!= null) {
237 // TODO support multiple names
238 runDir
= Paths
.get(xdgRunDir
);
240 String username
= System
.getProperty("user.name");
241 if (username
.equals("root")) {
242 runDir
= Paths
.get("/run");
244 Path homeDir
= Paths
.get(System
.getProperty("user.home"));
245 if (!Files
.isWritable(homeDir
)) {
246 // typically, dameon's home (/usr/sbin) is not writable
247 runDir
= Paths
.get("/tmp/" + username
+ "/run");
249 runDir
= homeDir
.resolve(".cache/argeo");
259 /** Set the terminal to raw mode. */
260 protected synchronized void toRawTerminal() {
261 boolean isWindows
= File
.separatorChar
== '\\';
266 // save current configuration
267 ttyConfig
= stty("-g");
268 if (ttyConfig
== null)
271 // set the console to be character-buffered instead of line-buffered
272 stty("-icanon min 1");
273 // disable character echoing
277 /** Restore original terminal configuration. */
278 protected synchronized void toOriginalTerminal() {
279 if (ttyConfig
== null)
283 } catch (Exception e
) {
290 * Execute the stty command with the specified arguments against the current
293 protected String
stty(String args
) {
294 List
<String
> cmd
= new ArrayList
<>();
297 cmd
.add(sttyExec
+ " " + args
+ " < /dev/tty");
299 logger
.log(TRACE
, () -> cmd
.toString());
302 ProcessBuilder pb
= new ProcessBuilder(cmd
);
303 Process p
= pb
.start();
304 String firstLine
= new BufferedReader(new InputStreamReader(p
.getInputStream())).readLine();
306 logger
.log(TRACE
, () -> firstLine
);
308 } catch (IOException
| InterruptedException e
) {
317 private class ScriptThread
extends Thread
{
318 private SocketChannel channel
;
320 public ScriptThread(SocketChannel channel
) {
321 super("JShell script writer");
322 this.channel
= channel
;
329 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
330 StringBuilder sb
= new StringBuilder();
331 if (!scriptArgs
.isEmpty()) {
332 // additional arguments as $1, $2, etc.
333 for (String arg
: scriptArgs
)
334 sb
.append('\"').append(arg
).append('\"').append(";\n");
339 try (BufferedReader reader
= Files
.newBufferedReader(script
)) {
341 lines
: while ((line
= reader
.readLine()) != null) {
342 if (line
.startsWith("#"))
349 if (channel
.isConnected())
351 } catch (IOException e
) {
352 logger
.log(ERROR
, "Cannot execute " + script
, e
);
356 /** Not optimal, but performance is not critical here. */
357 private void writeLine(Object obj
) throws IOException
{
358 channel
.write(ByteBuffer
.wrap((obj
+ "\n").getBytes(UTF_8
)));
363 /** Pipe streams to a channel. */
364 class SocketPipeSource
{
365 private ReadableByteChannel inChannel
;
366 private WritableByteChannel outChannel
;
368 private Thread readThread
;
369 private Thread forwardThread
;
371 private int inBufferSize
= 1;
372 private int outBufferSize
= 1024;
374 private final String id
;
375 private final boolean batch
;
377 private boolean completed
= false;
379 public SocketPipeSource(String id
, boolean batch
) {
384 public void process(SocketChannel channel
) throws IOException
{
386 Integer socketRcvBuf
= channel
.getOption(StandardSocketOptions
.SO_RCVBUF
);
387 inBufferSize
= socketRcvBuf
;
388 outBufferSize
= socketRcvBuf
;
391 readThread
= new Thread(() -> {
394 ByteBuffer buffer
= ByteBuffer
.allocate(outBufferSize
);
396 if (channel
.read(buffer
) < 0)
399 outChannel
.write(buffer
);
402 } catch (ClosedByInterruptException e
) {
404 } catch (AsynchronousCloseException e
) {
406 } catch (IOException e
) {
410 }, "JShell read " + id
);
413 // TODO make it smarter than a 1 byte buffer
414 // we should recognize control characters
416 // int c = System.in.read();
421 if (inChannel
!= null) {
422 forwardThread
= new Thread(() -> {
424 ByteBuffer buffer
= ByteBuffer
.allocate(inBufferSize
);
425 while (channel
.isConnected()) {
426 if (inChannel
.read(buffer
) < 0) {
427 System
.err
.println("in EOF");
428 channel
.shutdownOutput();
431 // int b = (int) buffer.get(0);
433 // System.out.println("Ctrl+C");
437 channel
.write(buffer
);
440 } catch (IOException e
) {
443 }, "JShell write " + id
);
444 forwardThread
.setDaemon(true);
445 forwardThread
.start();
447 // TODO make it more robust
448 // we want to be asynchronous when read only
450 // // TODO add timeout
451 // readThread.join();
452 // } catch (InterruptedException e) {
453 // e.printStackTrace();
459 public synchronized boolean isCompleted() {
463 } catch (InterruptedException e
) {
469 protected synchronized void markCompleted() {
474 public void shutdown() {
475 if (inChannel
!= null)
478 } catch (IOException e
) {
483 } catch (IOException e
) {
486 // if (inChannel != null)
487 // forwardThread.interrupt();
488 // readThread.interrupt();
491 public void setInputStream(InputStream in
) {
492 inChannel
= Channels
.newChannel(in
);
495 public void setOutputStream(OutputStream out
) {
496 outChannel
= Channels
.newChannel(out
);