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
.lang
.System
.Logger
;
15 import java
.lang
.management
.ManagementFactory
;
16 import java
.net
.StandardSocketOptions
;
17 import java
.net
.UnixDomainSocketAddress
;
18 import java
.nio
.ByteBuffer
;
19 import java
.nio
.channels
.AsynchronousCloseException
;
20 import java
.nio
.channels
.Channels
;
21 import java
.nio
.channels
.ClosedByInterruptException
;
22 import java
.nio
.channels
.ReadableByteChannel
;
23 import java
.nio
.channels
.SocketChannel
;
24 import java
.nio
.channels
.WritableByteChannel
;
25 import java
.nio
.file
.Files
;
26 import java
.nio
.file
.Path
;
27 import java
.nio
.file
.Paths
;
28 import java
.util
.ArrayList
;
29 import java
.util
.HashMap
;
30 import java
.util
.List
;
32 import java
.util
.UUID
;
34 /** A JShell client to a local CMS node. */
35 public class JShellClient
{
36 private final static Logger logger
= System
.getLogger(JShellClient
.class.getName());
38 public final static String STD
= "std";
39 public final static String CTL
= "ctl";
41 public final static String JSH
= "jsh";
42 public final static String JTERM
= "jterm";
44 private static String sttyExec
= "/usr/bin/stty";
46 /** Benchmark based on uptime. */
47 private static boolean benchmark
= false;
50 * The real path (following symbolic links) to the directory were to create
53 private Path localBase
;
55 /** The symbolic name of the bundle from which to run. */
56 private String symbolicName
;
58 /** The script to run. */
60 /** Additional arguments of the script */
61 private List
<String
> scriptArgs
;
63 private String ttyConfig
;
64 private boolean terminal
;
66 /** Workaround to be able to test in Eclipse console */
67 private boolean inEclipse
= false;
69 public JShellClient(Path targetStateDirectory
, String symbolicName
, Path script
, List
<String
> scriptArgs
) {
71 this.terminal
= System
.console() != null && script
== null;
72 if (inEclipse
&& script
== null)
75 localBase
= targetStateDirectory
.resolve(JTERM
);
77 localBase
= targetStateDirectory
.resolve(JSH
);
79 if (Files
.isSymbolicLink(localBase
)) {
80 localBase
= localBase
.toRealPath();
82 this.symbolicName
= symbolicName
;
84 this.scriptArgs
= scriptArgs
== null ?
new ArrayList
<>() : scriptArgs
;
85 } catch (IOException e
) {
86 throw new IllegalStateException("Cannot initialise client", e
);
94 SocketPipeSource std
= new SocketPipeSource(STD
, script
!= null);
95 std
.setInputStream(System
.in
);
96 std
.setOutputStream(System
.out
);
98 SocketPipeSource ctl
= new SocketPipeSource(CTL
, false);
99 ctl
.setOutputStream(System
.err
);
101 Runtime
.getRuntime().addShutdownHook(new Thread(() -> {
102 // System.out.println("\nShutting down...");
103 toOriginalTerminal();
106 }, "Shut down JShell client"));
108 Path bundleSnDir
= localBase
.resolve(symbolicName
);
109 if (!Files
.exists(bundleSnDir
))
110 Files
.createDirectory(bundleSnDir
);
111 UUID uuid
= UUID
.randomUUID();
112 Path sessionDir
= bundleSnDir
.resolve(uuid
.toString());
114 // creating the directory will trigger opening of the session on server side
115 Files
.createDirectory(sessionDir
);
117 Path stdPath
= sessionDir
.resolve(JShellClient
.STD
);
118 Path ctlPath
= sessionDir
.resolve(JShellClient
.CTL
);
120 while (!(Files
.exists(stdPath
) && Files
.exists(ctlPath
))) {
124 } catch (InterruptedException e
) {
129 UnixDomainSocketAddress stdSocketAddress
= UnixDomainSocketAddress
.of(stdPath
.toRealPath());
130 UnixDomainSocketAddress ctlSocketAddress
= UnixDomainSocketAddress
.of(ctlPath
.toRealPath());
132 try (SocketChannel stdChannel
= SocketChannel
.open(UNIX
);
133 SocketChannel ctlChannel
= SocketChannel
.open(UNIX
);) {
134 ctlChannel
.connect(ctlSocketAddress
);
135 ctl
.process(ctlChannel
);
136 if (script
!= null) {
137 new ScriptThread(ctlChannel
).start();
139 stdChannel
.connect(stdSocketAddress
);
140 std
.process(stdChannel
);
142 while (!std
.isCompleted() && !ctl
.isCompleted()) {
143 // isCompleted() will block
147 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
150 } catch (IOException e
) {
153 toOriginalTerminal();
158 public static void main(String
[] args
) throws IOException
, InterruptedException
{
160 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
161 List
<String
> plainArgs
= new ArrayList
<>();
162 Map
<String
, List
<String
>> options
= new HashMap
<>();
163 String currentOption
= null;
164 for (int i
= 0; i
< args
.length
; i
++) {
165 if (args
[i
].startsWith("-")) {
166 currentOption
= args
[i
];
167 if (!options
.containsKey(currentOption
))
168 options
.put(currentOption
, new ArrayList
<>());
170 options
.get(currentOption
).add(args
[i
]);
172 plainArgs
.add(args
[i
]);
176 Path targetStateDirectory
= Paths
.get(options
.get("-d").get(0));
177 String symbolicName
= options
.get("-b").get(0);
179 Path script
= plainArgs
.isEmpty() ?
null : Paths
.get(plainArgs
.get(0));
180 List
<String
> scriptArgs
= new ArrayList
<>();
181 for (int i
= 1; i
< plainArgs
.size(); i
++)
182 scriptArgs
.add(plainArgs
.get(i
));
184 JShellClient client
= new JShellClient(targetStateDirectory
, symbolicName
, script
, scriptArgs
);
191 /** Set the terminal to raw mode. */
192 protected synchronized void toRawTerminal() {
193 boolean isWindows
= File
.separatorChar
== '\\';
198 // save current configuration
199 ttyConfig
= stty("-g");
200 if (ttyConfig
== null)
203 // set the console to be character-buffered instead of line-buffered
204 stty("-icanon min 1");
205 // disable character echoing
209 /** Restore original terminal configuration. */
210 protected synchronized void toOriginalTerminal() {
211 if (ttyConfig
== null)
215 } catch (Exception e
) {
222 * Execute the stty command with the specified arguments against the current
225 protected String
stty(String args
) {
226 List
<String
> cmd
= new ArrayList
<>();
229 cmd
.add(sttyExec
+ " " + args
+ " < /dev/tty");
231 logger
.log(TRACE
, () -> cmd
.toString());
234 ProcessBuilder pb
= new ProcessBuilder(cmd
);
235 Process p
= pb
.start();
236 String firstLine
= new BufferedReader(new InputStreamReader(p
.getInputStream())).readLine();
238 logger
.log(TRACE
, () -> firstLine
);
240 } catch (IOException
| InterruptedException e
) {
249 private class ScriptThread
extends Thread
{
250 private SocketChannel channel
;
252 public ScriptThread(SocketChannel channel
) {
253 super("JShell script writer");
254 this.channel
= channel
;
261 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
262 StringBuilder sb
= new StringBuilder();
263 if (!scriptArgs
.isEmpty()) {
264 // additional arguments as $1, $2, etc.
265 for (String arg
: scriptArgs
)
266 sb
.append('\"').append(arg
).append('\"').append(";\n");
271 try (BufferedReader reader
= Files
.newBufferedReader(script
)) {
273 lines
: while ((line
= reader
.readLine()) != null) {
274 if (line
.startsWith("#"))
281 if (channel
.isConnected())
283 } catch (IOException e
) {
284 logger
.log(ERROR
, "Cannot execute " + script
, e
);
288 /** Not optimal, but performance is not critical here. */
289 private void writeLine(Object obj
) throws IOException
{
290 channel
.write(ByteBuffer
.wrap((obj
+ "\n").getBytes(UTF_8
)));
295 /** Pipe streams to a channel. */
296 class SocketPipeSource
{
297 private ReadableByteChannel inChannel
;
298 private WritableByteChannel outChannel
;
300 private Thread readThread
;
301 private Thread forwardThread
;
303 private int inBufferSize
= 1;
304 private int outBufferSize
= 1024;
306 private final String id
;
307 private final boolean batch
;
309 private boolean completed
= false;
311 public SocketPipeSource(String id
, boolean batch
) {
316 public void process(SocketChannel channel
) throws IOException
{
318 Integer socketRcvBuf
= channel
.getOption(StandardSocketOptions
.SO_RCVBUF
);
319 inBufferSize
= socketRcvBuf
;
320 outBufferSize
= socketRcvBuf
;
323 readThread
= new Thread(() -> {
326 ByteBuffer buffer
= ByteBuffer
.allocate(outBufferSize
);
328 if (channel
.read(buffer
) < 0)
331 outChannel
.write(buffer
);
334 } catch (ClosedByInterruptException e
) {
336 } catch (AsynchronousCloseException e
) {
338 } catch (IOException e
) {
342 }, "JShell read " + id
);
345 // TODO make it smarter than a 1 byte buffer
346 // we should recognize control characters
348 // int c = System.in.read();
353 if (inChannel
!= null) {
354 forwardThread
= new Thread(() -> {
356 ByteBuffer buffer
= ByteBuffer
.allocate(inBufferSize
);
357 while (channel
.isConnected()) {
358 if (inChannel
.read(buffer
) < 0) {
359 System
.err
.println("in EOF");
360 channel
.shutdownOutput();
363 // int b = (int) buffer.get(0);
365 // System.out.println("Ctrl+C");
369 channel
.write(buffer
);
372 } catch (IOException e
) {
375 }, "JShell write " + id
);
376 forwardThread
.setDaemon(true);
377 forwardThread
.start();
379 // TODO make it more robust
380 // we want to be asynchronous when read only
382 // // TODO add timeout
383 // readThread.join();
384 // } catch (InterruptedException e) {
385 // e.printStackTrace();
391 public synchronized boolean isCompleted() {
395 } catch (InterruptedException e
) {
401 protected synchronized void markCompleted() {
406 public void shutdown() {
407 if (inChannel
!= null)
410 } catch (IOException e
) {
415 } catch (IOException e
) {
418 // if (inChannel != null)
419 // forwardThread.interrupt();
420 // readThread.interrupt();
423 public void setInputStream(InputStream in
) {
424 inChannel
= Channels
.newChannel(in
);
427 public void setOutputStream(OutputStream out
) {
428 outChannel
= Channels
.newChannel(out
);