Merge pull request #5081 from eatkins/close-run

Close in-process classloader only once run has completed
This commit is contained in:
eugene yokota 2019-09-14 21:45:11 -04:00 committed by GitHub
commit 44cc19e66d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 31 deletions

View File

@ -26,6 +26,21 @@ abstract class BackgroundJobService extends Closeable {
start: (Logger, File) => Unit
): JobHandle
/**
* Launch a background job which is a function that runs inside another thread.
* Differs from [[BackgroundJobService.runInBackground]] in that the start task
* provides both an optional classloader and the work to run in the background thread.
* The service should close the classloader when the task completes.
* killing the job will interrupt() the thread. If your thread blocks on a process,
* then you should get an InterruptedException while blocking on the process, and
* then you could process.destroy() for example.
*/
private[sbt] def runInBackgroundWithLoader(spawningTask: ScopedKey[_], state: State)(
start: (Logger, File) => (Option[ClassLoader], () => Unit)
): JobHandle = runInBackground(spawningTask, state) { (logger, file) =>
start(logger, file)._2.apply()
}
/** Same as shutown. */
def close(): Unit

View File

@ -1434,12 +1434,19 @@ object Defaults extends BuildCommon {
val service = bgJobService.value
val (mainClass, args) = parser.parsed
val hashClasspath = (bgHashClasspath in bgRunMain).value
service.runInBackground(resolvedScoped.value, state.value) { (logger, workingDir) =>
val cp =
service.runInBackgroundWithLoader(resolvedScoped.value, state.value) { (logger, workingDir) =>
val files =
if (copyClasspath.value)
service.copyClasspath(products.value, classpath.value, workingDir, hashClasspath)
else classpath.value
scalaRun.value.run(mainClass, data(cp), args, logger).get
val cp = data(files)
scalaRun.value match {
case r: Run =>
val loader = r.newLoader(cp)
(Some(loader), () => r.runWithLoader(loader, cp, mainClass, args, logger).get)
case sr =>
(None, () => sr.run(mainClass, cp, args, logger).get)
}
}
}
}
@ -1457,12 +1464,20 @@ object Defaults extends BuildCommon {
val service = bgJobService.value
val mainClass = mainClassTask.value getOrElse sys.error("No main class detected.")
val hashClasspath = (bgHashClasspath in bgRun).value
service.runInBackground(resolvedScoped.value, state.value) { (logger, workingDir) =>
val cp =
service.runInBackgroundWithLoader(resolvedScoped.value, state.value) { (logger, workingDir) =>
val files =
if (copyClasspath.value)
service.copyClasspath(products.value, classpath.value, workingDir, hashClasspath)
else classpath.value
scalaRun.value.run(mainClass, data(cp), parser.parsed, logger).get
val cp = data(files)
val args = parser.parsed
scalaRun.value match {
case r: Run =>
val loader = r.newLoader(cp)
(Some(loader), () => r.runWithLoader(loader, cp, mainClass, args, logger).get)
case sr =>
(None, () => sr.run(mainClass, cp, args, logger).get)
}
}
}
}

View File

