diff --git a/build.sbt b/build.sbt
index 851694fe4..bc3070129 100644
--- a/build.sbt
+++ b/build.sbt
@@ -680,6 +680,12 @@ lazy val mainProj = (project in file("main"))
exclude[IncompatibleMethTypeProblem]("sbt.Defaults.allTestGroupsTask"),
exclude[DirectMissingMethodProblem]("sbt.StandardMain.shutdownHook"),
exclude[MissingClassProblem]("sbt.internal.ResourceLoaderImpl"),
+ // Removed private internal classes
+ exclude[MissingClassProblem]("sbt.internal.ReverseLookupClassLoaderHolder$BottomClassLoader"),
+ exclude[MissingClassProblem]("sbt.internal.ReverseLookupClassLoaderHolder$ReverseLookupClassLoader$ResourceLoader"),
+ exclude[MissingClassProblem]("sbt.internal.ReverseLookupClassLoaderHolder$ClassLoadingLock"),
+ exclude[MissingClassProblem]("sbt.internal.ReverseLookupClassLoaderHolder$ReverseLookupClassLoader"),
+ exclude[MissingClassProblem]("sbt.internal.LayeredClassLoaderImpl"),
)
)
.configure(
diff --git a/main/src/main/java/sbt/internal/BottomClassLoader.java b/main/src/main/java/sbt/internal/BottomClassLoader.java
new file mode 100644
index 000000000..4607777ee
--- /dev/null
+++ b/main/src/main/java/sbt/internal/BottomClassLoader.java
@@ -0,0 +1,76 @@
+/*
+ * 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.io.IOException;
+import java.net.URL;
+import sbt.util.Logger;
+
+/**
+ * The bottom most layer in our layering hierarchy. This layer should never be cached. The
+ * dependency layer may need access to classes only available at this layer using java reflection.
+ * To make this work, we register this loader with the parent in its constructor. We also add the
+ * lookupClass method which gives ReverseLookupClassLoader a public interface to findClass.
+ *
+ *
To improve performance, when loading classes from the parent, we call the loadClass method
+ * with the reverseLookup flag set to false. This prevents the ReverseLookupClassLoader from trying
+ * to call back into this loader when it can't find a particular class.
+ */
+final class BottomClassLoader extends ManagedClassLoader {
+ private final ReverseLookupClassLoaderHolder holder;
+ private final ReverseLookupClassLoader parent;
+ private final ClassLoadingLock classLoadingLock = new ClassLoadingLock();
+
+ BottomClassLoader(
+ final ReverseLookupClassLoaderHolder holder,
+ final URL[] dynamicClasspath,
+ final ReverseLookupClassLoader reverseLookupClassLoader,
+ final File tempDir,
+ final boolean allowZombies,
+ final Logger logger) {
+ super(dynamicClasspath, reverseLookupClassLoader, allowZombies, logger);
+ setTempDir(tempDir);
+ this.holder = holder;
+ this.parent = reverseLookupClassLoader;
+ parent.setDescendant(this);
+ }
+
+ static {
+ ClassLoader.registerAsParallelCapable();
+ }
+
+ @Override
+ public Class> findClass(final String name) throws ClassNotFoundException {
+ return classLoadingLock.withLock(
+ name,
+ () -> {
+ final Class> prev = findLoadedClass(name);
+ if (prev != null) return prev;
+ return super.findClass(name);
+ });
+ }
+
+ @Override
+ protected Class> loadClass(final String name, final boolean resolve)
+ throws ClassNotFoundException {
+ try {
+ return parent.loadClass(name, resolve, false);
+ } catch (final ClassNotFoundException e) {
+ final Class> clazz = findClass(name);
+ if (resolve) resolveClass(clazz);
+ return clazz;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ holder.checkin(parent);
+ super.close();
+ }
+}
diff --git a/main/src/main/java/sbt/internal/ClassLoadingLock.java b/main/src/main/java/sbt/internal/ClassLoadingLock.java
new file mode 100644
index 000000000..482ec07b7
--- /dev/null
+++ b/main/src/main/java/sbt/internal/ClassLoadingLock.java
@@ -0,0 +1,36 @@
+/*
+ * sbt
+ * Copyright 2011 - 2018, Lightbend, Inc.
+ * Copyright 2008 - 2010, Mark Harrah
+ * Licensed under Apache License 2.0 (see LICENSE)
+ */
+
+package sbt.internal;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+final class ClassLoadingLock {
+ interface ThrowsClassNotFound {
+ R get() throws ClassNotFoundException;
+ }
+
+ private final ConcurrentHashMap locks = new ConcurrentHashMap<>();
+
+ R withLock(final String name, final ThrowsClassNotFound supplier) throws ClassNotFoundException {
+ final Object newLock = new Object();
+ Object prevLock;
+ synchronized (locks) {
+ prevLock = locks.putIfAbsent(name, newLock);
+ }
+ final Object lock = prevLock == null ? newLock : prevLock;
+ try {
+ synchronized (lock) {
+ return supplier.get();
+ }
+ } finally {
+ synchronized (locks) {
+ locks.remove(name);
+ }
+ }
+ }
+}
diff --git a/main/src/main/java/sbt/internal/FlatLoader.java b/main/src/main/java/sbt/internal/FlatLoader.java
index 83fcabe02..519610768 100644
--- a/main/src/main/java/sbt/internal/FlatLoader.java
+++ b/main/src/main/java/sbt/internal/FlatLoader.java
@@ -10,20 +10,20 @@ package sbt.internal;
import java.io.File;
import java.net.URL;
import sbt.util.Logger;
-import scala.collection.Seq;
-final class FlatLoader extends LayeredClassLoaderImpl {
+final class FlatLoader extends ManagedClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
FlatLoader(
- final Seq files,
+ final URL[] urls,
final ClassLoader parent,
final File file,
final boolean allowZombies,
final Logger logger) {
- super(files, parent, file, allowZombies, logger);
+ super(urls, parent, allowZombies, logger);
+ setTempDir(file);
}
@Override
diff --git a/main/src/main/java/sbt/internal/LayeredClassLoader.java b/main/src/main/java/sbt/internal/LayeredClassLoader.java
index 132e7f7cc..088af46d6 100644
--- a/main/src/main/java/sbt/internal/LayeredClassLoader.java
+++ b/main/src/main/java/sbt/internal/LayeredClassLoader.java
@@ -8,13 +8,14 @@
package sbt.internal;
import java.io.File;
+import java.net.URL;
import sbt.util.Logger;
-import scala.collection.Seq;
-final class LayeredClassLoader extends LayeredClassLoaderImpl {
- LayeredClassLoader(final Seq classpath, final ClassLoader parent, final File tempDir, final
+final class LayeredClassLoader extends ManagedClassLoader {
+ LayeredClassLoader(final URL[] classpath, final ClassLoader parent, final File tempDir, final
boolean allowZombies, final Logger logger) {
- super(classpath, parent, tempDir, allowZombies, logger);
+ super(classpath, parent, allowZombies, logger);
+ setTempDir(tempDir);
}
static {
diff --git a/main/src/main/java/sbt/internal/ManagedClassLoader.java b/main/src/main/java/sbt/internal/ManagedClassLoader.java
new file mode 100644
index 000000000..46d160b0d
--- /dev/null
+++ b/main/src/main/java/sbt/internal/ManagedClassLoader.java
@@ -0,0 +1,111 @@
+/*
+ * 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.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import sbt.util.Logger;
+
+abstract class ManagedClassLoader extends URLClassLoader implements NativeLoader {
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+ private final AtomicBoolean printedWarning = new AtomicBoolean(false);
+ private final AtomicReference zombieLoader = new AtomicReference<>();
+ private final boolean allowZombies;
+ private final Logger logger;
+ private final NativeLookup nativeLookup = new NativeLookup();
+
+ static {
+ ClassLoader.registerAsParallelCapable();
+ }
+
+ ManagedClassLoader(
+ final URL[] urls, final ClassLoader parent, final boolean allowZombies, final Logger logger) {
+ super(urls, parent);
+ this.allowZombies = allowZombies;
+ this.logger = logger;
+ }
+
+ private class ZombieClassLoader extends URLClassLoader {
+ private final URL[] urls;
+
+ ZombieClassLoader(URL[] urls) {
+ super(urls, ManagedClassLoader.this);
+ this.urls = urls;
+ }
+
+ Class> lookupClass(final String name) throws ClassNotFoundException {
+ try {
+ return findClass(name);
+ } catch (final ClassNotFoundException e) {
+ final StringBuilder builder = new StringBuilder();
+ for (final URL u : urls) {
+ final File f = new File(u.getPath());
+ if (f.exists()) builder.append(f.toString()).append('\n');
+ }
+ final String deleted = builder.toString();
+ if (!deleted.isEmpty()) {
+ final String msg =
+ "Couldn't load class $name. "
+ + "The following urls on the classpath do not exist:\n"
+ + deleted
+ + "This may be due to shutdown hooks added during an invocation of `run`.";
+ System.err.println(msg);
+ }
+ throw e;
+ }
+ }
+ }
+
+ private ZombieClassLoader getZombieLoader(final String name) {
+ if (printedWarning.compareAndSet(false, true) && !allowZombies) {
+ final String msg =
+ (Thread.currentThread() + " loading " + name + " after test or run ")
+ + "has completed. This is a likely resource leak";
+ logger.warn((Supplier) () -> msg);
+ }
+ final ZombieClassLoader maybeLoader = zombieLoader.get();
+ if (maybeLoader != null) return maybeLoader;
+ else {
+ final ZombieClassLoader zb = new ZombieClassLoader(getURLs());
+ zombieLoader.set(zb);
+ return zb;
+ }
+ }
+
+ @Override
+ public URL findResource(String name) {
+ return closed.get() ? getZombieLoader(name).findResource(name) : super.findResource(name);
+ }
+
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ return closed.get() ? getZombieLoader(name).lookupClass(name) : super.findClass(name);
+ }
+
+ @Override
+ public void close() throws IOException {
+ final ZombieClassLoader zb = zombieLoader.getAndSet(null);
+ if (zb != null) zb.close();
+ if (closed.compareAndSet(false, true)) super.close();
+ }
+
+ @Override
+ public String findLibrary(final String name) {
+ return nativeLookup.findLibrary(name);
+ }
+
+ @Override
+ public void setTempDir(final File file) {
+ nativeLookup.setTempDir(file);
+ }
+}
diff --git a/main/src/main/java/sbt/internal/ReverseLookupClassLoader.java b/main/src/main/java/sbt/internal/ReverseLookupClassLoader.java
new file mode 100644
index 000000000..5a155005c
--- /dev/null
+++ b/main/src/main/java/sbt/internal/ReverseLookupClassLoader.java
@@ -0,0 +1,127 @@
+/*
+ * 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.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import sbt.util.Logger;
+
+/**
+ * A ClassLoader for the dependency layer of a run or test task. It is almost a normal
+ * URLClassLoader except that it has the ability to look one level down the classloading hierarchy
+ * to load a class via reflection that is not directly available to it. The ClassLoader that is
+ * below it in the hierarchy must be registered via setDescendant. If it ever loads a class from its
+ * descendant, then it cannot be used in a subsequent run because it will not be possible to reload
+ * that class.
+ *
+ * The descendant classloader checks it in and out via [[checkout]] and [[checkin]]. When it
+ * returns the loader via [[checkin]], if the loader is dirty, we close it. Otherwise we cache it if
+ * there is no existing cache entry.
+ *
+ *
Performance degrades if loadClass is constantly looking back up to the provided
+ * BottomClassLoader so we provide an alternate loadClass definition that takes a reverseLookup
+ * boolean parameter. Because the [[BottomClassLoader]] knows what loader is calling into, when it
+ * delegates its search to the ReverseLookupClassLoader, it passes false for the reverseLookup flag.
+ * By default the flag is true. Most of the time, the default loadClass will only be invoked by java
+ * reflection calls. Even then, there's some chance that the class being loaded by java reflection
+ * is _not_ available on the bottom classpath so it is not guaranteed that performing the reverse
+ * lookup will invalidate this loader.
+ */
+final class ReverseLookupClassLoader extends ManagedClassLoader {
+ ReverseLookupClassLoader(
+ final URL[] urls, final ClassLoader parent, final boolean allowZombies, final Logger logger) {
+ super(urls, parent, allowZombies, logger);
+ this.parent = parent;
+ }
+
+ private final AtomicReference directDescendant = new AtomicReference<>();
+ private final AtomicBoolean dirty = new AtomicBoolean(false);
+ private final ClassLoadingLock classLoadingLock = new ClassLoadingLock();
+ private final AtomicReference resourceLoader = new AtomicReference<>();
+ private final ClassLoader parent;
+
+ boolean isDirty() {
+ return dirty.get();
+ }
+
+ void setDescendant(final BottomClassLoader bottomClassLoader) {
+ directDescendant.set(bottomClassLoader);
+ }
+
+ private class ResourceLoader extends URLClassLoader {
+ ResourceLoader(final URL[] urls) {
+ super(urls, parent);
+ }
+
+ final URL lookup(final String name) {
+ return findResource(name);
+ }
+ }
+
+ @Override
+ public URL findResource(String name) {
+ final ResourceLoader loader = resourceLoader.get();
+ return loader == null ? null : loader.lookup(name);
+ }
+
+ void setup(final File tmpDir, final URL[] urls) throws IOException {
+ setTempDir(tmpDir);
+ final ResourceLoader previous = resourceLoader.getAndSet(new ResourceLoader(urls));
+ if (previous != null) previous.close();
+ }
+
+ @Override
+ protected Class> loadClass(final String name, final boolean resolve)
+ throws ClassNotFoundException {
+ return loadClass(name, resolve, true);
+ }
+
+ Class> loadClass(final String name, final boolean resolve, final boolean childLookup)
+ throws ClassNotFoundException {
+ Class> result;
+ try {
+ result = parent.loadClass(name);
+ } catch (final ClassNotFoundException e) {
+ result = findClass(name, childLookup);
+ }
+ if (result == null) throw new ClassNotFoundException(name);
+ if (resolve) resolveClass(result);
+ return result;
+ }
+
+ private Class> findClass(final String name, final boolean childLookup)
+ throws ClassNotFoundException {
+ return classLoadingLock.withLock(
+ name,
+ () -> {
+ try {
+ final Class> prev = findLoadedClass(name);
+ if (prev != null) return prev;
+ return findClass(name);
+ } catch (final ClassNotFoundException e) {
+ if (childLookup) {
+ final BottomClassLoader loader = directDescendant.get();
+ if (loader == null) throw e;
+ final Class> clazz = loader.findClass(name);
+ dirty.set(true);
+ return clazz;
+ } else {
+ throw e;
+ }
+ }
+ });
+ }
+
+ static {
+ registerAsParallelCapable();
+ }
+}
diff --git a/main/src/main/scala/sbt/internal/ClassLoaders.scala b/main/src/main/scala/sbt/internal/ClassLoaders.scala
index 1387d4abb..bdf25ebe6 100644
--- a/main/src/main/scala/sbt/internal/ClassLoaders.scala
+++ b/main/src/main/scala/sbt/internal/ClassLoaders.scala
@@ -9,6 +9,7 @@ package sbt
package internal
import java.io.File
+import java.net.URL
import java.nio.file.Path
import sbt.ClassLoaderLayeringStrategy._
@@ -25,6 +26,9 @@ import sbt.nio.Keys._
import sbt.util.Logger
private[sbt] object ClassLoaders {
+ private implicit class SeqFileOps(val files: Seq[File]) extends AnyVal {
+ def urls: Array[URL] = files.toArray.map(_.toURI.toURL)
+ }
private[this] val interfaceLoader = classOf[sbt.testing.Framework].getClassLoader
/*
* Get the class loader for a test task. The configuration could be IntegrationTest or Test.
@@ -136,7 +140,7 @@ private[sbt] object ClassLoaders {
): ClassLoader = {
val cpFiles = fullCP.map(_._1)
strategy match {
- case Flat => new FlatLoader(cpFiles, interfaceLoader, tmp, allowZombies, logger)
+ case Flat => new FlatLoader(cpFiles.urls, interfaceLoader, tmp, allowZombies, logger)
case _ =>
val layerDependencies = strategy match {
case _: AllLibraryJars => true
@@ -194,7 +198,8 @@ private[sbt] object ClassLoaders {
case cl =>
cl.getParent match {
case dl: ReverseLookupClassLoaderHolder => dl.checkout(cpFiles, tmp)
- case _ => new LayeredClassLoader(dynamicClasspath, cl, tmp, allowZombies, logger)
+ case _ =>
+ new LayeredClassLoader(dynamicClasspath.urls, cl, tmp, allowZombies, logger)
}
}
}
diff --git a/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala b/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala
index 5f0da410f..3f50f8c2a 100644
--- a/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala
+++ b/main/src/main/scala/sbt/internal/LayeredClassLoaders.scala
@@ -8,7 +8,7 @@
package sbt.internal
import java.io.File
-import java.net.{ URL, URLClassLoader }
+import java.net.URLClassLoader
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
@@ -17,23 +17,6 @@ import sbt.util.Logger
import scala.collection.JavaConverters._
-/**
- * A simple ClassLoader that copies native libraries to a temporary directory before loading them.
- * Otherwise the same as a normal URLClassLoader.
- * @param classpath the classpath of the url
- * @param parent the parent loader
- * @param tempDir the directory into which native libraries are copied before loading
- */
-private[internal] class LayeredClassLoaderImpl(
- classpath: Seq[File],
- parent: ClassLoader,
- tempDir: File,
- allowZombies: Boolean,
- logger: Logger
-) extends ManagedClassLoader(classpath.toArray.map(_.toURI.toURL), parent, allowZombies, logger) {
- setTempDir(tempDir)
-}
-
/**
* This classloader doesn't load any classes. It is able to create a two layer bundled ClassLoader
* that is able to load the full project classpath. The top layer is able to directly load the
@@ -79,14 +62,21 @@ private[internal] final class ReverseLookupClassLoaderHolder(
throw new IllegalStateException(msg)
}
val reverseLookupClassLoader = cached.getAndSet(null) match {
- case null => new ReverseLookupClassLoader
+ case null => new ReverseLookupClassLoader(urls, parent, allowZombies, logger)
case c => c
}
- reverseLookupClassLoader.setup(tempDir, fullClasspath)
- new BottomClassLoader(fullClasspath, reverseLookupClassLoader, tempDir)
+ reverseLookupClassLoader.setup(tempDir, fullClasspath.map(_.toURI.toURL).toArray)
+ new BottomClassLoader(
+ ReverseLookupClassLoaderHolder.this,
+ fullClasspath.map(_.toURI.toURL).toArray,
+ reverseLookupClassLoader,
+ tempDir,
+ allowZombies,
+ logger
+ )
}
- private def checkin(reverseLookupClassLoader: ReverseLookupClassLoader): Unit = {
+ private[sbt] def checkin(reverseLookupClassLoader: ReverseLookupClassLoader): Unit = {
if (reverseLookupClassLoader.isDirty) reverseLookupClassLoader.close()
else {
if (closed.get()) reverseLookupClassLoader.close()
@@ -97,6 +87,7 @@ private[internal] final class ReverseLookupClassLoaderHolder(
}
}
}
+
override def close(): Unit = {
closed.set(true)
cached.get() match {
@@ -104,149 +95,6 @@ private[internal] final class ReverseLookupClassLoaderHolder(
case c => c.close()
}
}
-
- private class ClassLoadingLock {
- private[this] val locks = new ConcurrentHashMap[String, AnyRef]()
- def withLock[R](name: String)(f: => R): R = {
- val newLock = new AnyRef
- val lock = locks.synchronized(locks.put(name, newLock) match {
- case null => newLock
- case l => l
- })
- try lock.synchronized(f)
- finally locks.synchronized {
- locks.remove(name)
- ()
- }
- }
- }
-
- /**
- * A ClassLoader for the dependency layer of a run or test task. It is almost a normal
- * URLClassLoader except that it has the ability to look one level down the classloading
- * hierarchy to load a class via reflection that is not directly available to it. The ClassLoader
- * that is below it in the hierarchy must be registered via setDescendant. If it ever loads a
- * class from its descendant, then it cannot be used in a subsequent run because it will not be
- * possible to reload that class.
- *
- * The descendant classloader checks it in and out via [[checkout]] and [[checkin]]. When it
- * returns the loader via [[checkin]], if the loader is dirty, we close it. Otherwise we
- * cache it if there is no existing cache entry.
- *
- * Performance degrades if loadClass is constantly looking back up to the provided
- * BottomClassLoader so we provide an alternate loadClass definition that takes a reverseLookup
- * boolean parameter. Because the [[BottomClassLoader]] knows what loader is calling into, when
- * it delegates its search to the ReverseLookupClassLoader, it passes false for the reverseLookup
- * flag. By default the flag is true. Most of the time, the default loadClass will only be
- * invoked by java reflection calls. Even then, there's some chance that the class being loaded
- * by java reflection is _not_ available on the bottom classpath so it is not guaranteed that
- * performing the reverse lookup will invalidate this loader.
- *
- */
- private class ReverseLookupClassLoader
- extends ManagedClassLoader(urls, parent, allowZombies, logger)
- with NativeLoader {
- override def getURLs: Array[URL] = urls
- private[this] val directDescendant: AtomicReference[BottomClassLoader] =
- new AtomicReference
- private[this] val dirty = new AtomicBoolean(false)
- private[this] val classLoadingLock = new ClassLoadingLock
- def isDirty: Boolean = dirty.get()
- def setDescendant(classLoader: BottomClassLoader): Unit = directDescendant.set(classLoader)
- private[this] val resourceLoader = new AtomicReference[ResourceLoader](null)
- private class ResourceLoader(cp: Seq[File])
- extends URLClassLoader(cp.map(_.toURI.toURL).toArray, parent) {
- def lookup(name: String): URL = findResource(name)
- }
- private[ReverseLookupClassLoaderHolder] def setup(tmpDir: File, cp: Seq[File]): Unit = {
- setTempDir(tmpDir)
- resourceLoader.set(new ResourceLoader(cp))
- }
- override def findResource(name: String): URL = resourceLoader.get() match {
- case null => null
- case l => l.lookup(name)
- }
- def loadClass(name: String, resolve: Boolean, reverseLookup: Boolean): Class[_] = {
- classLoadingLock.withLock(name) {
- try super.loadClass(name, resolve)
- catch {
- case e: ClassNotFoundException if reverseLookup =>
- directDescendant.get match {
- case null => throw e
- case cl =>
- val res = cl.lookupClass(name)
- dirty.set(true)
- res
- }
- }
- }
- }
- override def loadClass(name: String, resolve: Boolean): Class[_] =
- loadClass(name, resolve, reverseLookup = true)
- }
-
- /**
- * The bottom most layer in our layering hierarchy. This layer should never be cached. The
- * dependency layer may need access to classes only available at this layer using java
- * reflection. To make this work, we register this loader with the parent in its
- * constructor. We also add the lookupClass method which gives ReverseLookupClassLoader
- * a public interface to findClass.
- *
- * To improve performance, when loading classes from the parent, we call the loadClass
- * method with the reverseLookup flag set to false. This prevents the ReverseLookupClassLoader
- * from trying to call back into this loader when it can't find a particular class.
- *
- * @param dynamicClasspath the classpath for the run or test task excluding the dependency jars
- * @param parent the ReverseLookupClassLoader with which this loader needs to register itself
- * so that reverse lookups required by java reflection will work
- * @param tempDir the temp directory to copy native libraries
- */
- private class BottomClassLoader(
- dynamicClasspath: Seq[File],
- parent: ReverseLookupClassLoader,
- tempDir: File
- ) extends ManagedClassLoader(
- dynamicClasspath.map(_.toURI.toURL).toArray,
- parent,
- allowZombies,
- logger
- )
- with NativeLoader {
- parent.setDescendant(this)
- setTempDir(tempDir)
- val classLoadingLock = new ClassLoadingLock
-
- final def lookupClass(name: String): Class[_] = findClass(name)
-
- override def findClass(name: String): Class[_] = {
- findLoadedClass(name) match {
- case null =>
- classLoadingLock.withLock(name) {
- findLoadedClass(name) match {
- case null => super.findClass(name)
- case c => c
- }
- }
- case c => c
- }
- }
- override def loadClass(name: String, resolve: Boolean): Class[_] = {
- val clazz = findLoadedClass(name) match {
- case null =>
- val c = try parent.loadClass(name, resolve = false, reverseLookup = false)
- catch { case _: ClassNotFoundException => findClass(name) }
- if (resolve) resolveClass(c)
- c
- case c => c
- }
- if (resolve) resolveClass(clazz)
- clazz
- }
- override def close(): Unit = {
- checkin(parent)
- super.close()
- }
- }
}
/**
@@ -255,17 +103,18 @@ private[internal] final class ReverseLookupClassLoaderHolder(
* to be dynamically reset. The explicit mappings feature isn't used by sbt. The dynamic
* temp directory use case is needed in some layered class loading scenarios.
*/
-private trait NativeLoader extends ClassLoader with AutoCloseable {
+private[internal] trait NativeLoader extends AutoCloseable {
+ private[internal] def setTempDir(file: File): Unit = {}
+}
+private[internal] class NativeLookup extends NativeLoader {
private[this] val mapped = new ConcurrentHashMap[String, String]
private[this] val searchPaths =
sys.props.get("java.library.path").map(IO.parseClasspath).getOrElse(Nil)
private[this] val tempDir = new AtomicReference(new File("/dev/null"))
- abstract override def close(): Unit = {
- setTempDir(new File("/dev/null"))
- super.close()
- }
- override protected def findLibrary(name: String): String = synchronized {
+ override def close(): Unit = setTempDir(new File("/dev/null"))
+
+ def findLibrary(name: String): String = synchronized {
mapped.get(name) match {
case null =>
findLibrary0(name) match {
@@ -278,23 +127,28 @@ private trait NativeLoader extends ClassLoader with AutoCloseable {
case n => n
}
}
- private[internal] def setTempDir(file: File): Unit = {
+
+ private[internal] override def setTempDir(file: File): Unit = {
deleteNativeLibs()
tempDir.set(file)
}
+
private[this] def deleteNativeLibs(): Unit = {
mapped.values().forEach(NativeLibs.delete)
mapped.clear()
}
+
private[this] def findLibrary0(name: String): String = {
val mappedName = System.mapLibraryName(name)
val search = searchPaths.toStream flatMap relativeLibrary(mappedName)
search.headOption.map(copy).orNull
}
+
private[this] def relativeLibrary(mappedName: String)(base: File): Seq[File] = {
val f = new File(base, mappedName)
if (f.isFile) f :: Nil else Nil
}
+
private[this] def copy(f: File): String = {
val target = new File(tempDir.get(), f.getName)
IO.copyFile(f, target)
@@ -320,64 +174,3 @@ private[internal] object NativeLibs {
()
}
}
-
-private sealed abstract class ManagedClassLoader(
- urls: Array[URL],
- parent: ClassLoader,
- allowZombies: Boolean,
- logger: Logger
-) extends URLClassLoader(urls, parent)
- with NativeLoader {
- private[this] val closed = new AtomicBoolean(false)
- private[this] val printedWarning = new AtomicBoolean(false)
- private[this] val zombieLoader = new AtomicReference[ZombieClassLoader]
- private class ZombieClassLoader extends URLClassLoader(urls, this) {
- def lookupClass(name: String): Class[_] =
- try findClass(name)
- catch {
- case e: ClassNotFoundException =>
- val deleted = urls.flatMap { u =>
- val f = new File(u.getPath)
- if (f.exists) None else Some(f)
- }
- if (deleted.toSeq.nonEmpty) {
- // TODO - add doc link
- val msg = s"Couldn't load class $name. " +
- s"The following urls on the classpath do not exist:\n${deleted mkString "\n"}\n" +
- "This may be due to shutdown hooks added during an invocation of `run`."
- // logging may be shutdown at this point so we need to print directly to System.err.
- System.err.println(msg)
- }
- throw e
- }
- }
- private def getZombieLoader(name: String): ZombieClassLoader = {
- if (printedWarning.compareAndSet(false, true) && !allowZombies) {
- // TODO - Need to add link to documentation in website
- val thread = Thread.currentThread
- val msg =
- s"$thread loading $name after test or run has completed. This is a likely resource leak."
- logger.warn(msg)
- }
- zombieLoader.get match {
- case null =>
- val zb = new ZombieClassLoader
- zombieLoader.set(zb)
- zb
- case zb => zb
- }
- }
- override def findResource(name: String): URL = {
- if (closed.get) getZombieLoader(name).findResource(name)
- else super.findResource(name)
- }
- override def findClass(name: String): Class[_] = {
- if (closed.get) getZombieLoader(name).lookupClass(name)
- else super.findClass(name)
- }
- override def close(): Unit = {
- closed.set(true)
- Option(zombieLoader.getAndSet(null)).foreach(_.close())
- super.close()
- }
-}
diff --git a/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/Bar.scala b/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/Bar.scala
new file mode 100644
index 000000000..459991fa3
--- /dev/null
+++ b/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/Bar.scala
@@ -0,0 +1,3 @@
+package test
+
+trait Bar
\ No newline at end of file
diff --git a/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/Foo.scala b/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/Foo.scala
index ea880687e..ce6e4e590 100644
--- a/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/Foo.scala
+++ b/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/Foo.scala
@@ -1,6 +1,6 @@
package test
-class Foo extends Serializable {
+class Foo extends Bar with Serializable {
private[this] var value: Int = 0
def getValue(): Int = value
def setValue(newValue: Int): Unit = value = newValue
diff --git a/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/ReflectionTest.scala b/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/ReflectionTest.scala
index a1988034d..73df72562 100644
--- a/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/ReflectionTest.scala
+++ b/sbt/src/sbt-test/classloader-cache/java-serialization/descendant/src/test/scala/test/ReflectionTest.scala
@@ -1,12 +1,32 @@
package test
+import java.util.concurrent.{ CountDownLatch, TimeUnit }
+
import org.scalatest._
class ReflectionTest extends FlatSpec {
- val foo = new Foo
- foo.setValue(3)
- val newFoo = reflection.Reflection.roundTrip(foo)
- assert(newFoo == foo)
- assert(System.identityHashCode(newFoo) != System.identityHashCode(foo))
+ val procs = 2
+ val initLatch = new CountDownLatch(procs)
+ val loader = this.getClass.getClassLoader
+ val latch = new CountDownLatch(procs)
+ (1 to procs).foreach { i =>
+ new Thread() {
+ setDaemon(true)
+ start()
+ override def run(): Unit = {
+ initLatch.countDown()
+ initLatch.await(5, TimeUnit.SECONDS)
+ val className = if (i % 2 == 0) "test.Foo" else "test.Bar"
+ loader.loadClass(className)
+ val foo = new Foo
+ foo.setValue(3)
+ val newFoo = reflection.Reflection.roundTrip(foo)
+ assert(newFoo == foo)
+ assert(System.identityHashCode(newFoo) != System.identityHashCode(foo))
+ latch.countDown()
+ }
+ }
+ }
+ assert(latch.await(5, TimeUnit.SECONDS))
}
diff --git a/sbt/src/sbt-test/classloader-cache/java-serialization/test b/sbt/src/sbt-test/classloader-cache/java-serialization/test
index 78ba31f24..cb8ead9be 100644
--- a/sbt/src/sbt-test/classloader-cache/java-serialization/test
+++ b/sbt/src/sbt-test/classloader-cache/java-serialization/test
@@ -1,12 +1,20 @@
-> set classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars
+> set descendant / Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars
+
+# We run test a number of times to ensure that it doesn't deadlock
+> test
+
+> test
+
+> test
+
+> test
> test
> testOnly
-> set classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ScalaLibrary
+> set descendant / Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.ScalaLibrary
> test
> testOnly
-