Can specify JShell bundle
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 8 May 2023 05:41:25 +0000 (07:41 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 8 May 2023 05:41:25 +0000 (07:41 +0200)
org.argeo.cms.jshell/src/org/argeo/cms/jshell/CmsJShell.java
org.argeo.cms.jshell/src/org/argeo/cms/jshell/JShellClient.java
org.argeo.cms.jshell/src/org/argeo/cms/jshell/LocalJShellSession.java
org.argeo.cms.jshell/src/org/argeo/internal/cms/jshell/osgi/OsgiExecutionControlProvider.java

index aec3da1f6fb5a7b1da6151f4455ec2a0aca50bc5..4df9b819e95591fdb8df809e0d2b7426351d6f6b 100644 (file)
@@ -1,6 +1,7 @@
 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;
@@ -16,6 +17,8 @@ import org.argeo.api.cms.CmsLog;
 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);
@@ -26,17 +29,24 @@ public class CmsJShell {
        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);
 
@@ -50,14 +60,34 @@ public class CmsJShell {
 
                                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");
@@ -70,32 +100,14 @@ public class CmsJShell {
                                                                        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();
@@ -103,7 +115,7 @@ public class CmsJShell {
                        } 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);
@@ -113,6 +125,19 @@ public class CmsJShell {
 
        }
 
+       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();
 //
index c86049881e4cebe9e1c1264fa5548aeb9a5f79a9..f458c5c29d4e8559a3824a57596fbedf6c9861ef 100644 (file)
@@ -5,24 +5,23 @@ import java.io.Console;
 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";
@@ -32,6 +31,7 @@ public class JShellClient {
        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();
@@ -42,44 +42,60 @@ public class JShellClient {
                                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");
+                               }
                }
 
        }
@@ -309,8 +325,10 @@ class SocketPipeSource {
        private WritableByteChannel outChannel;
 
        private Thread readOutThread;
+       private Thread forwardThread;
 
        public void forward(SocketChannel channel) throws IOException {
+               forwardThread = Thread.currentThread();
                readOutThread = new Thread(() -> {
 
                        try {
@@ -323,6 +341,8 @@ class SocketPipeSource {
                                        buffer.rewind();
                                }
                                System.exit(0);
+                       } catch (ClosedByInterruptException e) {
+                               // silent
                        } catch (IOException e) {
                                e.printStackTrace();
                        }
@@ -341,6 +361,11 @@ class SocketPipeSource {
                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();
@@ -356,6 +381,21 @@ class SocketPipeSource {
                }
        }
 
+       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);
        }
index 786ee272df87d23bebcb29056c40741be0974ac7..196ee04689c9154e613e303b174c47c2624dda40 100644 (file)
@@ -1,17 +1,13 @@
 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;
 
@@ -21,6 +17,7 @@ import javax.security.auth.login.LoginException;
 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;
@@ -30,8 +27,7 @@ class LocalJShellSession implements Runnable {
 
        private UUID uuid;
        private Path sessionDir;
-
-       private String fromBundle = "eu.netiket.on.apaf.project.togo2023";
+       private Path socketsDir;
 
        private Path stdioPath;
        private Path stderrPath;
@@ -41,21 +37,32 @@ class LocalJShellSession implements Runnable {
 
        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();
@@ -80,9 +87,9 @@ class LocalJShellSession implements Runnable {
                                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();
@@ -93,8 +100,9 @@ class LocalJShellSession implements Runnable {
                                                //
                                                // 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 {
@@ -111,10 +119,10 @@ class LocalJShellSession implements Runnable {
 
        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);
                }
index 673233cdee54dc92f3b5761318f13abc444596af..f785919b2455c6d0ff7c6d00173b24603a750856 100644 (file)
@@ -1,11 +1,20 @@
 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;
 
@@ -46,45 +55,119 @@ public class OsgiExecutionControlProvider implements ExecutionControlProvider {
        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;
+       }
 }