]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java
JShell improvements
[lgpl/argeo-commons.git] / org.argeo.cms.jshell / src / org / argeo / cms / jshell / LocalJShellSession.java
1 package org.argeo.cms.jshell;
2
3 import static java.net.StandardProtocolFamily.UNIX;
4
5 import java.io.IOException;
6 import java.io.OutputStream;
7 import java.io.PrintStream;
8 import java.net.UnixDomainSocketAddress;
9 import java.nio.channels.ServerSocketChannel;
10 import java.nio.channels.SocketChannel;
11 import java.nio.file.Files;
12 import java.nio.file.Path;
13 import java.util.UUID;
14 import java.util.concurrent.Executors;
15
16 import javax.security.auth.login.LoginContext;
17 import javax.security.auth.login.LoginException;
18
19 import org.argeo.api.cms.CmsAuth;
20 import org.argeo.api.cms.CmsLog;
21 import org.argeo.cms.util.CurrentSubject;
22 import org.argeo.cms.util.FsUtils;
23 import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider;
24
25 import jdk.jshell.tool.JavaShellToolBuilder;
26
27 /** A JShell session based on local UNIX sockets. */
28 class LocalJShellSession implements Runnable {
29 private final static CmsLog log = CmsLog.getLog(LocalJShellSession.class);
30
31 private UUID uuid;
32 private Path sessionDir;
33 private Path socketsDir;
34
35 private Path stdPath;
36 private Path ctlPath;
37
38 private Thread replThread;
39
40 private LoginContext loginContext;
41
42 private Long bundleId;
43
44 private final boolean interactive;
45
46 LocalJShellSession(Path sessionDir, Path bundleIdDir, boolean interactive) {
47 this.interactive = interactive;
48 try {
49 this.sessionDir = sessionDir;
50 this.uuid = UUID.fromString(sessionDir.getFileName().toString());
51 bundleId = Long.parseLong(bundleIdDir.getFileName().toString());
52 socketsDir = bundleIdDir.resolve(uuid.toString());
53 Files.createDirectories(socketsDir);
54
55 stdPath = socketsDir.resolve(JShellClient.STD);
56 Files.createSymbolicLink(sessionDir.resolve(JShellClient.STD), stdPath);
57
58 ctlPath = socketsDir.resolve(JShellClient.CTL);
59 Files.createSymbolicLink(sessionDir.resolve(JShellClient.CTL), ctlPath);
60
61 // TODO proper login
62 try {
63 loginContext = new LoginContext(CmsAuth.DATA_ADMIN.getLoginContextName());
64 loginContext.login();
65 } catch (LoginException e1) {
66 throw new RuntimeException("Could not login as data admin", e1);
67 } finally {
68 }
69
70 } catch (IOException e) {
71 log.error("Cannot initiate local session " + uuid, e);
72 cleanUp();
73 return;
74 }
75 replThread = new Thread(() -> CurrentSubject.callAs(loginContext.getSubject(), Executors.callable(this)),
76 "JShell " + sessionDir);
77 replThread.start();
78 }
79
80 public void run() {
81
82 log.debug(() -> "Started JShell session " + sessionDir);
83 try (SocketPipeMirror std = new SocketPipeMirror(JShellClient.STD + " " + uuid); //
84 SocketPipeMirror ctl = new SocketPipeMirror(JShellClient.CTL + " " + uuid);) {
85 // prepare jshell tool builder
86 String feedbackMode;
87 JavaShellToolBuilder builder = JavaShellToolBuilder.builder();
88 if (interactive) {
89 builder.in(std.getInputStream(), null);
90 builder.out(new PrintStream(std.getOutputStream()));
91 builder.err(new PrintStream(ctl.getOutputStream()));
92 builder.interactiveTerminal(true);
93 feedbackMode = "concise";
94 } else {
95 builder.in(ctl.getInputStream(), std.getInputStream());
96 PrintStream cmdOut = new PrintStream(ctl.getOutputStream());
97 PrintStream discard = new PrintStream(OutputStream.nullOutputStream());
98 builder.out(cmdOut, discard, new PrintStream(std.getOutputStream()));
99 builder.err(cmdOut);
100 builder.promptCapture(true);
101 feedbackMode = "silent";
102 }
103
104 UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdPath);
105 UnixDomainSocketAddress ctlSocketAddress = UnixDomainSocketAddress.of(ctlPath);
106
107 try (ServerSocketChannel stdServerChannel = ServerSocketChannel.open(UNIX);
108 ServerSocketChannel ctlServerChannel = ServerSocketChannel.open(UNIX);) {
109 stdServerChannel.bind(stdSocketAddress);
110 ctlServerChannel.bind(ctlSocketAddress);
111 try (SocketChannel stdChannel = stdServerChannel.accept();
112 SocketChannel ctlChannel = ctlServerChannel.accept();) {
113 std.open(stdChannel);
114 ctl.open(ctlChannel);
115
116 ClassLoader cmsJShellBundleCL = OsgiExecutionControlProvider.class.getClassLoader();
117 ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
118 try {
119 String classpath = OsgiExecutionControlProvider.getBundleClasspath(bundleId);
120 Path bundleStartupScript = OsgiExecutionControlProvider.getBundleStartupScript(bundleId);
121 // we need our own class loader so that Java service loader
122 // finds our ExecutionControlProvider implementation
123 Thread.currentThread().setContextClassLoader(cmsJShellBundleCL);
124 //
125 // START JSHELL
126 //
127 int exitCode = builder.start("--execution", "osgi:bundle(" + bundleId + ")", "--class-path",
128 classpath, "--startup", bundleStartupScript.toString(), "--feedback", feedbackMode);
129 //
130 log.debug("JShell " + sessionDir + " completed with exit code " + exitCode);
131 } finally {
132 Thread.currentThread().setContextClassLoader(currentContextClassLoader);
133 }
134 } finally {
135 }
136 }
137 } catch (Exception e) {
138 throw new RuntimeException("JShell " + sessionDir + " failed", e);
139 } finally {
140 cleanUp();
141 }
142 }
143
144 private void cleanUp() {
145 try {
146 if (Files.exists(socketsDir))
147 FsUtils.delete(socketsDir);
148 if (Files.exists(sessionDir))
149 FsUtils.delete(sessionDir);
150 } catch (IOException e) {
151 log.error("Cannot clean up JShell " + sessionDir, e);
152 }
153
154 if (loginContext != null)
155 try {
156 loginContext.logout();
157 } catch (LoginException e) {
158 log.error("Cannot log out JShell " + sessionDir, e);
159 }
160 }
161 }