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
.Channels
;
20 import java
.nio
.channels
.ClosedByInterruptException
;
21 import java
.nio
.channels
.ReadableByteChannel
;
22 import java
.nio
.channels
.SocketChannel
;
23 import java
.nio
.channels
.WritableByteChannel
;
24 import java
.nio
.file
.Files
;
25 import java
.nio
.file
.Path
;
26 import java
.nio
.file
.Paths
;
27 import java
.util
.ArrayList
;
28 import java
.util
.HashMap
;
29 import java
.util
.List
;
31 import java
.util
.UUID
;
33 public class JShellClient
{
34 private final static Logger logger
= System
.getLogger(JShellClient
.class.getName());
36 public final static String STD
= "std";
37 public final static String CTL
= "ctl";
39 public final static String JSH
= "jsh";
40 public final static String JTERM
= "jterm";
42 private static String sttyExec
= "/usr/bin/stty";
44 /** Benchmark based on uptime. */
45 private static boolean benchmark
= false;
48 * The real path (following symbolic links) to the directory were to create
51 private Path localBase
;
53 /** The symbolic name of the bundle from which to run. */
54 private String symbolicName
;
56 /** The script to run. */
58 /** Additional arguments of the script */
59 private List
<String
> scriptArgs
;
61 private String ttyConfig
;
62 private boolean terminal
;
64 /** Workaround to be able to test in Eclipse console */
65 private boolean inEclipse
= false;
67 public JShellClient(Path targetStateDirectory
, String symbolicName
, Path script
, List
<String
> scriptArgs
) {
69 this.terminal
= System
.console() != null && script
== null;
70 if (inEclipse
&& script
== null)
73 localBase
= targetStateDirectory
.resolve(JTERM
);
75 localBase
= targetStateDirectory
.resolve(JSH
);
77 if (Files
.isSymbolicLink(localBase
)) {
78 localBase
= localBase
.toRealPath();
80 this.symbolicName
= symbolicName
;
82 this.scriptArgs
= scriptArgs
== null ?
new ArrayList
<>() : scriptArgs
;
83 } catch (IOException e
) {
84 throw new IllegalStateException("Cannot initialise client", e
);
92 SocketPipeSource std
= new SocketPipeSource(STD
, script
!= null);
93 std
.setInputStream(System
.in
);
94 std
.setOutputStream(System
.out
);
96 SocketPipeSource ctl
= new SocketPipeSource(CTL
, false);
97 ctl
.setOutputStream(System
.err
);
99 Runtime
.getRuntime().addShutdownHook(new Thread(() -> {
100 System
.out
.println("\nShutting down...");
101 toOriginalTerminal();
104 }, "Shut down JShell client"));
106 Path bundleSnDir
= localBase
.resolve(symbolicName
);
107 if (!Files
.exists(bundleSnDir
))
108 Files
.createDirectory(bundleSnDir
);
109 UUID uuid
= UUID
.randomUUID();
110 Path sessionDir
= bundleSnDir
.resolve(uuid
.toString());
112 // creating the directory will trigger opening of the session on server side
113 Files
.createDirectory(sessionDir
);
115 Path stdPath
= sessionDir
.resolve(JShellClient
.STD
);
116 Path ctlPath
= sessionDir
.resolve(JShellClient
.CTL
);
118 while (!(Files
.exists(stdPath
) && Files
.exists(ctlPath
))) {
122 } catch (InterruptedException e
) {
127 UnixDomainSocketAddress stdSocketAddress
= UnixDomainSocketAddress
.of(stdPath
.toRealPath());
128 UnixDomainSocketAddress ctlSocketAddress
= UnixDomainSocketAddress
.of(ctlPath
.toRealPath());
130 try (SocketChannel stdChannel
= SocketChannel
.open(UNIX
);
131 SocketChannel ctlChannel
= SocketChannel
.open(UNIX
);) {
132 ctlChannel
.connect(ctlSocketAddress
);
133 ctl
.process(ctlChannel
);
134 if (script
!= null) {
135 new ScriptThread(ctlChannel
).start();
137 stdChannel
.connect(stdSocketAddress
);
138 std
.process(stdChannel
);
140 while (!std
.isCompleted() && !ctl
.isCompleted()) {
141 // isCompleted() will block
145 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
148 } catch (IOException e
) {
151 toOriginalTerminal();
156 public static void main(String
[] args
) throws IOException
, InterruptedException
{
158 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
159 List
<String
> plainArgs
= new ArrayList
<>();
160 Map
<String
, List
<String
>> options
= new HashMap
<>();
161 String currentOption
= null;
162 for (int i
= 0; i
< args
.length
; i
++) {
163 if (args
[i
].startsWith("-")) {
164 currentOption
= args
[i
];
165 if (!options
.containsKey(currentOption
))
166 options
.put(currentOption
, new ArrayList
<>());
168 options
.get(currentOption
).add(args
[i
]);
170 plainArgs
.add(args
[i
]);
174 Path targetStateDirectory
= Paths
.get(options
.get("-d").get(0));
175 String symbolicName
= options
.get("-b").get(0);
177 Path script
= plainArgs
.isEmpty() ?
null : Paths
.get(plainArgs
.get(0));
178 List
<String
> scriptArgs
= new ArrayList
<>();
179 for (int i
= 1; i
< plainArgs
.size(); i
++)
180 scriptArgs
.add(plainArgs
.get(i
));
182 JShellClient client
= new JShellClient(targetStateDirectory
, symbolicName
, script
, scriptArgs
);
189 /** Set the terminal to raw mode. */
190 protected synchronized void toRawTerminal() {
191 boolean isWindows
= File
.separatorChar
== '\\';
196 // save current configuration
197 ttyConfig
= stty("-g");
198 if (ttyConfig
== null)
201 // set the console to be character-buffered instead of line-buffered
202 stty("-icanon min 1");
203 // disable character echoing
207 /** Restore original terminal configuration. */
208 protected synchronized void toOriginalTerminal() {
209 if (ttyConfig
== null)
213 } catch (Exception e
) {
220 * Execute the stty command with the specified arguments against the current
223 protected String
stty(String args
) {
224 List
<String
> cmd
= new ArrayList
<>();
227 cmd
.add(sttyExec
+ " " + args
+ " < /dev/tty");
229 logger
.log(TRACE
, () -> cmd
.toString());
232 ProcessBuilder pb
= new ProcessBuilder(cmd
);
233 Process p
= pb
.start();
234 String firstLine
= new BufferedReader(new InputStreamReader(p
.getInputStream())).readLine();
236 logger
.log(TRACE
, () -> firstLine
);
238 } catch (IOException
| InterruptedException e
) {
247 private class ScriptThread
extends Thread
{
248 private SocketChannel channel
;
250 public ScriptThread(SocketChannel channel
) {
251 super("JShell script writer");
252 this.channel
= channel
;
259 System
.err
.println(ManagementFactory
.getRuntimeMXBean().getUptime());
260 StringBuilder sb
= new StringBuilder();
261 // sb.append("/set feedback silent\n");
262 if (!scriptArgs
.isEmpty()) {
263 // additional arguments as $1, $2, etc.
264 for (String arg
: scriptArgs
)
265 sb
.append('\"').append(arg
).append('\"').append(";\n");
270 ByteBuffer buffer
= ByteBuffer
.allocate(1024);
271 try (BufferedReader reader
= Files
.newBufferedReader(script
)) {
273 lines
: while ((line
= reader
.readLine()) != null) {
274 if (line
.startsWith("#"))
276 buffer
.put((line
+ "\n").getBytes(UTF_8
));
278 channel
.write(buffer
);
283 // ByteBuffer buffer = ByteBuffer.allocate(1024);
284 // try (SeekableByteChannel scriptChannel = Files.newByteChannel(script, StandardOpenOption.READ)) {
285 // while (channel.isConnected()) {
286 // if (scriptChannel.read(buffer) < 0)
289 // channel.write(buffer);
295 if (channel
.isConnected())
297 } catch (IOException e
) {
298 logger
.log(ERROR
, "Cannot execute " + script
, e
);
302 private void writeLine(Object obj
) throws IOException
{
303 channel
.write(ByteBuffer
.wrap((obj
+ "\n").getBytes(UTF_8
)));
308 /** Pipe streams to a channel. */
309 class SocketPipeSource
{
310 private ReadableByteChannel inChannel
;
311 private WritableByteChannel outChannel
;
313 private Thread readThread
;
314 private Thread forwardThread
;
316 private int inBufferSize
= 1;
317 private int outBufferSize
= 1024;
319 private final String id
;
320 private final boolean batch
;
322 private boolean completed
= false;
324 public SocketPipeSource(String id
, boolean batch
) {
329 public void process(SocketChannel channel
) throws IOException
{
331 Integer socketRcvBuf
= channel
.getOption(StandardSocketOptions
.SO_RCVBUF
);
332 inBufferSize
= socketRcvBuf
;
333 outBufferSize
= socketRcvBuf
;
336 readThread
= new Thread(() -> {
339 ByteBuffer buffer
= ByteBuffer
.allocate(outBufferSize
);
341 if (channel
.read(buffer
) < 0)
344 outChannel
.write(buffer
);
347 } catch (ClosedByInterruptException e
) {
349 } catch (IOException e
) {
353 }, "JShell read " + id
);
356 // TODO make it smarter than a 1 byte buffer
357 // we should recognize control characters
359 // int c = System.in.read();
364 if (inChannel
!= null) {
365 forwardThread
= new Thread(() -> {
367 ByteBuffer buffer
= ByteBuffer
.allocate(inBufferSize
);
368 while (channel
.isConnected()) {
369 if (inChannel
.read(buffer
) < 0)
371 // int b = (int) buffer.get(0);
373 // System.out.println("Ctrl+C");
377 channel
.write(buffer
);
380 } catch (IOException e
) {
383 }, "JShell write " + id
);
384 forwardThread
.setDaemon(true);
385 forwardThread
.start();
387 // TODO make it more robust
388 // we want to be asynchronous when read only
390 // // TODO add timeout
391 // readThread.join();
392 // } catch (InterruptedException e) {
393 // e.printStackTrace();
399 public synchronized boolean isCompleted() {
403 } catch (InterruptedException e
) {
409 protected synchronized void markCompleted() {
414 public void shutdown() {
415 if (inChannel
!= null)
418 } catch (IOException e
) {
423 } catch (IOException e
) {
426 if (inChannel
!= null)
427 forwardThread
.interrupt();
428 readThread
.interrupt();
431 public void setInputStream(InputStream in
) {
432 inChannel
= Channels
.newChannel(in
);
435 public void setOutputStream(OutputStream out
) {
436 outChannel
= Channels
.newChannel(out
);