Simplify ClassLoader isolation in launch command

No more SBT-inspired ClasspathFilter. It now just tries to find the ClassLoader that loaded coursier, and use the parent as a base to load the launched application.

Allows to remove the -b option of the bootstrap command, and remove BaseCommand (that was holding options common to all commands).
This commit is contained in:
Alexandre Archambault 2016-01-23 15:42:06 +01:00
parent 3e9a37edc3
commit 92e5917af6
6 changed files with 70 additions and 197 deletions

View File

@ -8,7 +8,6 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.file.Files;
import java.security.CodeSource;
@ -170,8 +169,6 @@ public class Bootstrap {
String mainClass0 = System.getProperty("bootstrap.mainClass");
String jarDir0 = System.getProperty("bootstrap.jarDir");
boolean prependClasspath = Boolean.parseBoolean(System.getProperty("bootstrap.prependClasspath", "false"));
final File jarDir = new File(jarDir0);
if (jarDir.exists()) {
@ -267,7 +264,7 @@ public class Bootstrap {
parentClassLoader = new IsolatedClassLoader(contextURLs, parentClassLoader, new String[]{ isolationID });
}
URLClassLoader classLoader = new URLClassLoader(localURLs.toArray(new URL[localURLs.size()]), parentClassLoader);
ClassLoader classLoader = new BootstrapClassLoader(localURLs.toArray(new URL[localURLs.size()]), parentClassLoader);
Class<?> mainClass = null;
Method mainMethod = null;
@ -288,14 +285,6 @@ public class Bootstrap {
List<String> userArgs0 = new ArrayList<>();
if (prependClasspath) {
for (URL url : localURLs) {
assert url.getProtocol().equals("file");
userArgs0.add("-B");
userArgs0.add(url.getPath());
}
}
for (int i = 0; i < args.length; i++)
userArgs0.add(args[i]);

View File

@ -0,0 +1,25 @@
package coursier;
import java.net.URL;
import java.net.URLClassLoader;
public class BootstrapClassLoader extends URLClassLoader {
public BootstrapClassLoader(
URL[] urls,
ClassLoader parent
) {
super(urls, parent);
}
/**
* Can be called by reflection by launched applications, to find the "main" `ClassLoader`
* that loaded them, and possibly short-circuit it to load other things for example.
*
* The `launch` command of coursier does that.
*/
public boolean isBootstrapLoader() {
return true;
}
}

View File

@ -9,7 +9,10 @@ import java.util.Properties
import java.util.zip.{ ZipEntry, ZipOutputStream, ZipInputStream }
import caseapp.{ HelpMessage => Help, ValueDescription => Value, ExtraName => Short, _ }
import coursier.util.{ Parse, ClasspathFilter }
import coursier.util.Parse
import scala.annotation.tailrec
import scala.language.reflectiveCalls
case class CommonOptions(
@Help("Keep optional dependencies (Maven)")
@ -161,6 +164,32 @@ case class IsolatedLoaderOptions(
}
object Launch {
@tailrec
def mainClassLoader(cl: ClassLoader): Option[ClassLoader] =
if (cl == null)
None
else {
val isMainLoader = try {
val cl0 = cl.asInstanceOf[Object {
def isBootstrapLoader: Boolean
}]
cl0.isBootstrapLoader
} catch {
case e: Exception =>
false
}
if (isMainLoader)
Some(cl)
else
mainClassLoader(cl.getParent)
}
}
case class Launch(
@Short("M")
@Short("main")
@ -188,12 +217,20 @@ case class Launch(
val files0 = helper.fetch(sources = false, javadoc = false)
val contextLoader = Thread.currentThread().getContextClassLoader
val parentLoader0: ClassLoader = new ClasspathFilter(
Thread.currentThread().getContextClassLoader,
Coursier.baseCp.map(new File(_)).toSet,
exclude = true
)
val parentLoader0: ClassLoader = Launch.mainClassLoader(contextLoader)
.flatMap(cl => Option(cl.getParent))
.getOrElse {
if (common.verbose0 >= 0)
Console.err.println(
"Warning: cannot find the main ClassLoader that launched coursier. " +
"Was coursier launched by its main launcher? " +
"The ClassLoader of the application that is about to be launched will be intertwined " +
"with the one of coursier, which may be a problem if their dependencies conflict."
)
contextLoader
}
val (parentLoader, filteredFiles) =
if (isolated.isolated.isEmpty)
@ -307,9 +344,6 @@ case class Bootstrap(
downloadDir: String,
@Short("f")
force: Boolean,
@Help(s"Internal use - prepend base classpath options to arguments (like -B jar1 -B jar2 etc.)")
@Short("b")
prependClasspath: Boolean,
@Help("Set environment variables in the generated launcher. No escaping is done. Value is simply put between quotes in the launcher preamble.")
@Value("key=value")
@Short("P")
@ -440,7 +474,6 @@ case class Bootstrap(
val properties = new Properties()
properties.setProperty("bootstrap.mainClass", mainClass)
properties.setProperty("bootstrap.jarDir", downloadDir)
properties.setProperty("bootstrap.prependClasspath", prependClasspath.toString)
outputZip.putNextEntry(propsEntry)
properties.store(outputZip, "")
@ -487,27 +520,7 @@ case class Bootstrap(
}
case class BaseCommand(
@Hidden
@Short("B")
baseCp: List[String]
) extends Command {
Coursier.baseCp = baseCp
// FIXME Should be in a trait in case-app
override def setCommand(cmd: Option[Either[String, String]]): Unit = {
if (cmd.isEmpty) {
// FIXME Print available commands too?
Console.err.println("Error: no command specified")
sys.exit(255)
}
super.setCommand(cmd)
}
}
object Coursier extends CommandAppOfWithBase[BaseCommand, CoursierCommand] {
object Coursier extends CommandAppOf[CoursierCommand] {
override def appName = "Coursier"
override def progName = "coursier"
private[coursier] var baseCp = Seq.empty[String]
}

View File

@ -1,37 +0,0 @@
package coursier
import java.io.File
import java.net.URLClassLoader
import coursier.util.ClasspathFilter
class ResolutionClassLoader(
val resolution: Resolution,
val artifacts: Seq[(Dependency, Artifact, File)],
parent: ClassLoader
) extends URLClassLoader(
artifacts.map { case (_, _, f) => f.toURI.toURL }.toArray,
parent
) {
/**
* Filtered version of this `ClassLoader`, exposing only `dependencies` and their
* their transitive dependencies, and filtering out the other dependencies from
* `resolution` - for `ClassLoader` isolation.
*
* An application launched by `coursier launch -C` has `ResolutionClassLoader` set as its
* context `ClassLoader` (can be obtain with `Thread.currentThread().getContextClassLoader`).
* If it aims at doing `ClassLoader` isolation, exposing only a dependency `dep` to the isolated
* things, `filter(dep)` provides a `ClassLoader` that loaded `dep` and all its transitive
* dependencies through the same loader as the contextual one, but that "exposes" only
* `dep` and its transitive dependencies, nothing more.
*/
def filter(dependencies: Set[Dependency]): ClassLoader = {
val subRes = resolution.subset(dependencies)
val subArtifacts = subRes.dependencyArtifacts.map { case (_, a) => a }.toSet
val subFiles = artifacts.collect { case (_, a, f) if subArtifacts(a) => f }
new ClasspathFilter(this, subFiles.toSet, exclude = false)
}
}

View File

@ -1,116 +0,0 @@
package coursier.util
// Extracted and adapted from SBT
import java.io.File
import java.net._
/**
* Doesn't load any classes itself, but instead verifies that all classes loaded through `parent`
* come from `classpath`.
*
* If `exclude` is `true`, does the opposite - exclude classes from `classpath`.
*/
class ClasspathFilter(parent: ClassLoader, classpath: Set[File], exclude: Boolean) extends ClassLoader(parent) {
override def toString = s"ClasspathFilter(parent = $parent, cp = $classpath)"
private val directories = classpath.toSeq.filter(_.isDirectory)
private def toFile(url: URL) =
try { new File(url.toURI) }
catch { case _: URISyntaxException => new File(url.getPath) }
private def uriToFile(uriString: String) = {
val uri = new URI(uriString)
assert(uri.getScheme == "file", s"Expected protocol to be 'file' in URI $uri")
if (uri.getAuthority == null)
new File(uri)
else {
/* https://github.com/sbt/sbt/issues/564
* http://blogs.msdn.com/b/ie/archive/2006/12/06/file-uris-in-windows.aspx
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5086147
* The specific problem here is that `uri` will have a defined authority component for UNC names like //foo/bar/some/path.jar
* but the File constructor requires URIs with an undefined authority component.
*/
new File(uri.getSchemeSpecificPart)
}
}
private def urlAsFile(url: URL) =
url.getProtocol match {
case "file" => Some(toFile(url))
case "jar" =>
val path = url.getPath
val end = path.indexOf('!')
Some(uriToFile(if (end < 0) path else path.substring(0, end)))
case _ => None
}
private def baseFileString(baseFile: File) =
Some(baseFile).filter(_.isDirectory).map { d =>
val cp = d.getAbsolutePath.ensuring(_.nonEmpty)
if (cp.last == File.separatorChar) cp else cp + File.separatorChar
}
private def relativize(base: File, file: File) =
baseFileString(base) .flatMap { baseString =>
Some(file.getAbsolutePath).filter(_ startsWith baseString).map(_ substring baseString.length)
}
private def fromClasspath(c: Class[_]): Boolean = {
val codeSource = c.getProtectionDomain.getCodeSource
(codeSource eq null) ||
onClasspath(codeSource.getLocation) ||
// workaround SBT classloader returning the target directory as sourcecode
// make us keep more class than expected
urlAsFile(codeSource.getLocation).exists(_.isDirectory)
}
private val onClasspath: URL => Boolean = {
if (exclude)
src =>
src == null || urlAsFile(src).forall(f =>
!classpath(f) &&
directories.forall(relativize(_, f).isEmpty)
)
else
src =>
src == null || urlAsFile(src).exists(f =>
classpath(f) ||
directories.exists(relativize(_, f).isDefined)
)
}
override def loadClass(className: String, resolve: Boolean): Class[_] = {
val c =
try super.loadClass(className, resolve)
catch {
case e: LinkageError =>
// Happens when trying to derive a shapeless.Generic
// from an Ammonite session launched like
// ./coursier launch com.lihaoyi:ammonite-repl_2.11.7:0.5.2
// For className == "shapeless.GenericMacros",
// the super.loadClass above - which would be filtered out below anyway,
// raises a NoClassDefFoundError.
null
}
if (c != null && fromClasspath(c))
c
else
throw new ClassNotFoundException(className)
}
override def getResource(name: String): URL = {
val res = super.getResource(name)
if (onClasspath(res)) res else null
}
override def getResources(name: String): java.util.Enumeration[URL] = {
import collection.JavaConverters._
val res = super.getResources(name)
if (res == null) null else res.asScala.filter(onClasspath).asJavaEnumeration
}
}

View File

@ -8,7 +8,6 @@ CACHE_VERSION=v1
--no-default \
-r central \
-D "\${user.home}/.coursier/bootstrap/$VERSION" \
-b \
-f -o coursier \
-M coursier.cli.Coursier \
-P coursier.cache="\${user.home}/.coursier/cache/$CACHE_VERSION"