diff --git a/main-command/src/main/java/sbt/internal/classpath/WrappedLoader.java b/main-command/src/main/java/sbt/internal/classpath/WrappedLoader.java new file mode 100644 index 000000000..8bf7b7edc --- /dev/null +++ b/main-command/src/main/java/sbt/internal/classpath/WrappedLoader.java @@ -0,0 +1,34 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.classpath; + +import java.net.URL; +import java.net.URLClassLoader; + +public class WrappedLoader extends URLClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + WrappedLoader(final ClassLoader parent) { + super(new URL[] {}, parent); + } + + @Override + public URL[] getURLs() { + final ClassLoader parent = getParent(); + return (parent instanceof URLClassLoader) + ? ((URLClassLoader) parent).getURLs() + : super.getURLs(); + } + + @Override + public String toString() { + return "WrappedClassLoader(" + getParent() + ")"; + } +} diff --git a/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala b/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala index dee8e88c4..c0683d040 100644 --- a/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala +++ b/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala @@ -10,7 +10,7 @@ package sbt.internal.classpath import java.io.File import java.lang.management.ManagementFactory import java.lang.ref.{ Reference, ReferenceQueue, SoftReference } -import java.net.{ URL, URLClassLoader } +import java.net.URLClassLoader import java.util.concurrent.atomic.AtomicInteger import sbt.internal.inc.classpath.{ @@ -142,14 +142,6 @@ private[sbt] class ClassLoaderCache( private[this] val cleanupThread = new CleanupThread(ClassLoaderCache.threadID.getAndIncrement()) private[this] val lock = new Object - private class WrappedLoader(parent: ClassLoader) extends URLClassLoader(Array.empty, parent) { - // This is to make dotty work which extracts the URLs from the loader - override def getURLs: Array[URL] = parent match { - case u: URLClassLoader => u.getURLs - case _ => Array.empty - } - override def toString: String = s"WrappedLoader($parent)" - } private def close(classLoader: ClassLoader): Unit = classLoader match { case a: AutoCloseable => a.close() case _ => diff --git a/main/src/main/java/sbt/internal/FlatLoader.java b/main/src/main/java/sbt/internal/FlatLoader.java new file mode 100644 index 000000000..e1586d8be --- /dev/null +++ b/main/src/main/java/sbt/internal/FlatLoader.java @@ -0,0 +1,32 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal; + +import java.net.URL; +import java.net.URLClassLoader; + +class FlatLoader extends URLClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + FlatLoader(final URL[] urls, final ClassLoader parent) { + super(urls, parent); + } + + @Override + public String toString() { + final StringBuilder jars = new StringBuilder(); + for (final URL u : getURLs()) { + jars.append(" "); + jars.append(u); + jars.append("\n"); + } + return "FlatLoader(\n parent = " + getParent() + "\n jars = " + jars.toString() + ")"; + } +} diff --git a/main/src/main/java/sbt/internal/LayeredClassLoader.java b/main/src/main/java/sbt/internal/LayeredClassLoader.java new file mode 100644 index 000000000..241f94a05 --- /dev/null +++ b/main/src/main/java/sbt/internal/LayeredClassLoader.java @@ -0,0 +1,26 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal; + +import java.io.File; +import scala.collection.immutable.Map; +import scala.collection.Seq; + +class LayeredClassLoader extends LayeredClassLoaderImpl { + LayeredClassLoader( + final Seq classpath, + final ClassLoader parent, + final Map resources, + final File tempDir) { + super(classpath, parent, resources, tempDir); + } + + static { + ClassLoader.registerAsParallelCapable(); + } +} diff --git a/main/src/main/java/sbt/internal/ResourceLoader.java b/main/src/main/java/sbt/internal/ResourceLoader.java new file mode 100644 index 000000000..b408252d7 --- /dev/null +++ b/main/src/main/java/sbt/internal/ResourceLoader.java @@ -0,0 +1,23 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal; + +import java.io.File; +import scala.collection.immutable.Map; +import scala.collection.Seq; + +class ResourceLoader extends ResourceLoaderImpl { + ResourceLoader( + final Seq classpath, final ClassLoader parent, final Map resources) { + super(classpath, parent, resources); + } + + static { + ClassLoader.registerAsParallelCapable(); + } +} diff --git a/main/src/main/java/sbt/internal/ScalaReflectClassLoader.java b/main/src/main/java/sbt/internal/ScalaReflectClassLoader.java new file mode 100644 index 000000000..b8a3fff17 --- /dev/null +++ b/main/src/main/java/sbt/internal/ScalaReflectClassLoader.java @@ -0,0 +1,29 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal; + +import java.net.URL; +import java.net.URLClassLoader; + +final class ScalaReflectClassLoader extends URLClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + private final URL jar; + + ScalaReflectClassLoader(final URL jar, final ClassLoader parent) { + super(new URL[] {jar}, parent); + this.jar = jar; + } + + @Override + public String toString() { + return "ScalaReflectClassLoader(" + jar + " parent = " + getParent() + ")"; + } +} diff --git a/main/src/main/scala/sbt/internal/ClassLoaders.scala b/main/src/main/scala/sbt/internal/ClassLoaders.scala index 54e3895ad..df02a7f07 100644 --- a/main/src/main/scala/sbt/internal/ClassLoaders.scala +++ b/main/src/main/scala/sbt/internal/ClassLoaders.scala @@ -9,7 +9,6 @@ package sbt package internal import java.io.File -import java.net.URLClassLoader import java.nio.file.Path import sbt.ClassLoaderLayeringStrategy._ @@ -139,17 +138,12 @@ private[sbt] object ClassLoaders { case f if f.getName == "scala-reflect.jar" => si.allJars.find(_.getName == "scala-reflect.jar") }.flatten - class ScalaReflectClassLoader(jar: File) - extends URLClassLoader(Array(jar.toURI.toURL), scalaLibraryLayer) { - override def toString: String = - s"ScalaReflectClassLoader($jar, parent = $scalaLibraryLayer)" - } val scalaReflectLayer = scalaReflectJar .map { file => cache.apply( file -> IO.getModifiedTimeOrZero(file) :: Nil, scalaLibraryLayer, - () => new ScalaReflectClassLoader(file) + () => new ScalaReflectClassLoader(file.toURI.toURL, scalaLibraryLayer) ) } .getOrElse(scalaLibraryLayer) @@ -201,19 +195,6 @@ private[sbt] object ClassLoaders { } else parent } - private class ResourceLoader( - classpath: Seq[File], - parent: ClassLoader, - resources: Map[String, String] - ) extends LayeredClassLoader(classpath, parent, resources, new File("/dev/null")) { - override def findClass(name: String): Class[_] = throw new ClassNotFoundException(name) - override def loadClass(name: String, resolve: Boolean): Class[_] = { - val clazz = parent.loadClass(name) - if (resolve) resolveClass(clazz) - clazz - } - override def toString: String = "ResourceLoader" - } // Creates a one or two layered classloader for the provided classpaths depending on whether // or not the classpath contains any snapshots. If it does, the snapshots are placed in a layer // above the regular jar layer. This allows the snapshot layer to be invalidated without @@ -235,14 +216,9 @@ private[sbt] object ClassLoaders { } else parent } - private[this] class FlatLoader(classpath: Seq[File], parent: ClassLoader) - extends URLClassLoader(classpath.map(_.toURI.toURL).toArray, parent) { - override def toString: String = - s"FlatClassLoader(parent = $interfaceLoader, jars =\n${classpath.mkString("\n")}\n)" - } // helper methods private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader = - new FlatLoader(classpath, parent) + new FlatLoader(classpath.map(_.toURI.toURL).toArray, parent) private[this] def modifiedTimes(stamps: Seq[(Path, FileStamp)]): Seq[(File, Long)] = stamps.map { case (p, LastModified(lm)) => p.toFile -> lm case (p, _) => diff --git a/main/src/main/scala/sbt/internal/LayeredClassLoader.scala b/main/src/main/scala/sbt/internal/LayeredClassLoader.scala deleted file mode 100644 index 095fb42f1..000000000 --- a/main/src/main/scala/sbt/internal/LayeredClassLoader.scala +++ /dev/null @@ -1,112 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal - -import java.io.File -import java.net.URLClassLoader -import java.util.concurrent.ConcurrentHashMap - -import sbt.internal.inc.classpath._ -import sbt.io.IO - -import scala.collection.JavaConverters._ -import scala.collection.mutable.ListBuffer - -private[sbt] class LayeredClassLoader( - classpath: Seq[File], - parent: ClassLoader, - override protected val resources: Map[String, String], - tempDir: File, -) extends URLClassLoader(classpath.toArray.map(_.toURI.toURL), parent) - with RawResources - with NativeCopyLoader - with AutoCloseable { - private[this] val nativeLibs = new java.util.HashSet[File]().asScala - override protected val config = new NativeCopyConfig( - tempDir, - classpath, - IO.parseClasspath(System.getProperty("java.library.path", "")) - ) - override def findLibrary(name: String): String = { - super.findLibrary(name) match { - case null => null - case l => - nativeLibs += new File(l) - l - } - } - - private[this] val loaded = new ConcurrentHashMap[String, Class[_]] - /* - * Override findClass to memoize its result. We need to do this because in loadClass we will - * delegate to findClass if the current LayeredClassLoader cannot load a class but it is a - * descendant of the thread's context class loader and a class loader below it in the layering - * hierarchy is able to load the required class. Unlike loadClass, findClass does not cache - * the result which would make it possible to return multiple versions of the same class. - */ - override def findClass(name: String): Class[_] = loaded.get(name) match { - case null => - val res = super.findClass(name) - loaded.putIfAbsent(name, res) match { - case null => res - case clazz => clazz - } - case c => c - } - override def loadClass(name: String, resolve: Boolean): Class[_] = { - try super.loadClass(name, resolve) - catch { - case e: ClassNotFoundException => - val loaders = new ListBuffer[LayeredClassLoader] - var currentLoader: ClassLoader = Thread.currentThread.getContextClassLoader - do { - currentLoader match { - case cl: LayeredClassLoader if cl != this => loaders.prepend(cl) - case _ => - } - currentLoader = currentLoader.getParent - } while (currentLoader != null && currentLoader != this) - if (currentLoader == this) { - val resourceName = name.replace('.', '/').concat(".class") - loaders - .collectFirst { - case l if l.findResource(resourceName) != null => - val res = l.findClass(name) - if (resolve) l.resolveClass(res) - res - } - .getOrElse(throw e) - } else throw e - } - } - override def close(): Unit = nativeLibs.foreach(NativeLibs.delete) - override def toString: String = s"""LayeredClassLoader( - | classpath = - | ${classpath mkString "\n "} - | parent = - | ${parent.toString.linesIterator.mkString("\n ")} - |)""".stripMargin -} - -private[internal] object NativeLibs { - private[this] val nativeLibs = new java.util.HashSet[File].asScala - ShutdownHooks.add(() => { - nativeLibs.foreach(IO.delete) - IO.deleteIfEmpty(nativeLibs.map(_.getParentFile).toSet) - nativeLibs.clear() - }) - def addNativeLib(lib: String): Unit = { - nativeLibs.add(new File(lib)) - () - } - def delete(file: File): Unit = { - nativeLibs.remove(file) - file.delete() - () - } -} diff --git a/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala b/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala new file mode 100644 index 000000000..d366dc875 --- /dev/null +++ b/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala @@ -0,0 +1,151 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal + +import java.io.File +import java.net.URLClassLoader +import java.util.concurrent.ConcurrentHashMap + +import sbt.internal.inc.classpath._ +import sbt.io.IO + +import scala.collection.JavaConverters._ +import scala.collection.mutable.ListBuffer + +private[sbt] class LayeredClassLoaderImpl( + classpath: Seq[File], + parent: ClassLoader, + override protected val resources: Map[String, String], + tempDir: File, +) extends URLClassLoader(classpath.toArray.map(_.toURI.toURL), parent) + with RawResources + with NativeCopyLoader + with AutoCloseable { + private[this] val nativeLibs = new java.util.HashSet[File]().asScala + override protected val config = new NativeCopyConfig( + tempDir, + classpath, + IO.parseClasspath(System.getProperty("java.library.path", "")) + ) + override def findLibrary(name: String): String = { + super.findLibrary(name) match { + case null => null + case l => + nativeLibs += new File(l) + l + } + } + + private[this] val loaded = new ConcurrentHashMap[String, Class[_]] + private[this] val classLocks = new ConcurrentHashMap[String, AnyRef]() + /* + * Override findClass to both memoize its result and look down the class hierarchy to attempt to + * load a missing class from a descendant loader. If we didn't cache the loaded classes, + * then it would be possible for this class loader to load a different version of the class than + * the descendant, which would likely cause a crash. The search for the class in the descendants + * allows java reflection to work in cases where the class to load via reflection is not directly + * visible to the class that is attempting to load it. + */ + override def findClass(name: String): Class[_] = loaded.get(name) match { + case null => + val newLock = new AnyRef + val lock = classLocks.putIfAbsent(name, newLock) match { + case null => newLock + case l => l + } + lock.synchronized { + try { + val clazz = super.findClass(name) + loaded.putIfAbsent(name, clazz) match { + case null => clazz + case c => c + } + } catch { + case e: ClassNotFoundException => + /* + * If new threads are spawned, they inherit the context class loader of the parent + * This means that if a test or run task spawns background threads to do work, then the + * same context class loader is available on all of the background threads. In the test + * and run tasks, we temporarily set the context class loader of the main sbt thread to + * be the classloader generated by ClassLoaders.getLayers. This creates an environment + * that looks somewhat like a forked jvm with the app classloader set to be the + * generated class loader. If the test or run main changes the thread context class + * loader, this search might fail even if it would have passed on the initial entry + * into the method. Applications generally only modify the context classloader if they + * are manually loading classes. It's likely that if an application generated + * ClassLoader needs access to the classes in the sbt classpath, then it would be using + * the original context class loader as the parent of the new context class loader + * anyway. + * + * If we wanted to make this change permanent so that the user could not + * override the global context classloader, we would possibly need to intercept the + * classloading of java.lang.Thread itself to return a custom Thread class that mirrors + * the java.lang.Thread api, but stores the context class loader in a custom field. + * + */ + var currentLoader: ClassLoader = Thread.currentThread.getContextClassLoader + val loaders = new ListBuffer[LayeredClassLoader] + do { + currentLoader match { + case cl: LayeredClassLoader if cl != this => loaders.prepend(cl) + case _ => + } + currentLoader = currentLoader.getParent + } while (currentLoader != null && currentLoader != this) + if (currentLoader == this && loaders.nonEmpty) { + val resourceName = name.replace('.', '/').concat(".class") + loaders + .collectFirst { + case l if l.findResource(resourceName) != null => l.findClass(name) + } + .getOrElse(throw e) + } else throw e + } + } + case c => c + } + override def close(): Unit = nativeLibs.foreach(NativeLibs.delete) + override def toString: String = s"""LayeredClassLoader( + | classpath = + | ${classpath mkString "\n "} + | parent = + | ${parent.toString.linesIterator.mkString("\n ")} + |)""".stripMargin +} + +private[internal] class ResourceLoaderImpl( + classpath: Seq[File], + parent: ClassLoader, + resources: Map[String, String] +) extends LayeredClassLoader(classpath, parent, resources, new File("/dev/null")) { + override def findClass(name: String): Class[_] = throw new ClassNotFoundException(name) + override def loadClass(name: String, resolve: Boolean): Class[_] = { + val clazz = parent.loadClass(name) + if (resolve) resolveClass(clazz) + clazz + } + override def toString: String = "ResourceLoader" +} + +private[internal] object NativeLibs { + private[this] val nativeLibs = new java.util.HashSet[File].asScala + ShutdownHooks.add(() => { + nativeLibs.foreach(IO.delete) + IO.deleteIfEmpty(nativeLibs.map(_.getParentFile).toSet) + nativeLibs.clear() + }) + def addNativeLib(lib: String): Unit = { + nativeLibs.add(new File(lib)) + () + } + def delete(file: File): Unit = { + nativeLibs.remove(file) + file.delete() + () + } +}