diff --git a/run/src/main/scala/sbt/TrapExit.scala b/run/src/main/scala/sbt/TrapExit.scala index 56980e3f6..ce761b002 100644 --- a/run/src/main/scala/sbt/TrapExit.scala +++ b/run/src/main/scala/sbt/TrapExit.scala @@ -20,14 +20,15 @@ import java.lang.Long.{toHexString => hexL} import TrapExit._ -/** Provides an approximation to isolated execution within a single JVM. +/** Provides an approximation to isolated execution within a single JVM. * System.exit calls are trapped to prevent the JVM from terminating. This is useful for executing * user code that may call System.exit, but actually exiting is undesirable. * * Exit is simulated by disposing all top-level windows and interrupting user-started threads. * Threads are not stopped and shutdown hooks are not called. It is -* therefore inappropriate to use this with code that requires shutdown hooks or creates threads that -* do not terminate. This category of code should only be called by forking a new JVM. */ +* therefore inappropriate to use this with code that requires shutdown hooks, creates threads that +* do not terminate, or if concurrent AWT applications are run. +* This category of code should only be called by forking a new JVM. */ object TrapExit { /** Run `execute` in a managed context, using `log` for debugging messages. @@ -66,6 +67,9 @@ object TrapExit /** `true` if the thread `t` is in the TERMINATED state.x*/ private def isDone(t: Thread): Boolean = t.getState == Thread.State.TERMINATED + private def computeID(g: ThreadGroup): ThreadID = + s"g:${hex(System.identityHashCode(g))}:${g.getName}" + /** Computes an identifier for a Thread that has a high probability of being unique within a single JVM execution. */ private def computeID(t: Thread): ThreadID = // can't use t.getId because when getAccess first sees a Thread, it hasn't been initialized yet @@ -121,14 +125,18 @@ object TrapExit /** Simulates isolation via a SecurityManager. * Multiple applications are supported by tracking Thread constructions via `checkAccess`. * The Thread that constructed each Thread is used to map a new Thread to an application. +* This is not reliable on all jvms, so ThreadGroup creations are also tracked via +* `checkAccess` and traversed on demand to collect threads. * This association of Threads with an application allows properly waiting for -* non-daemon threads to terminate or to interrupt the correct threads when terminating.*/ +* non-daemon threads to terminate or to interrupt the correct threads when terminating. +* It also allows disposing AWT windows if the application created any. +* Only one AWT application is supported at a time, however.*/ private final class TrapExit(delegateManager: SecurityManager) extends SecurityManager { /** Tracks the number of running applications in order to short-cut SecurityManager checks when no applications are active.*/ private[this] val running = new java.util.concurrent.atomic.AtomicInteger - /** Maps a thread to its originating application. The thread is represented by a unique identifier to avoid leaks. */ + /** Maps a thread or thread group to its originating application. The thread is represented by a unique identifier to avoid leaks. */ private[this] val threadToApp = new CMap[ThreadID, App] /** Executes `f` in a managed context. */ @@ -141,8 +149,8 @@ private final class TrapExit(delegateManager: SecurityManager) extends SecurityM private[this] def runManaged0(f: xsbti.F0[Unit], xlog: xsbti.Logger): Int = { val log: Logger = xlog - val app = newApp(f, log) - val executionThread = new Thread(app, "run-main") + val app = new App(f, log) + val executionThread = app.mainThread try { executionThread.start() // thread actually evaluating `f` finish(app, log) @@ -150,7 +158,7 @@ private final class TrapExit(delegateManager: SecurityManager) extends SecurityM catch { case e: InterruptedException => // here, the thread that started the run has been interrupted, not the main thread of the executing code cancel(executionThread, app, log) } - finally app.cleanUp() + finally app.cleanup() } /** Interrupt all threads and indicate failure in the exit code. */ private[this] def cancel(executionThread: Thread, app: App, log: Logger): Int = @@ -192,16 +200,43 @@ private final class TrapExit(delegateManager: SecurityManager) extends SecurityM waitForExit(app) } + /** Gives managed applications a unique ID to use in the IDs of the main thread and thread group. */ + private[this] val nextAppID = new java.util.concurrent.atomic.AtomicLong + private def nextID(): String = nextAppID.getAndIncrement.toHexString + /** Represents an isolated application as simulated by [[TrapExit]]. - * `starterThread` is the user thread that called TrapExit and not the main application thread. * `execute` is the application code to evalute. * `log` is used for debug logging. */ - private final class App(val starterThread: ThreadID, val execute: xsbti.F0[Unit], val log: Logger) extends Runnable + private final class App(val execute: xsbti.F0[Unit], val log: Logger) extends Runnable { - val exitCode = new ExitCode - /** Tracks threads created by this application. To avoid leaks, keys are a unique identifier and values are held via WeakReference. - * A TrieMap supports the necessary concurrent updates and snapshots.*/ + /** Tracks threads and groups created by this application. + * To avoid leaks, keys are a unique identifier and values are held via WeakReference. + * A TrieMap supports the necessary concurrent updates and snapshots. */ private[this] val threads = new TrieMap[ThreadID, WeakReference[Thread]] + private[this] val groups = new TrieMap[ThreadID, WeakReference[ThreadGroup]] + + /** Tracks whether AWT has ever been used in this jvm execution. */ + @volatile + var awtUsed = false + + /** The unique ID of the application. */ + val id = nextID() + + /** The ThreadGroup to use to try to track created threads. */ + val mainGroup: ThreadGroup = new ThreadGroup("run-main-group-" + id) { + private[this] val handler = new LoggingExceptionHandler(log, None) + override def uncaughtException(t: Thread, e: Throwable): Unit = handler.uncaughtException(t, e) + } + val mainThread = new Thread(mainGroup, this, "run-main-" + id) + + /** Saves the ids of the creating thread and thread group to avoid tracking them as coming from this application. */ + val creatorThreadID = computeID(currentThread) + val creatorGroup = currentThread.getThreadGroup + + register(mainThread) + register(mainGroup) + + val exitCode = new ExitCode def run() { try execute() @@ -213,32 +248,73 @@ private final class TrapExit(delegateManager: SecurityManager) extends SecurityM } } + /** Records a new group both in the global [[TrapExit]] manager and for this [[App]].*/ + def register(g: ThreadGroup): Unit = + if(g != null && g != creatorGroup && !isSystemGroup(g)) + { + val groupID = computeID(g) + val old = groups.putIfAbsent(groupID, new WeakReference(g)) + if(old.isEmpty) { // wasn't registered + threadToApp.put(groupID, this) + } + } + /** Records a new thread both in the global [[TrapExit]] manager and for this [[App]]. * Its uncaught exception handler is configured to log exceptions through `log`. */ - def register(t: Thread, threadID: ThreadID): Unit = if(!isDone(t)) { - threadToApp.put(threadID, this) - threads.put(threadID, new WeakReference(t)) - t.setUncaughtExceptionHandler(new LoggingExceptionHandler(log)) + def register(t: Thread): Unit = + { + val threadID = computeID(t) + if(!isDone(t) && threadID != creatorThreadID) { + val old = threads.putIfAbsent(threadID, new WeakReference(t)) + if(old.isEmpty) { // wasn't registered + threadToApp.put(threadID, this) + setExceptionHandler(t) + if(!awtUsed && isEventQueue(t)) + awtUsed = true + } + } } - /** Removes a thread from this [[App]] and the global [[TrapExit]] manager. */ + + /** Registers the logging exception handler on `t`, delegating to the existing handler if it isn't the default. */ + private[this] def setExceptionHandler(t: Thread) + { + val group = t.getThreadGroup + val previousHandler = t.getUncaughtExceptionHandler match { + case null | `group` | (_: LoggingExceptionHandler) => None + case x => Some(x) // delegate to a custom handler only + } + t.setUncaughtExceptionHandler(new LoggingExceptionHandler(log, previousHandler)) + } + + /** Removes a thread or group from this [[App]] and the global [[TrapExit]] manager. */ private[this] def unregister(id: ThreadID): Unit = { threadToApp.remove(id) threads.remove(id) + groups.remove(id) } /** Final cleanup for this application after it has terminated. */ - def cleanUp(): Unit = { - val snap = threads.readOnlySnapshot - threads.clear() + def cleanup(): Unit = { + cleanup(threads) + cleanup(groups) + } + private[this] def cleanup(resources: TrieMap[ThreadID, _]) { + val snap = resources.readOnlySnapshot + resources.clear() for( (id, _) <- snap) unregister(id) - threadToApp.remove(starterThread) } // only want to operate on unterminated threads // want to drop terminated threads, including those that have been gc'd /** Evaluates `f` on each `Thread` started by this [[App]] at single instant shortly after this method is called. */ - def processThreads(f: Thread => Unit) { - for((id, tref) <- threads.readOnlySnapshot) { + def processThreads(f: Thread => Unit) + { + // pulls in threads that weren't recorded by checkAccess(Thread) (which is jvm-dependent) + // but can be reached via the Threads in the ThreadGroups recorded by checkAccess(ThreadGroup) (not jvm-dependent) + addUntrackedThreads() + + val snap = threads.readOnlySnapshot + for((id, tref) <- snap) { val t = tref.get if( (t eq null) || isDone(t)) unregister(id) @@ -249,38 +325,71 @@ private final class TrapExit(delegateManager: SecurityManager) extends SecurityM } } } - } - /** Constructs a new application for `f` that will use `log` for debug logging.*/ - private[this] def newApp(f: xsbti.F0[Unit], log: Logger): App = - { - val threadID = computeID(currentThread) - val a = new App(threadID, f, log) - threadToApp.put(threadID, a) - a + + // registers Threads from the tracked ThreadGroups + private[this] def addUntrackedThreads(): Unit = + groupThreadsSnapshot foreach register + + private[this] def groupThreadsSnapshot: Seq[Thread] = + { + val snap = groups.readOnlySnapshot.values.map(_.get).filter(_ != null) + threadsInGroups(snap.toList, Nil) + } + + // takes a snapshot of the threads in `toProcess`, acquiring nested locks on each group to do so + // the thread groups are accumulated in `accum` and then the threads in each are collected all at + // once while they are all locked. This is the closest thing to a snapshot that can be accomplished. + private[this] def threadsInGroups(toProcess: List[ThreadGroup], accum: List[ThreadGroup]): List[Thread] = toProcess match { + case group :: tail => + // ThreadGroup implementation synchronizes on its methods, so by synchronizing here, we can workaround its quirks somewhat + group.synchronized { + // not tail recursive because of synchronized + threadsInGroups(threadGroups(group) ::: tail, group :: accum) + } + case Nil => accum.flatMap(threads) + } + + // gets the immediate child ThreadGroups of `group` + private[this] def threadGroups(group: ThreadGroup): List[ThreadGroup] = + { + val upperBound = group.activeGroupCount + val groups = new Array[ThreadGroup](upperBound) + val childrenCount = group.enumerate(groups, false) + groups.take(childrenCount).toList + } + + // gets the immediate child Threads of `group` + private[this] def threads(group: ThreadGroup): List[Thread] = + { + val upperBound = group.activeCount + val threads = new Array[Thread](upperBound) + val childrenCount = group.enumerate(threads, false) + threads.take(childrenCount).toList + } } private[this] def stopAllThreads(app: App) { - disposeAllFrames(app) + // only try to dispose frames if we think the App used AWT + // otherwise, we initialize AWT as a side effect of asking for the frames + // also, we only assume one AWT application at a time + if(app.awtUsed) + disposeAllFrames(app.log) interruptAllThreads(app) } private[this] def interruptAllThreads(app: App): Unit = app processThreads { t => if(!isSystemThread(t)) safeInterrupt(t, app.log) else println(s"Not interrupting system thread $t") } - /** Records a thread if it is not already associated with an application. */ - private[this] def recordThread(t: Thread, threadID: ThreadID) - { - val callerID = computeID(Thread.currentThread) - val app = threadToApp.get(callerID) - if(app ne null) - app.register(t, threadID) - } - + /** Gets the managed application associated with Thread `t` */ private[this] def getApp(t: Thread): Option[App] = - Option(threadToApp.get(computeID(t))) + Option( threadToApp.get(computeID(t)) ) orElse getApp(t.getThreadGroup) - /** Handles a valid call to `System.exit` by setting the exit code and + /** Gets the managed application associated with ThreadGroup `group` */ + private[this] def getApp(group: ThreadGroup): Option[App] = + Option(group).flatMap( g => Option( threadToApp.get(computeID(g)) )) + + /** Handles a valid call to `System.exit` by setting the exit code and * interrupting remaining threads for the application associated with `t`, if one exists. */ private[this] def exitApp(t: Thread, status: Int): Unit = getApp(t) match { case None => System.err.println(s"Could not exit($status): no application associated with $t") @@ -313,43 +422,68 @@ private final class TrapExit(delegateManager: SecurityManager) extends SecurityM delegateManager.checkPermission(perm, context) } - /** SecurityManager hook that is abused to record every created Thread and associate it with a managed application. */ - override def checkAccess(t: Thread) { + /** SecurityManager hook that is abused to record every created Thread and associate it with a managed application. + * This is not reliably called on different jvm implementations. On openjdk and similar jvms, the Thread constructor + * calls setPriority, which triggers this SecurityManager check. For Java 6 on OSX, this is not called, however. */ + override def checkAccess(t: Thread) + { if(active) { - val id = computeID(t) - if(threadToApp.get(id) eq null) - recordThread(t, id) + val group = t.getThreadGroup + noteAccess(group) { app => + app.register(group) + app.register(t) + app.register(currentThread) + } } if(delegateManager ne null) delegateManager.checkAccess(t) } + /** This is specified to be called in every Thread's constructor and every time a ThreadGroup is created. + * This allows us to reliably track every ThreadGroup that is created and map it back to the constructing application. */ + override def checkAccess(tg: ThreadGroup) + { + if(active && !isSystemGroup(tg)) { + noteAccess(tg) { app => + app.register(tg) + app.register(currentThread) + } + } + + if(delegateManager ne null) + delegateManager.checkAccess(tg) + } + + private[this] def noteAccess(group: ThreadGroup)(f: App => Unit): Unit = + getApp(currentThread) orElse getApp(group) foreach f + + private[this] def isSystemGroup(group: ThreadGroup): Boolean = + (group != null) && (group.getName == "system") + /** `true` if there is at least one application currently being managed. */ private[this] def active = running.get > 0 - private def disposeAllFrames(app: App) // TODO: allow multiple graphical applications + private def disposeAllFrames(log: Logger) { val allFrames = java.awt.Frame.getFrames if(allFrames.length > 0) { - app.log.debug(s"Disposing ${allFrames.length} top-level windows...") + log.debug(s"Disposing ${allFrames.length} top-level windows...") allFrames.foreach(_.dispose) // dispose all top-level windows, which will cause the AWT-EventQueue-* threads to exit val waitSeconds = 2 - app.log.debug(s"Waiting $waitSeconds s to let AWT thread exit.") + log.debug(s"Waiting $waitSeconds s to let AWT thread exit.") Thread.sleep(waitSeconds * 1000) // AWT Thread doesn't exit immediately, so wait to interrupt it } } - /** Returns true if the given thread is in the 'system' thread group and is an AWT thread other than AWT-EventQueue.*/ + /** Returns true if the given thread is in the 'system' thread group or is an AWT thread other than AWT-EventQueue.*/ private def isSystemThread(t: Thread) = - { - val name = t.getName - if(name.startsWith("AWT-")) - !name.startsWith("AWT-EventQueue") + if(t.getName.startsWith("AWT-")) + !isEventQueue(t) else - { - val group = t.getThreadGroup - (group != null) && (group.getName == "system") - } - } + isSystemGroup(t.getThreadGroup) + + /** An App is identified as using AWT if it gets associated with the event queue thread. + * The event queue thread is not treated as a system thread. */ + private[this] def isEventQueue(t: Thread): Boolean = t.getName.startsWith("AWT-EventQueue") } /** A thread-safe, write-once, optional cell for tracking an application's exit code.*/ @@ -362,11 +496,12 @@ private final class ExitCode /** The default uncaught exception handler for managed executions. * It logs the thread and the exception. */ -private final class LoggingExceptionHandler(log: Logger) extends Thread.UncaughtExceptionHandler +private final class LoggingExceptionHandler(log: Logger, delegate: Option[Thread.UncaughtExceptionHandler]) extends Thread.UncaughtExceptionHandler { def uncaughtException(t: Thread, e: Throwable) { log.error("(" + t.getName + ") " + e.toString) log.trace(e) + delegate.foreach(_.uncaughtException(t, e)) } } diff --git a/src/sphinx/Detailed-Topics/Running-Project-Code.rst b/src/sphinx/Detailed-Topics/Running-Project-Code.rst index 132d6d819..09a0e2df1 100644 --- a/src/sphinx/Detailed-Topics/Running-Project-Code.rst +++ b/src/sphinx/Detailed-Topics/Running-Project-Code.rst @@ -96,3 +96,6 @@ The feature of allowing `System.exit` and multiple threads to be used cannot completely emulate the situation of running in a separate JVM and is intended for development. Program execution should be checked in a :doc:`forked jvm ` when using multiple threads or `System.exit`. + +As of sbt 0.13.1, multiple `run` instances can be managed. There can +only be one application that uses AWT at a time, however.