/* sbt -- Simple Build Tool
* Copyright 2008 Mark Harrah
*
* Partially based on exit trapping in Nailgun by Pete Kirkham,
* copyright 2004, Martian Software, Inc
* licensed under Apache 2.0 License.
*/package sbt
import scala.collection.Set
import scala.reflect.Manifest
import scala.collection.concurrent.TrieMap
import java.lang.ref.WeakReference
importThread.currentThread
import java.security.Permission
import java.util.concurrent.{ConcurrentHashMap => CMap}import java.lang.Integer.{toHexString => hex}import java.lang.Long.{toHexString => hexL}importTrapExit._
/** 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, 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. */objectTrapExit{/** Run `execute` in a managed context, using `log` for debugging messages.
* `installManager` must be called before calling this method. */defapply(execute: => Unit, log: Logger): Int =
System.getSecurityManagermatch{casem: TrapExit => m.runManaged(Logger.f0(execute), log)case _ => runUnmanaged(execute, log)}/** Installs the SecurityManager that implements the isolation and returns the previously installed SecurityManager, which may be null.
* This method must be called before using `apply`. */definstallManager(): SecurityManager =
System.getSecurityManagermatch{casem: TrapExit => mcasem => System.setSecurityManager(newTrapExit(m)); m}/** Uninstalls the isolation SecurityManager and restores the old security manager. */defuninstallManager(previous: SecurityManager): Unit =
System.setSecurityManager(previous)private[this]defrunUnmanaged(execute: => Unit, log: Logger): Int =
{log.warn("Managed execution not possible: security manager not installed.")try{execute; 0}catch{casee: Exception =>
log.error("Error during execution: "+e.toString)log.trace(e)1}}privatetypeThreadID = String/** `true` if the thread `t` is in the TERMINATED state.x*/privatedefisDone(t: Thread): Boolean = t.getState== Thread.State.TERMINATEDprivatedefcomputeID(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. */privatedefcomputeID(t: Thread): ThreadID =
// can't use t.getId because when getAccess first sees a Thread, it hasn't been initialized yet// can't use t.getName because calling it on AWT thread in certain circumstances generates a segfault (#997):// Apple AWT: +[ThreadUtilities getJNIEnvUncached] attempting to attach current thread after JNFObtainEnv() faileds"${hex(System.identityHashCode(t))}"/** Waits for the given `thread` to terminate. However, if the thread state is NEW, this method returns immediately. */privatedefwaitOnThread(thread: Thread, log: Logger){log.debug("Waiting for thread "+thread.getName+" to terminate.")thread.joinlog.debug("\tThread "+thread.getName+" exited.")}// interrupts the given thread, but first replaces the exception handler so that the InterruptedException is not printedprivatedefsafeInterrupt(thread: Thread, log: Logger){valname = thread.getNamelog.debug("Interrupting thread "+thread.getName)thread.setUncaughtExceptionHandler(newTrapInterrupt(thread.getUncaughtExceptionHandler))thread.interruptlog.debug("\tInterrupted "+thread.getName)}// an uncaught exception handler that swallows InterruptedExceptions and otherwise defers to originalHandlerprivatefinalclassTrapInterrupt(originalHandler: Thread.UncaughtExceptionHandler)extends Thread.UncaughtExceptionHandler{defuncaughtException(thread: Thread, e: Throwable){withCause[InterruptedException, Unit](e){interrupted => ()}{other => originalHandler.uncaughtException(thread, e)}thread.setUncaughtExceptionHandler(originalHandler)}}/** Recurses into the causes of the given exception looking for a cause of type CauseType. If one is found, `withType` is called with that cause.
* If not, `notType` is called with the root cause.*/privatedefwithCause[CauseType <: Throwable, T](e: Throwable)(withType: CauseType => T)(notType: Throwable => T)(implicitmf: Manifest[CauseType]): T =
{valclazz = mf.runtimeClassif(clazz.isInstance(e))withType(e.asInstanceOf[CauseType])else{valcause = e.getCauseif(cause==null)notType(e)elsewithCause(cause)(withType)(notType)(mf)}}}/** 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.
* It also allows disposing AWT windows if the application created any.
* Only one AWT application is supported at a time, however.*/privatefinalclassTrapExit(delegateManager: SecurityManager)extendsSecurityManager{/** Tracks the number of running applications in order to short-cut SecurityManager checks when no applications are active.*/private[this]valrunning = new java.util.concurrent.atomic.AtomicInteger/** Maps a thread or thread group to its originating application. The thread is represented by a unique identifier to avoid leaks. */private[this]valthreadToApp = newCMap[ThreadID, App]/** Executes `f` in a managed context. */defrunManaged(f: xsbti.F0[Unit], xlog: xsbti.Logger): Int =
{val_ = running.incrementAndGet()tryrunManaged0(f, xlog)finallyrunning.decrementAndGet()}private[this]defrunManaged0(f: xsbti.F0[Unit], xlog: xsbti.Logger): Int =
{vallog: Logger = xlogvalapp = newApp(f, log)valexecutionThread = app.mainThreadtry{executionThread.start()// thread actually evaluating `f`finish(app, log)}catch{casee: InterruptedException => // here, the thread that started the run has been interrupted, not the main thread of the executing codecancel(executionThread, app, log)}finallyapp.cleanup()}/** Interrupt all threads and indicate failure in the exit code. */private[this]defcancel(executionThread: Thread, app: App, log: Logger): Int =
{log.warn("Run canceled.")executionThread.interrupt()stopAllThreads(app)1}/** Wait for all non-daemon threads for `app` to exit, for an exception to be thrown in the main thread,
* or for `System.exit` to be called in a thread started by `app`. */private[this]deffinish(app: App, log: Logger): Int =
{log.debug("Waiting for threads to exit or System.exit to be called.")waitForExit(app)log.debug("Interrupting remaining threads (should be all daemons).")stopAllThreads(app)// should only be daemon threads left nowlog.debug("Sandboxed run complete..")app.exitCode.value.getOrElse(0)}// wait for all non-daemon threads to terminateprivate[this]defwaitForExit(app: App){vardaemonsOnly = trueapp.processThreads{thread =>
// check isAlive because calling `join` on a thread that hasn't started returns immediately// and App will only remove threads that have terminated, which will make this method loop continuously// if a thread is created but not startedif(thread.isAlive&&!thread.isDaemon){daemonsOnly = falsewaitOnThread(thread, app.log)}}// processThreads takes a snapshot of the threads at a given moment, so if there were only daemons, the application should shut downif(!daemonsOnly)waitForExit(app)}/** Gives managed applications a unique ID to use in the IDs of the main thread and thread group. */private[this]valnextAppID = new java.util.concurrent.atomic.AtomicLongprivatedefnextID(): String = nextAppID.getAndIncrement.toHexString/** Represents an isolated application as simulated by [[TrapExit]].
* `execute` is the application code to evalute.
* `log` is used for debug logging. */privatefinalclassApp(valexecute: xsbti.F0[Unit], vallog: Logger)extendsRunnable{/** 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]valthreads = newTrieMap[ThreadID, WeakReference[Thread]]private[this]valgroups = newTrieMap[ThreadID, WeakReference[ThreadGroup]]/** Tracks whether AWT has ever been used in this jvm execution. */
@volatile
varawtUsed = false/** The unique ID of the application. */valid = nextID()/** The ThreadGroup to use to try to track created threads. */valmainGroup: ThreadGroup = newThreadGroup("run-main-group-"+id){private[this]valhandler = newLoggingExceptionHandler(log, None)overridedefuncaughtException(t: Thread, e: Throwable): Unit = handler.uncaughtException(t, e)}valmainThread = newThread(mainGroup, this, "run-main-"+id)/** Saves the ids of the creating thread and thread group to avoid tracking them as coming from this application. */valcreatorThreadID = computeID(currentThread)valcreatorGroup = currentThread.getThreadGroupregister(mainThread)register(mainGroup)valexitCode = newExitCodedefrun(){tryexecute()catch{casex: Throwable =>
exitCode.set(1)//exceptions in the main thread cause the exit code to be 1throwx}}/** Records a new group both in the global [[TrapExit]] manager and for this [[App]].*/defregister(g: ThreadGroup): Unit =
if(g!=null&&g!=creatorGroup&&!isSystemGroup(g)){valgroupID = computeID(g)valold = groups.putIfAbsent(groupID, newWeakReference(g))if(old.isEmpty){// wasn't registeredthreadToApp.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`. */defregister(t: Thread): Unit =
{valthreadID = computeID(t)if(!isDone(t)&&threadID!=creatorThreadID){valold = threads.putIfAbsent(threadID, newWeakReference(t))if(old.isEmpty){// wasn't registeredthreadToApp.put(threadID, this)setExceptionHandler(t)if(!awtUsed&&isEventQueue(t))awtUsed = true}}}/** Registers the logging exception handler on `t`, delegating to the existing handler if it isn't the default. */private[this]defsetExceptionHandler(t: Thread){valgroup = t.getThreadGroupvalpreviousHandler = t.getUncaughtExceptionHandlermatch{casenull | `group` | (_: LoggingExceptionHandler) => Nonecasex => Some(x)// delegate to a custom handler only}t.setUncaughtExceptionHandler(newLoggingExceptionHandler(log, previousHandler))}/** Removes a thread or group from this [[App]] and the global [[TrapExit]] manager. */private[this]defunregister(id: ThreadID): Unit = {threadToApp.remove(id)threads.remove(id)groups.remove(id)}/** Final cleanup for this application after it has terminated. */defcleanup(): Unit = {cleanup(threads)cleanup(groups)}private[this]defcleanup(resources: TrieMap[ThreadID, _]){valsnap = resources.readOnlySnapshotresources.clear()for((id, _) <- snap)unregister(id)}// 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. */defprocessThreads(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()valsnap = threads.readOnlySnapshotfor((id, tref) <- snap){valt = tref.getif((teqnull)||isDone(t))unregister(id)else{f(t)if(isDone(t))unregister(id)}}}// registers Threads from the tracked ThreadGroupsprivate[this]defaddUntrackedThreads(): Unit =
groupThreadsSnapshotforeachregisterprivate[this]defgroupThreadsSnapshot: Seq[Thread] =
{valsnap = 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]defthreadsInGroups(toProcess: List[ThreadGroup], accum: List[ThreadGroup]): List[Thread] = toProcessmatch{casegroup :: tail =>
// ThreadGroup implementation synchronizes on its methods, so by synchronizing here, we can workaround its quirks somewhatgroup.synchronized{// not tail recursive because of synchronizedthreadsInGroups(threadGroups(group):::tail, group::accum)}caseNil => accum.flatMap(threads)}// gets the immediate child ThreadGroups of `group`private[this]defthreadGroups(group: ThreadGroup): List[ThreadGroup] =
{valupperBound = group.activeGroupCountvalgroups = newArray[ThreadGroup](upperBound)valchildrenCount = group.enumerate(groups, false)groups.take(childrenCount).toList}// gets the immediate child Threads of `group`private[this]defthreads(group: ThreadGroup): List[Thread] =
{valupperBound = group.activeCountvalthreads = newArray[Thread](upperBound)valchildrenCount = group.enumerate(threads, false)threads.take(childrenCount).toList}}private[this]defstopAllThreads(app: 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 timeif(app.awtUsed)disposeAllFrames(app.log)interruptAllThreads(app)}private[this]definterruptAllThreads(app: App): Unit =
appprocessThreads{t => if(!isSystemThread(t))safeInterrupt(t, app.log)elseapp.log.debug(s"Not interrupting system thread $t")}/** Gets the managed application associated with Thread `t` */private[this]defgetApp(t: Thread): Option[App] =
Option(threadToApp.get(computeID(t)))orElsegetApp(t.getThreadGroup)/** Gets the managed application associated with ThreadGroup `group` */private[this]defgetApp(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]defexitApp(t: Thread, status: Int): Unit = getApp(t)match{caseNone => System.err.println(s"Could not exit($status): no application associated with $t")case Some(a) =>
a.exitCode.set(status)stopAllThreads(a)}/** SecurityManager hook to trap calls to `System.exit` to avoid shutting down the whole JVM.*/overridedefcheckExit(status: Int): Unit = if(active){valt = currentThreadvalstack = t.getStackTraceif(stack==null||stack.exists(isRealExit)){exitApp(t, status)thrownewTrapExitSecurityException(status)}}/** This ensures that only actual calls to exit are trapped and not just calls to check if exit is allowed.*/privatedefisRealExit(element: StackTraceElement): Boolean =
element.getClassName=="java.lang.Runtime"&&element.getMethodName=="exit"// These are overridden to do nothing because there is a substantial filesystem performance penalty// when there is a SecurityManager defined. The default implementations of these construct a// FilePermission, and its initialization involves canonicalization, which is expensive.overridedefcheckRead(file: String){}overridedefcheckRead(file: String, context: AnyRef){}overridedefcheckWrite(file: String){}overridedefcheckDelete(file: String){}overridedefcheckExec(cmd: String){}overridedefcheckPermission(perm: Permission){if(delegateManagernenull)delegateManager.checkPermission(perm)}overridedefcheckPermission(perm: Permission, context: AnyRef){if(delegateManagernenull)delegateManager.checkPermission(perm, context)}/** 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. */overridedefcheckAccess(t: Thread){if(active){valgroup = t.getThreadGroupnoteAccess(group){app =>
app.register(group)app.register(t)app.register(currentThread)}}if(delegateManagernenull)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. */overridedefcheckAccess(tg: ThreadGroup){if(active&&!isSystemGroup(tg)){noteAccess(tg){app =>
app.register(tg)app.register(currentThread)}}if(delegateManagernenull)delegateManager.checkAccess(tg)}private[this]defnoteAccess(group: ThreadGroup)(f: App => Unit): Unit =
getApp(currentThread)orElsegetApp(group)foreachfprivate[this]defisSystemGroup(group: ThreadGroup): Boolean =
(group!=null)&&(group.getName=="system")/** `true` if there is at least one application currently being managed. */private[this]defactive = running.get>0privatedefdisposeAllFrames(log: Logger){valallFrames = java.awt.Frame.getFramesif(allFrames.length>0){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 exitvalwaitSeconds = 2log.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 or is an AWT thread other than AWT-EventQueue.*/privatedefisSystemThread(t: Thread) =
if(t.getName.startsWith("AWT-"))!isEventQueue(t)elseisSystemGroup(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]defisEventQueue(t: Thread): Boolean = t.getName.startsWith("AWT-EventQueue")}/** A thread-safe, write-once, optional cell for tracking an application's exit code.*/privatefinalclassExitCode{privatevarcode: Option[Int] = Nonedefset(c: Int): Unit = synchronized{code = codeorElseSome(c)}defvalue: Option[Int] = synchronized{code}}/** The default uncaught exception handler for managed executions.
* It logs the thread and the exception. */privatefinalclassLoggingExceptionHandler(log: Logger, delegate: Option[Thread.UncaughtExceptionHandler])extends Thread.UncaughtExceptionHandler{defuncaughtException(t: Thread, e: Throwable){log.error("("+t.getName+") "+e.toString)log.trace(e)delegate.foreach(_.uncaughtException(t, e))}}