From 77fd840af97975c28dd2f55bc88184a522e76f84 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Mon, 11 Jan 2016 21:20:54 +0100 Subject: [PATCH] Multiple isolation levels support in generated boostraps --- .../src/main/java/coursier/Bootstrap.java | 82 +++++++++++-- .../java/coursier/IsolatedClassLoader.java | 29 +++++ .../scala-2.11/coursier/cli/Coursier.scala | 111 ++++++++++++------ 3 files changed, 177 insertions(+), 45 deletions(-) create mode 100644 bootstrap/src/main/java/coursier/IsolatedClassLoader.java diff --git a/bootstrap/src/main/java/coursier/Bootstrap.java b/bootstrap/src/main/java/coursier/Bootstrap.java index a86b7ef2f..d30c3b1b3 100644 --- a/bootstrap/src/main/java/coursier/Bootstrap.java +++ b/bootstrap/src/main/java/coursier/Bootstrap.java @@ -11,6 +11,8 @@ import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.nio.file.Files; +import java.security.CodeSource; +import java.security.ProtectionDomain; import java.util.*; import java.util.concurrent.*; import java.util.regex.Matcher; @@ -37,14 +39,41 @@ public class Bootstrap { return buffer.toByteArray(); } - static String[] readJarUrls() throws IOException { + final static String defaultURLResource = "bootstrap-jar-urls"; + final static String isolationIDsResource = "bootstrap-isolation-ids"; + + static String[] readStringSequence(String resource) throws IOException { ClassLoader loader = Thread.currentThread().getContextClassLoader(); - InputStream is = loader.getResourceAsStream("bootstrap-jar-urls"); + InputStream is = loader.getResourceAsStream(resource); + if (is == null) + return new String[] {}; byte[] rawContent = readFullySync(is); String content = new String(rawContent, "UTF-8"); + if (content.length() == 0) + return new String[] {}; return content.split("\n"); } + static Map readIsolationContexts(File jarDir, String[] isolationIDs) throws IOException { + final Map perContextURLs = new LinkedHashMap<>(); + + for (String isolationID: isolationIDs) { + String[] contextURLs = readStringSequence("bootstrap-isolation-" + isolationID + "-jar-urls"); + List urls = new ArrayList<>(); + for (String strURL : contextURLs) { + URL url = new URL(strURL); + File local = localFile(jarDir, url); + if (local.exists()) + urls.add(local.toURI().toURL()); + else + System.err.println("Warning: " + local + " not found."); + } + perContextURLs.put(isolationID, urls.toArray(new URL[urls.size()])); + } + + return perContextURLs; + } + final static int concurrentDownloadCount = 6; // http://stackoverflow.com/questions/872272/how-to-reference-another-property-in-java-util-properties/27724276#27724276 @@ -86,6 +115,33 @@ public class Bootstrap { return resolved; } + static String mainJarPath() { + ProtectionDomain protectionDomain = Bootstrap.class.getProtectionDomain(); + if (protectionDomain != null) { + CodeSource source = protectionDomain.getCodeSource(); + if (source != null) { + URL location = source.getLocation(); + if (location != null && location.getProtocol().equals("file")) { + return location.getPath(); + } + } + } + + return ""; + } + + static File localFile(File jarDir, URL url) { + if (url.getProtocol().equals("file")) + return new File(url.getPath()); + + String path = url.getPath(); + int idx = path.lastIndexOf('/'); + // FIXME Add other components in path to prevent conflicts? + String fileName = path.substring(idx + 1); + return new File(jarDir, fileName); + } + + public static void main(String[] args) throws Throwable { ThreadFactory threadFactory = new ThreadFactory() { @@ -100,6 +156,12 @@ public class Bootstrap { ExecutorService pool = Executors.newFixedThreadPool(concurrentDownloadCount, threadFactory); + System.setProperty("coursier.mainJar", mainJarPath()); + + for (int i = 0; i < args.length; i++) { + System.setProperty("coursier.main.arg-" + i, args[i]); + } + Map properties = loadPropertiesMap(Thread.currentThread().getContextClassLoader().getResourceAsStream("bootstrap.properties")); for (Map.Entry ent : properties.entrySet()) { System.setProperty(ent.getKey(), ent.getValue()); @@ -118,7 +180,7 @@ public class Bootstrap { } else if (!jarDir.mkdirs()) System.err.println("Warning: cannot create " + jarDir0 + ", continuing anyway."); - String[] jarStrUrls = readJarUrls(); + String[] jarStrUrls = readStringSequence(defaultURLResource); List errors = new ArrayList<>(); List urls = new ArrayList<>(); @@ -154,11 +216,7 @@ public class Bootstrap { completionService.submit(new Callable() { @Override public URL call() throws Exception { - String path = url0.getPath(); - int idx = path.lastIndexOf('/'); - // FIXME Add other components in path to prevent conflicts? - String fileName = path.substring(idx + 1); - File dest = new File(jarDir, fileName); + File dest = localFile(jarDir, url0); if (!dest.exists()) { System.err.println("Downloading " + url0); @@ -198,9 +256,17 @@ public class Bootstrap { exit("Interrupted"); } + final String[] isolationIDs = readStringSequence(isolationIDsResource); + final Map perIsolationContextURLs = readIsolationContexts(jarDir, isolationIDs); + Thread thread = Thread.currentThread(); ClassLoader parentClassLoader = thread.getContextClassLoader(); + for (String isolationID: isolationIDs) { + URL[] contextURLs = perIsolationContextURLs.get(isolationID); + parentClassLoader = new IsolatedClassLoader(contextURLs, parentClassLoader, new String[]{ isolationID }); + } + URLClassLoader classLoader = new URLClassLoader(localURLs.toArray(new URL[localURLs.size()]), parentClassLoader); Class mainClass = null; diff --git a/bootstrap/src/main/java/coursier/IsolatedClassLoader.java b/bootstrap/src/main/java/coursier/IsolatedClassLoader.java new file mode 100644 index 000000000..2f4516bba --- /dev/null +++ b/bootstrap/src/main/java/coursier/IsolatedClassLoader.java @@ -0,0 +1,29 @@ +package coursier; + +import java.net.URL; +import java.net.URLClassLoader; + +public class IsolatedClassLoader extends URLClassLoader { + + private String[] isolationTargets; + + public IsolatedClassLoader( + URL[] urls, + ClassLoader parent, + String[] isolationTargets + ) { + super(urls, parent); + this.isolationTargets = isolationTargets; + } + + /** + * Applications wanting to access an isolated `ClassLoader` should inspect the hierarchy of + * loaders, and look into each of them for this method, by reflection. Then they should + * call it (still by reflection), and look for an agreed in advance target in it. If it is found, + * then the corresponding `ClassLoader` is the one with isolated dependencies. + */ + public String[] getIsolationTargets() { + return isolationTargets; + } + +} diff --git a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala index 28483404f..af38c3003 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Coursier.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Coursier.scala @@ -97,30 +97,18 @@ case class Fetch( } -case class Launch( - @Short("M") - @Short("main") - mainClass: String, +case class IsolatedLoaderOptions( @Value("target:dependency") @Short("I") isolated: List[String], @Help("Comma-separated isolation targets") @Short("i") - isolateTarget: List[String], - @Recurse - common: CommonOptions -) extends CoursierCommand { + isolateTarget: List[String] +) { - val (rawDependencies, extraArgs) = { - val idxOpt = Some(remainingArgs.indexOf("--")).filter(_ >= 0) - idxOpt.fold((remainingArgs, Seq.empty[String])) { idx => - val (l, r) = remainingArgs.splitAt(idx) - assert(r.nonEmpty) - (l, r.tail) - } - } + def anyIsolatedDep = isolateTarget.nonEmpty || isolated.nonEmpty - val isolateTargets = { + lazy val targets = { val l = isolateTarget.flatMap(_.split(',')).filter(_.nonEmpty) val (invalid, valid) = l.partition(_.contains(":")) if (invalid.nonEmpty) { @@ -135,21 +123,23 @@ case class Launch( valid.toArray } - val (validIsolated, unrecognizedIsolated) = isolated.partition(s => isolateTargets.exists(t => s.startsWith(t + ":"))) + lazy val (validIsolated, unrecognizedIsolated) = isolated.partition(s => targets.exists(t => s.startsWith(t + ":"))) - if (unrecognizedIsolated.nonEmpty) { - Console.err.println(s"Unrecognized isolation targets in:") - for (i <- unrecognizedIsolated) - Console.err.println(s" $i") - sys.exit(255) + def check() = { + if (unrecognizedIsolated.nonEmpty) { + Console.err.println(s"Unrecognized isolation targets in:") + for (i <- unrecognizedIsolated) + Console.err.println(s" $i") + sys.exit(255) + } } - val rawIsolated = validIsolated.map { s => + lazy val rawIsolated = validIsolated.map { s => val Array(target, dep) = s.split(":", 2) target -> dep } - val isolatedModuleVersions = rawIsolated.groupBy { case (t, _) => t }.map { + lazy val isolatedModuleVersions = rawIsolated.groupBy { case (t, _) => t }.map { case (t, l) => val (errors, modVers) = Parse.moduleVersions(l.map { case (_, d) => d }) @@ -161,7 +151,7 @@ case class Launch( t -> modVers } - val isolatedDeps = isolatedModuleVersions.map { + lazy val isolatedDeps = isolatedModuleVersions.map { case (t, l) => t -> l.map { case (mod, ver) => @@ -169,9 +159,30 @@ case class Launch( } } +} + +case class Launch( + @Short("M") + @Short("main") + mainClass: String, + @Recurse + isolated: IsolatedLoaderOptions, + @Recurse + common: CommonOptions +) extends CoursierCommand { + + val (rawDependencies, extraArgs) = { + val idxOpt = Some(remainingArgs.indexOf("--")).filter(_ >= 0) + idxOpt.fold((remainingArgs, Seq.empty[String])) { idx => + val (l, r) = remainingArgs.splitAt(idx) + assert(r.nonEmpty) + (l, r.tail) + } + } + val helper = new Helper( common.copy(forceVersion = common.forceVersion), - rawDependencies ++ rawIsolated.map { case (_, dep) => dep } + rawDependencies ++ isolated.rawIsolated.map { case (_, dep) => dep } ) @@ -185,17 +196,17 @@ case class Launch( ) val (parentLoader, filteredFiles) = - if (isolated.isEmpty) + if (isolated.isolated.isEmpty) (parentLoader0, files0) else { - val (isolatedLoader, filteredFiles0) = isolateTargets.foldLeft((parentLoader0, files0)) { + val (isolatedLoader, filteredFiles0) = isolated.targets.foldLeft((parentLoader0, files0)) { case ((parent, files0), target) => // FIXME These were already fetched above val isolatedFiles = helper.fetch( sources = false, javadoc = false, - subset = isolatedDeps.getOrElse(target, Seq.empty).toSet + subset = isolated.isolatedDeps.getOrElse(target, Seq.empty).toSet ) if (common.verbose0 >= 1) { @@ -303,6 +314,8 @@ case class Bootstrap( @Value("key=value") @Short("P") property: List[String], + @Recurse + isolated: IsolatedLoaderOptions, @Recurse common: CommonOptions ) extends CoursierCommand { @@ -368,9 +381,20 @@ case class Bootstrap( val helper = new Helper(common, remainingArgs) - val artifacts = helper.res.artifacts + val urls = helper.res.artifacts.map(_.url) - val urls = artifacts.map(_.url) + val (_, isolatedUrls) = + isolated.targets.foldLeft((Vector.empty[String], Map.empty[String, Seq[String]])) { + case ((done, acc), target) => + val subRes = helper.res.subset(isolated.isolatedDeps.getOrElse(target, Nil).toSet) + val subUrls = subRes.artifacts.map(_.url) + + val filteredSubUrls = subUrls.diff(done) + + val updatedAcc = acc + (target -> filteredSubUrls) + + (done ++ filteredSubUrls, updatedAcc) + } val unrecognized = urls.filter(s => !s.startsWith("http://") && !s.startsWith("https://")) if (unrecognized.nonEmpty) @@ -390,12 +414,25 @@ case class Bootstrap( val time = System.currentTimeMillis() - val jarListEntry = new ZipEntry("bootstrap-jar-urls") - jarListEntry.setTime(time) + def putStringEntry(name: String, content: String): Unit = { + val entry = new ZipEntry(name) + entry.setTime(time) - outputZip.putNextEntry(jarListEntry) - outputZip.write(urls.mkString("\n").getBytes("UTF-8")) - outputZip.closeEntry() + outputZip.putNextEntry(entry) + outputZip.write(content.getBytes("UTF-8")) + outputZip.closeEntry() + } + + putStringEntry("bootstrap-jar-urls", urls.mkString("\n")) + + if (isolated.anyIsolatedDep) { + putStringEntry("bootstrap-isolation-ids", isolated.targets.mkString("\n")) + + for (target <- isolated.targets) { + val urls = isolatedUrls.getOrElse(target, Nil) + putStringEntry(s"bootstrap-isolation-$target-jar-urls", urls.mkString("\n")) + } + } val propsEntry = new ZipEntry("bootstrap.properties") propsEntry.setTime(time)