package org.argeo.cms.jshell;
import java.io.IOException;
+import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import org.argeo.api.cms.CmsState;
import org.argeo.api.uuid.UuidFactory;
import org.argeo.cms.util.OS;
+import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider;
+import org.osgi.framework.Bundle;
public class CmsJShell {
private final static CmsLog log = CmsLog.getLog(CmsJShell.class);
private CmsState cmsState;
private Map<Path, LocalJShellSession> localSessions = new HashMap<>();
+ private Map<Path, Path> bundleDirs = new HashMap<>();
+ private Path stateRunDir;
private Path localBase;
private Path linkedDir;
+// private String defaultBundle = "org.argeo.cms.cli";
+
public void start() throws Exception {
// Path localBase = cmsState.getStatePath("org.argeo.cms.jshell/local");
- UUID stateUuid = cmsState.getUuid();
+// UUID stateUuid = cmsState.getUuid();
+
+ // TODO better define application id, make it configurable
+ String applicationID = cmsState.getStatePath("").getFileName().toString();
// TODO centralise state run dir
- Path stateRunDir = OS.getRunDir().resolve(stateUuid.toString());
+ stateRunDir = OS.getRunDir().resolve(applicationID);
localBase = stateRunDir.resolve("jsh");
Files.createDirectories(localBase);
localBase.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE);
+ try (DirectoryStream<Path> bundleSns = Files.newDirectoryStream(localBase)) {
+ for (Path bundleSnDir : bundleSns) {
+ addBundleSnDir(bundleSnDir);
+ if (bundleDirs.containsKey(bundleSnDir)) {
+ bundleSnDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
+ StandardWatchEventKinds.ENTRY_DELETE);
+ }
+ }
+ }
WatchKey key;
while ((key = watchService.take()) != null) {
events: for (WatchEvent<?> event : key.pollEvents()) {
// System.out.println("Event kind:" + event.kind() + ". File affected: " + event.context() + ".");
- Path path = localBase.resolve((Path) event.context());
+ Path parent = (Path) key.watchable();
// sessions
- if (Files.isSameFile(localBase, path.getParent())) {
+ if (Files.isSameFile(localBase, parent)) {
+ Path bundleSnDir = localBase.resolve((Path) event.context());
+ if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) {
+ addBundleSnDir(bundleSnDir);
+ if (bundleDirs.containsKey(bundleSnDir)) {
+ bundleSnDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
+ StandardWatchEventKinds.ENTRY_DELETE);
+ }
+ } else if (StandardWatchEventKinds.ENTRY_DELETE.equals(event.kind())) {
+ }
+ } else {
+ Path path = parent.resolve((Path) event.context());
if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) {
if (!Files.isDirectory(path)) {
log.warn("Ignoring " + path + " as it is not a directory");
continue events;
}
- LocalJShellSession localSession = new LocalJShellSession(path);
+ Path bundleIdDir = bundleDirs.get(parent);
+ LocalJShellSession localSession = new LocalJShellSession(path, bundleIdDir);
localSessions.put(path, localSession);
} else if (StandardWatchEventKinds.ENTRY_DELETE.equals(event.kind())) {
// TODO clean up session
LocalJShellSession localSession = localSessions.remove(path);
localSession.cleanUp();
}
- } else {
-// if (StandardWatchEventKinds.ENTRY_CREATE.equals(event.kind())) {
-// Path sessionDir = path.getParent();
-// LocalSession session = localSessions.get(sessionDir);
-// if (session == null) {
-// sessions: for (Path p : localSessions.keySet()) {
-// if (Files.isSameFile(sessionDir, p)) {
-// session = localSessions.get(p);
-// break sessions;
-// }
-// }
-// }
-// if (session == null) {
-// log.warn("Ignoring " + path + " as its parent is not a registered session");
-// continue events;
-// }
-// session.addChild(path);
-// }
-
}
}
key.reset();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
- }, "JSChell local sessions watcher").start();
+ }, "JShell local sessions watcher").start();
// thread context class loader should be where the service is defined
// Thread.currentThread().setContextClassLoader(loader);
}
+ private void addBundleSnDir(Path bundleSnDir) throws IOException {
+ String symbolicName = bundleSnDir.getFileName().toString();
+ Bundle fromBundle = OsgiExecutionControlProvider.getBundleFromSn(symbolicName);
+ if (fromBundle == null) {
+ log.error("Ignoring bundle " + symbolicName + " because it was not found");
+ return;
+ }
+ Long bundleId = fromBundle.getBundleId();
+ Path bundleIdDir = stateRunDir.resolve(bundleId.toString());
+ Files.createDirectories(bundleIdDir);
+ bundleDirs.put(bundleSnDir, bundleIdDir);
+ }
+
// public void startX(BundleContext bc) {
// uuidFactory = new NoOpUuidFactory();
//
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.lang.System.Logger;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
+import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
-import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.nio.file.StandardWatchEventKinds;
-import java.nio.file.WatchEvent;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
import java.util.UUID;
public class JShellClient {
+ private final static Logger logger = System.getLogger(JShellClient.class.getName());
+
public final static String STDIO = "stdio";
public final static String STDERR = "stderr";
public final static String CMDIO = "cmdio";
public static void main(String[] args) throws IOException, InterruptedException {
try {
Path targetStateDirectory = Paths.get(args[0]);
+ String symbolicName = args[1];
Path localBase = targetStateDirectory.resolve("jsh");
if (Files.isSymbolicLink(localBase)) {
localBase = localBase.toRealPath();
toRawTerminal();
}
- SocketPipeSource std = new SocketPipeSource();
- std.setInputStream(System.in);
- std.setOutputStream(System.out);
+ SocketPipeSource stdio = new SocketPipeSource();
+ stdio.setInputStream(System.in);
+ stdio.setOutputStream(System.out);
+
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ // logger.log(Logger.Level.INFO, "Shutting down...");
+ System.out.println("\nShutting down...");
+ stdio.shutdown();
+ }, "Shut down JShell client"));
+ Path bundleSnDir = localBase.resolve(symbolicName);
+ if (!Files.exists(bundleSnDir))
+ Files.createDirectory(bundleSnDir);
UUID uuid = UUID.randomUUID();
- Path sessionDir = localBase.resolve(uuid.toString());
+ Path sessionDir = bundleSnDir.resolve(uuid.toString());
Files.createDirectory(sessionDir);
- Path stdPath = sessionDir.resolve(JShellClient.STDIO);
-
- // wait for sockets to be available
- WatchService watchService = FileSystems.getDefault().newWatchService();
- sessionDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
- WatchKey key;
- watch: while ((key = watchService.take()) != null) {
- for (WatchEvent<?> event : key.pollEvents()) {
- Path path = sessionDir.resolve((Path) event.context());
- if (Files.isSameFile(stdPath, path)) {
- break watch;
- }
- }
+ Path stdioPath = sessionDir.resolve(JShellClient.STDIO);
+
+ while (!(Files.exists(stdioPath))) {
+ // TODO timeout
+ Thread.sleep(50);
+
+// // wait for sockets to be available
+// WatchService watchService = FileSystems.getDefault().newWatchService();
+// sessionDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
+// WatchKey key;
+// watch: while ((key = watchService.take()) != null) {
+// for (WatchEvent<?> event : key.pollEvents()) {
+// Path path = sessionDir.resolve((Path) event.context());
+// if (Files.isSameFile(stdioPath, path)) {
+// break watch;
+// }
+// }
+// }
+// watchService.close();
}
- watchService.close();
- UnixDomainSocketAddress stdSocketAddress = UnixDomainSocketAddress.of(stdPath);
+ UnixDomainSocketAddress stdioSocketAddress = UnixDomainSocketAddress.of(stdioPath.toRealPath());
- SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX);
- channel.connect(stdSocketAddress);
+ try (SocketChannel stdioChannel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
+ stdioChannel.connect(stdioSocketAddress);
+ stdio.forward(stdioChannel);
+ }
- std.forward(channel);
} catch (IOException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
- try {
- stty(ttyConfig.trim());
- } catch (Exception e) {
- System.err.println("Exception restoring tty config");
- }
+ if (ttyConfig != null)
+ try {
+ stty(ttyConfig.trim());
+ } catch (Exception e) {
+ System.err.println("Exception restoring tty config");
+ }
}
}
private WritableByteChannel outChannel;
private Thread readOutThread;
+ private Thread forwardThread;
public void forward(SocketChannel channel) throws IOException {
+ forwardThread = Thread.currentThread();
readOutThread = new Thread(() -> {
try {
buffer.rewind();
}
System.exit(0);
+ } catch (ClosedByInterruptException e) {
+ // silent
} catch (IOException e) {
e.printStackTrace();
}
while (channel.isConnected()) {
if (inChannel.read(buffer) < 0)
break;
+// int b = (int) buffer.get(0);
+// if (b == 0x1B) {
+// System.out.println("Ctrl+C");
+// }
+
buffer.flip();
channel.write(buffer);
buffer.rewind();
}
}
+ public void shutdown() {
+ try {
+ inChannel.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ try {
+ outChannel.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ forwardThread.interrupt();
+ readOutThread.interrupt();
+ }
+
public void setInputStream(InputStream in) {
inChannel = Channels.newChannel(in);
}
package org.argeo.cms.jshell;
-import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.StandardProtocolFamily;
-import java.net.URI;
import java.net.UnixDomainSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.StringJoiner;
import java.util.UUID;
import java.util.concurrent.Executors;
import org.argeo.api.cms.CmsAuth;
import org.argeo.api.cms.CmsLog;
import org.argeo.cms.util.CurrentSubject;
+import org.argeo.cms.util.FsUtils;
import org.argeo.internal.cms.jshell.osgi.OsgiExecutionControlProvider;
import jdk.jshell.tool.JavaShellToolBuilder;
private UUID uuid;
private Path sessionDir;
-
- private String fromBundle = "eu.netiket.on.apaf.project.togo2023";
+ private Path socketsDir;
private Path stdioPath;
private Path stderrPath;
private LoginContext loginContext;
- LocalJShellSession(Path sessionDir) {
- this.sessionDir = sessionDir;
- this.uuid = UUID.fromString(sessionDir.getFileName().toString());
-
- stdioPath = sessionDir.resolve(JShellClient.STDIO);
+ private Long bundleId;
- // TODO proper login
+ LocalJShellSession(Path sessionDir, Path bundleIdDir) {
try {
- loginContext = new LoginContext(CmsAuth.DATA_ADMIN.getLoginContextName());
- loginContext.login();
- } catch (LoginException e1) {
- throw new RuntimeException("Could not login as data admin", e1);
- } finally {
- }
+ this.sessionDir = sessionDir;
+ this.uuid = UUID.fromString(sessionDir.getFileName().toString());
+ bundleId = Long.parseLong(bundleIdDir.getFileName().toString());
+ socketsDir = bundleIdDir.resolve(uuid.toString());
+ Files.createDirectories(socketsDir);
+
+ stdioPath = socketsDir.resolve(JShellClient.STDIO);
+ Files.createSymbolicLink(sessionDir.resolve(JShellClient.STDIO), stdioPath);
+
+ // TODO proper login
+ try {
+ loginContext = new LoginContext(CmsAuth.DATA_ADMIN.getLoginContextName());
+ loginContext.login();
+ } catch (LoginException e1) {
+ throw new RuntimeException("Could not login as data admin", e1);
+ } finally {
+ }
+ } catch (IOException e) {
+ log.error("Cannot initiate local session " + uuid, e);
+ cleanUp();
+ }
replThread = new Thread(() -> CurrentSubject.callAs(loginContext.getSubject(), Executors.callable(this)),
"JShell " + sessionDir);
replThread.start();
try (SocketChannel channel = stdServerChannel.accept()) {
std.open(channel);
- String frameworkLocation = System.getProperty("osgi.framework");
- StringJoiner classpath = new StringJoiner(File.pathSeparator);
- classpath.add(Paths.get(URI.create(frameworkLocation)).toAbsolutePath().toString());
+// StringJoiner classpath = new StringJoiner(File.pathSeparator);
+// String frameworkLocation = System.getProperty("osgi.framework");
+// classpath.add(Paths.get(URI.create(frameworkLocation)).toAbsolutePath().toString());
ClassLoader cmsJShellBundleCL = OsgiExecutionControlProvider.class.getClassLoader();
ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
//
// START JSHELL
//
- int exitCode = builder.start("--execution", "osgi:bundle(" + fromBundle + ")", "--class-path",
- classpath.toString());
+ int exitCode = builder.start("--execution", "osgi:bundle(" + bundleId + ")", "--class-path",
+ OsgiExecutionControlProvider.getBundleClasspath(bundleId), "--startup",
+ OsgiExecutionControlProvider.getBundleStartupScript(bundleId).toString());
//
log.debug("JShell " + sessionDir + " completed with exit code " + exitCode);
} finally {
void cleanUp() {
try {
- if (Files.exists(stdioPath))
- Files.delete(stdioPath);
+ if (Files.exists(socketsDir))
+ FsUtils.delete(socketsDir);
if (Files.exists(sessionDir))
- Files.delete(sessionDir);
+ FsUtils.delete(sessionDir);
} catch (IOException e) {
log.error("Cannot clean up JShell " + sessionDir, e);
}
package org.argeo.internal.cms.jshell.osgi;
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Set;
+import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.TreeSet;
public ExecutionControl generate(ExecutionEnv env, Map<String, String> parameters) throws Throwable {
// TODO find a better way to get a default bundle context
// NOTE: the related default bundle has to be started
- BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext();
- String symbolicName = parameters.get(BUNDLE_PARAMETER);
+// String symbolicName = parameters.get(BUNDLE_PARAMETER);
+// Bundle fromBundle = getBundleFromSn(symbolicName);
+
+ Long bundleId = Long.parseLong(parameters.get(BUNDLE_PARAMETER));
+ Bundle fromBundle = getBundleFromId(bundleId);
+
+ BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class);
+ ClassLoader fromBundleClassLoader = fromBundleWiring.getClassLoader();
+
+ // use the bundle classloade as context classloader
+ Thread.currentThread().setContextClassLoader(fromBundleClassLoader);
+
+ ExecutionControl executionControl = new DirectExecutionControl(
+ new WrappingLoaderDelegate(fromBundleClassLoader));
+ log.debug("JShell from " + fromBundle.getSymbolicName() + "_" + fromBundle.getVersion() + " ["
+ + fromBundle.getBundleId() + "]");
+ return executionControl;
+ }
+
+ public static Bundle getBundleFromSn(String symbolicName) {
+ BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext();
Objects.requireNonNull(symbolicName);
NavigableMap<Version, Bundle> bundles = new TreeMap<Version, Bundle>();
for (Bundle b : bc.getBundles()) {
if (symbolicName.equals(b.getSymbolicName()))
bundles.put(b.getVersion(), b);
}
+ if (bundles.isEmpty())
+ return null;
Bundle fromBundle = bundles.lastEntry().getValue();
+ return fromBundle;
+ }
+
+ public static Bundle getBundleFromId(Long bundleId) {
+ BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext();
+ Bundle fromBundle = bc.getBundle(bundleId);
+ return fromBundle;
+ }
+
+ public static Path getBundleStartupScript(Long bundleId) {
+ BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext();
+ Bundle fromBundle = bc.getBundle(bundleId);
+ Path bundleStartupScript = fromBundle.getDataFile("BUNDLE.jsh").toPath();
BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class);
ClassLoader fromBundleClassLoader = fromBundleWiring.getClassLoader();
Set<String> packagesToImport = new TreeSet<>();
- Set<Bundle> bundlesToAddToCompileClasspath = new TreeSet<>();
- // from bundle
- bundlesToAddToCompileClasspath.add(fromBundle);
// from bundle packages
for (Package pkg : fromBundleClassLoader.getDefinedPackages()) {
packagesToImport.add(pkg.getName());
}
-// System.out.println(Arrays.asList(fromBundleClassLoader.getDefinedPackages()));
List<BundleWire> bundleWires = fromBundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE);
for (BundleWire bw : bundleWires) {
-// System.out.println(bw.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE));
- bundlesToAddToCompileClasspath.add(bw.getProviderWiring().getBundle());
packagesToImport.add(bw.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE).toString());
}
- log.debug("JShell from " + fromBundle.getSymbolicName() + "_" + fromBundle.getVersion() + " ["
- + fromBundle.getBundleId() + "]");
- log.debug(" required packages " + packagesToImport);
- log.debug(" required bundles " + bundlesToAddToCompileClasspath);
- ExecutionControl executionControl = new DirectExecutionControl(
- new WrappingLoaderDelegate(fromBundleClassLoader));
- return executionControl;
+ try (Writer writer = Files.newBufferedWriter(bundleStartupScript, StandardCharsets.UTF_8)) {
+ for (String p : packagesToImport) {
+ writer.write("import " + p + ".*;\n");
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot writer bundle startup script to " + bundleStartupScript, e);
+ }
+
+ return bundleStartupScript;
}
+ public static String getBundleClasspath(Long bundleId) throws IOException {
+ String framework = System.getProperty("osgi.framework");
+ Path frameworkLocation = Paths.get(URI.create(framework)).toAbsolutePath();
+ BundleContext bc = FrameworkUtil.getBundle(OsgiExecutionControlProvider.class).getBundleContext();
+ Bundle fromBundle = bc.getBundle(bundleId);
+
+ BundleWiring fromBundleWiring = fromBundle.adapt(BundleWiring.class);
+
+ Set<Bundle> bundlesToAddToCompileClasspath = new TreeSet<>();
+
+ // from bundle
+ bundlesToAddToCompileClasspath.add(fromBundle);
+
+ List<BundleWire> bundleWires = fromBundleWiring.getRequiredWires(BundleRevision.PACKAGE_NAMESPACE);
+ for (BundleWire bw : bundleWires) {
+ bundlesToAddToCompileClasspath.add(bw.getProviderWiring().getBundle());
+ }
+
+ StringJoiner classpath = new StringJoiner(File.pathSeparator);
+ bundles: for (Bundle b : bundlesToAddToCompileClasspath) {
+ if (b.getBundleId() == 0) {// system bundle
+ classpath.add(frameworkLocation.toString());
+ continue bundles;
+ }
+ Path p = bundleToPath(frameworkLocation, b);
+ classpath.add(p.toString());
+ }
+
+ return classpath.toString();
+ }
+
+ static Path bundleToPath(Path frameworkLocation, Bundle bundle) throws IOException {
+ String location = bundle.getLocation();
+ if (location.startsWith("initial@reference:file:")) {
+ location = location.substring("initial@reference:file:".length());
+ Path p = frameworkLocation.getParent().resolve(location).toRealPath();
+ // TODO load dev.properties from OSGi configuration directory
+ if (Files.isDirectory(p))
+ p = p.resolve("bin");
+ return p;
+ }
+ Path p = Paths.get(location);
+ return p;
+ }
}