From f3c050921a579947a377ba4f71d1e202e7c8862b Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Mon, 18 Nov 2013 17:31:12 -0500 Subject: [PATCH] Fixes #989, #990. TrapExit jvm-independent and awt handling is only done when awt is used. SecurityManager.checkAccess(ThreadGroup) is specified to be called for every Thread creation and every ThreadGroup creation and is therefore jvm-independent. This can be used to get all Threads associated with an application with good enough accuracy. An application will be marked as using AWT if it gets associated with the AWT event queue thread. To avoid unwanted side effects of accidental AWT initialization, TrapExit only tries to dispose frames when an application is so marked. Only one AWT application is supported due to a lack of a way to associate displayed windows with an application. --- run/src/main/scala/sbt/TrapExit.scala | 261 +++++++++++++----- .../Detailed-Topics/Running-Project-Code.rst | 3 + 2 files changed, 201 insertions(+), 63 deletions(-) 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.