@ -16,6 +16,7 @@ import java.util.concurrent.atomic.AtomicLong
import sbt.Def.{ Classpath, ScopedKey, Setting }
import sbt.Scope.GlobalScope
import sbt.internal.inc.classpath.ClasspathFilter
import sbt.internal.util.{ Attributed, ManagedLogger }
import sbt.io.syntax._
import sbt.io.{ Hash, IO }
@ -148,6 +149,12 @@ private[sbt] abstract class AbstractBackgroundJobService extends BackgroundJobSe
pool.run(this, spawningTask, state)(start)
}
override private[sbt] def runInBackgroundWithLoader(spawningTask: ScopedKey[_], state: State)(
start: (Logger, File) => (Option[ClassLoader], () => Unit)
): JobHandle = {
pool.runWithLoader(this, spawningTask, state)(start)
}
override final def close(): Unit = shutdown()
override def shutdown(): Unit = {
while (jobSet.nonEmpty) {
@ -419,6 +426,20 @@ private[sbt] class BackgroundThreadPool extends java.io.Closeable {
}
}
}
private class BackgroundRunnableWithLoader(
val loader: Option[ClassLoader],
taskName: String,
body: () => Unit
) extends BackgroundRunnable(taskName, body) {
override def awaitTermination(): Unit = {
try super.awaitTermination()
finally loader.foreach {
case ac: AutoCloseable => ac.close()
case cp: ClasspathFilter => cp.close()
case _ =>
}
}
}
def run(manager: AbstractBackgroundJobService, spawningTask: ScopedKey[_], state: State)(
work: (Logger, File) => Unit
@ -433,6 +454,22 @@ private[sbt] class BackgroundThreadPool extends java.io.Closeable {
manager.doRunInBackground(spawningTask, state, start _)
}
private[sbt] def runWithLoader(
manager: AbstractBackgroundJobService,
spawningTask: ScopedKey[_],
state: State
)(
getWork: (Logger, File) => (Option[ClassLoader], () => Unit)
): JobHandle = {
def start(logger: Logger, workingDir: File): BackgroundJob = {
val (loader, work) = getWork(logger, workingDir)
val runnable = new BackgroundRunnableWithLoader(loader, spawningTask.key.label, work)
executor.execute(runnable)
runnable
}
manager.doRunInBackground(spawningTask, state, start _)
}
override def close(): Unit = {
executor.shutdown()
}

View File

@ -10,10 +10,9 @@ package sbt
import java.io.File
import java.lang.reflect.Method
import java.lang.reflect.Modifier.{ isPublic, isStatic }
import java.net.URLClassLoader
import sbt.internal.inc.ScalaInstance
import sbt.internal.inc.classpath.{ ClasspathFilter, ClasspathUtilities }
import sbt.internal.inc.classpath.ClasspathUtilities
import sbt.internal.util.MessageOnlyException
import sbt.io.Path
import sbt.util.Logger
@ -59,17 +58,25 @@ class ForkRun(config: ForkOptions) extends ScalaRun {
private def classpathOption(classpath: Seq[File]) =
"-classpath" :: Path.makeString(classpath) :: Nil
}
class Run(newLoader: Seq[File] => ClassLoader, trapExit: Boolean) extends ScalaRun {
class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolean)
extends ScalaRun {
def this(instance: ScalaInstance, trapExit: Boolean, nativeTmp: File) =
this((cp: Seq[File]) => ClasspathUtilities.makeLoader(cp, instance, nativeTmp), trapExit)
/** Runs the class 'mainClass' using the given classpath and options using the scala runner.*/
def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Try[Unit] = {
private[sbt] def runWithLoader(
loader: ClassLoader,
classpath: Seq[File],
mainClass: String,
options: Seq[String],
log: Logger
): Try[Unit] = {
log.info(s"running $mainClass ${Run.runOptionsStr(options)}")
def execute() =
def execute(): Unit =
try {
run0(mainClass, classpath, options, log)
log.debug(" Classpath:\n\t" + classpath.mkString("\n\t"))
val main = getMainMethod(mainClass, loader)
invokeMain(loader, main, options)
} catch {
case e: java.lang.reflect.InvocationTargetException => throw e.getCause
}
@ -85,24 +92,10 @@ class Run(newLoader: Seq[File] => ClassLoader, trapExit: Boolean) extends ScalaR
if (trapExit) Run.executeTrapExit(execute(), log)
else directExecute()
}
private def run0(
mainClassName: String,
classpath: Seq[File],
options: Seq[String],
log: Logger
): Unit = {
log.debug(" Classpath:\n\t" + classpath.mkString("\n\t"))
val loader = newLoader(classpath)
try {
val main = getMainMethod(mainClassName, loader)
invokeMain(loader, main, options)
} finally {
loader match {
case u: URLClassLoader => u.close()
case c: ClasspathFilter => c.close()
case _ =>
}
}
/** Runs the class 'mainClass' using the given classpath and options using the scala runner.*/
def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Try[Unit] = {
runWithLoader(newLoader(classpath), classpath, mainClass, options, log)
}
private def invokeMain(
loader: ClassLoader,

View File

@ -0,0 +1,10 @@
object Main extends App {
class Foo
new Thread {
override def run(): Unit = {
Thread.sleep(500)
try new Foo
catch { case t: Throwable => sys.exit(1) }
}
}.start()
}

View File

@ -0,0 +1 @@
> run