Merge pull request #6665 from eed3si9n/wip/drop_trapexit

Drop TrapExit
This commit is contained in:
eugene yokota 2021-09-19 13:25:15 -04:00 committed by GitHub
commit ce3015a2bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 41 additions and 536 deletions

View File

@ -598,6 +598,9 @@ lazy val runProj = (project in file("run"))
exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#CustomOutput.copy$default$*"),
exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#LoggedOutput.copy"),
exclude[DirectMissingMethodProblem]("sbt.OutputStrategy#LoggedOutput.copy$default$*"),
exclude[Problem]("sbt.TrapExit*"),
exclude[MissingClassProblem]("sbt.ExitCode"), // private
exclude[MissingClassProblem]("sbt.LoggingExceptionHandler"), // private
)
)
.configure(addSbtIO, addSbtCompilerClasspath)

View File

@ -71,7 +71,7 @@ final class Console(compiler: AnalyzingCompiler) {
terminal.withRawOutput {
jline.TerminalFactory.set(terminal.toJLine)
DeprecatedJLine.setTerminalOverride(jline3term)
terminal.withRawInput(Run.executeTrapExit(console0, log))
terminal.withRawInput(Run.executeSuccess(console0))
}
} finally {
sys.props("scala.color") = previous

View File

@ -281,7 +281,7 @@ object Keys {
val runMain = inputKey[Unit]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask)
val discoveredMainClasses = taskKey[Seq[String]]("Auto-detects main classes.").withRank(BMinusTask)
val runner = taskKey[ScalaRun]("Implementation used to run a main class.").withRank(DTask)
val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This is currently only suitable for serially-executed 'run'-like tasks.").withRank(CSetting)
val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This was removed in sbt 1.6.0 due to JDK 17 deprecating Security Manager.").withRank(CSetting)
val fork = settingKey[Boolean]("If true, forks a new JVM when running. If false, runs in the same JVM as the build.").withRank(ASetting)
val forkOptions = taskKey[ForkOptions]("Configures JVM forking.").withRank(DSetting)

View File

@ -210,21 +210,16 @@ object StandardMain {
private[this] val isShutdown = new AtomicBoolean(false)
def runManaged(s: State): xsbti.MainResult = {
val previous = TrapExit.installManager()
val hook = ShutdownHooks.add(closeRunnable)
try {
val hook = ShutdownHooks.add(closeRunnable)
try {
MainLoop.runLogged(s)
} catch {
case _: InterruptedException if isShutdown.get =>
new xsbti.Exit { override def code(): Int = 0 }
} finally {
try DefaultBackgroundJobService.shutdown()
finally hook.close()
()
}
MainLoop.runLogged(s)
} catch {
case _: InterruptedException if isShutdown.get =>
new xsbti.Exit { override def code(): Int = 0 }
} finally {
TrapExit.uninstallManager(previous)
try DefaultBackgroundJobService.shutdown()
finally hook.close()
()
}
}

View File

@ -58,6 +58,7 @@ class ForkRun(config: ForkOptions) extends ScalaRun {
private def classpathOption(classpath: Seq[File]) =
"-classpath" :: Path.makeString(classpath) :: Nil
}
class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolean)
extends ScalaRun {
def this(instance: ScalaInstance, trapExit: Boolean, nativeTmp: File) =
@ -105,9 +106,8 @@ class Run(private[sbt] val newLoader: Seq[File] => ClassLoader, trapExit: Boolea
// log.trace(e)
throw e
}
// try { execute(); None } catch { case e: Exception => log.trace(e); Some(e.toString) }
if (trapExit) Run.executeTrapExit(execute(), log)
if (trapExit) Run.executeSuccess(execute())
else directExecute()
}
@ -169,12 +169,12 @@ object Run {
runner.run(mainClass, classpath, options, log)
/** Executes the given function, trapping calls to System.exit. */
def executeTrapExit(f: => Unit, log: Logger): Try[Unit] = {
val exitCode = TrapExit(f, log)
if (exitCode == 0) {
log.debug("Exited with code 0")
Success(())
} else Failure(new MessageOnlyException("Nonzero exit code: " + exitCode))
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def executeTrapExit(f: => Unit, log: Logger): Try[Unit] = executeSuccess(f)
private[sbt] def executeSuccess(f: => Unit): Try[Unit] = {
f
Success(())
}
// quotes the option that includes a whitespace

View File

@ -7,20 +7,7 @@
package sbt
import scala.annotation.nowarn
import scala.reflect.Manifest
import scala.collection.concurrent.TrieMap
import java.lang.ref.WeakReference
import Thread.currentThread
import java.security.Permission
import java.util.concurrent.{ ConcurrentHashMap => CMap }
import java.lang.Integer.{ toHexString => hex }
import java.util.function.Supplier
import sbt.util.Logger
import sbt.util.InterfaceUtil
import sbt.internal.util.Util.{ AnyOps, none }
import TrapExit._
/**
* Provides an approximation to isolated execution within a single JVM.
@ -33,32 +20,27 @@ import TrapExit._
* do not terminate, or if concurrent AWT applications are run.
* This category of code should only be called by forking a new JVM.
*/
@nowarn
object TrapExit {
/**
* Run `execute` in a managed context, using `log` for debugging messages.
* `installManager` must be called before calling this method.
*/
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def apply(execute: => Unit, log: Logger): Int =
System.getSecurityManager match {
case m: TrapExit => m.runManaged(InterfaceUtil.toSupplier(execute), log)
case _ => runUnmanaged(execute, log)
}
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`.
*/
def installManager(): SecurityManager =
System.getSecurityManager match {
case m: TrapExit => m
case m => System.setSecurityManager(new TrapExit(m)); m
}
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def installManager(): Nothing =
sys.error("TrapExit feature is removed due to JDK 17 deprecating SecurityManager")
/** Uninstalls the isolation SecurityManager and restores the old security manager. */
def uninstallManager(previous: SecurityManager): Unit =
System.setSecurityManager(previous)
@deprecated("TrapExit feature is removed; just call the function instead", "1.6.0")
def uninstallManager(previous: Any): Unit = ()
private[this] def runUnmanaged(execute: => Unit, log: Logger): Int = {
log.warn("Managed execution not possible: security manager not installed.")
@ -71,474 +53,4 @@ object TrapExit {
1
}
}
private type ThreadID = String
/** `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
// 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() failed
s"${hex(System.identityHashCode(t))}"
/** Waits for the given `thread` to terminate. However, if the thread state is NEW, this method returns immediately. */
private def waitOnThread(thread: Thread, log: Logger): Unit = {
log.debug("Waiting for thread " + thread.getName + " to terminate.")
thread.join
log.debug("\tThread " + thread.getName + " exited.")
}
// interrupts the given thread, but first replaces the exception handler so that the InterruptedException is not printed
private def safeInterrupt(thread: Thread, log: Logger): Unit = {
log.debug("Interrupting thread " + thread.getName)
thread.setUncaughtExceptionHandler(new TrapInterrupt(thread.getUncaughtExceptionHandler))
thread.interrupt
log.debug("\tInterrupted " + thread.getName)
}
// an uncaught exception handler that swallows InterruptedExceptions and otherwise defers to originalHandler
private final class TrapInterrupt(originalHandler: Thread.UncaughtExceptionHandler)
extends Thread.UncaughtExceptionHandler {
def uncaughtException(thread: Thread, e: Throwable): Unit = {
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.
*/
private def withCause[CauseType <: Throwable, T](
e: Throwable
)(withType: CauseType => T)(notType: Throwable => T)(implicit mf: Manifest[CauseType]): T = {
val clazz = mf.runtimeClass
if (clazz.isInstance(e))
withType(e.asInstanceOf[CauseType])
else {
val cause = e.getCause
if (cause == null)
notType(e)
else
withCause(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.
*/
@nowarn
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 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. */
def runManaged(f: Supplier[Unit], xlog: xsbti.Logger): Int = {
val _ = running.incrementAndGet()
try runManaged0(f, xlog)
finally {
running.decrementAndGet(); ()
}
}
private[this] def runManaged0(f: Supplier[Unit], xlog: xsbti.Logger): Int = {
val log: Logger = xlog
val app = new App(f, log)
val executionThread = app.mainThread
try {
executionThread.start() // thread actually evaluating `f`
finish(app, log)
} catch {
case _: 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()
}
/** Interrupt all threads and indicate failure in the exit code. */
private[this] def cancel(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] def finish(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 now
log.debug("Sandboxed run complete..")
app.exitCode.value.getOrElse(0)
}
// wait for all non-daemon threads to terminate
private[this] def waitForExit(app: App): Unit = {
var daemonsOnly = true
app.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 started
if (thread.isAlive && !thread.isDaemon) {
daemonsOnly = false
waitOnThread(thread, app.log)
}
}
// processThreads takes a snapshot of the threads at a given moment, so if there were only daemons, the application should shut down
if (!daemonsOnly)
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]].
* `execute` is the application code to evaluate.
* `log` is used for debug logging.
*/
private final class App(val execute: Supplier[Unit], val log: Logger) extends Runnable {
/**
* 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(): Unit = {
try execute.get()
catch {
case x: Throwable =>
exitCode.set(1) //exceptions in the main thread cause the exit code to be 1
throw x
}
}
/** 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): 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
}
}
}
/** Registers the logging exception handler on `t`, delegating to the existing handler if it isn't the default. */
private[this] def setExceptionHandler(t: Thread): Unit = {
val group = t.getThreadGroup
val previousHandler = t.getUncaughtExceptionHandler match {
case null | `group` | (_: LoggingExceptionHandler) => none[Thread.UncaughtExceptionHandler]
case x => x.some // 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 = {
cleanup(threads)
cleanup(groups)
}
private[this] def cleanup(resources: TrieMap[ThreadID, _]): Unit = {
val snap = resources.readOnlySnapshot
resources.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. */
def processThreads(f: Thread => Unit): 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)
else {
f(t)
if (isDone(t))
unregister(id)
}
}
}
// 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): Unit = {
// 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 app.log.debug(s"Not interrupting system thread $t")
}
/** Gets the managed application associated with Thread `t` */
private[this] def getApp(t: Thread): Option[App] =
Option(threadToApp.get(computeID(t))) orElse getApp(t.getThreadGroup)
/** 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")
case Some(a) =>
a.exitCode.set(status)
stopAllThreads(a)
}
/** SecurityManager hook to trap calls to `System.exit` to avoid shutting down the whole JVM.*/
override def checkExit(status: Int): Unit = if (active) {
val t = currentThread
val stack = t.getStackTrace
if (stack == null || stack.exists(isRealExit)) {
exitApp(t, status)
throw new TrapExitSecurityException(status)
}
}
/** This ensures that only actual calls to exit are trapped and not just calls to check if exit is allowed.*/
private def isRealExit(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.
override def checkRead(file: String): Unit = ()
override def checkRead(file: String, context: AnyRef): Unit = ()
override def checkWrite(file: String): Unit = ()
override def checkDelete(file: String): Unit = ()
override def checkExec(cmd: String): Unit = ()
override def checkPermission(perm: Permission): Unit = {
if (delegateManager ne null)
delegateManager.checkPermission(perm)
}
override def checkPermission(perm: Permission, context: AnyRef): Unit = {
if (delegateManager ne null)
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.
*/
override def checkAccess(t: Thread): Unit = {
if (active) {
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): Unit = {
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(log: Logger): Unit = {
val allFrames = java.awt.Frame.getFrames
if (allFrames.nonEmpty) {
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
log.debug(s"Waiting $waitSeconds s to let AWT thread exit.")
Thread.sleep(waitSeconds * 1000L) // 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.*/
private def isSystemThread(t: Thread) =
if (t.getName.startsWith("AWT-"))
!isEventQueue(t)
else
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.*/
private final class ExitCode {
private var code: Option[Int] = None
def set(c: Int): Unit = synchronized { code = code orElse Some(c) }
def value: Option[Int] = synchronized { code }
}
/**
* The default uncaught exception handler for managed executions.
* It logs the thread and the exception.
*/
private final class LoggingExceptionHandler(
log: Logger,
delegate: Option[Thread.UncaughtExceptionHandler]
) extends Thread.UncaughtExceptionHandler {
def uncaughtException(t: Thread, e: Throwable): Unit = {
log.error("(" + t.getName + ") " + e.toString)
log.trace(e)
delegate.foreach(_.uncaughtException(t, e))
}
}

View File

@ -13,7 +13,7 @@ import java.util.concurrent.atomic.AtomicInteger
// starts svr using server-test/events and perform event related tests
object EventsTest extends AbstractServerTest {
override val testDirectory: String = "events"
val currentID = new AtomicInteger(0)
val currentID = new AtomicInteger(1000)
test("report task failures in case of exceptions") { _ =>
val id = currentID.getAndIncrement()
@ -30,9 +30,7 @@ object EventsTest extends AbstractServerTest {
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":$id, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
s contains "Waiting for"
})
Thread.sleep(1000)
val cancelID = currentID.getAndIncrement()
val invalidID = currentID.getAndIncrement()
svr.sendJsonRpc(
@ -41,52 +39,49 @@ object EventsTest extends AbstractServerTest {
assert(svr.waitForString(20.seconds) { s =>
(s contains """"error":{"code":-32800""")
})
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":${currentID.getAndIncrement}, "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
s contains """"result":{"status":"Task cancelled""""
})
}
/*
test("cancel on-going task with numeric id") { _ =>
val id = currentID.getAndIncrement()
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":$id, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(20.seconds) { s =>
s contains "Compiled events"
})
assert(svr.waitForString(10.seconds) { s =>
s contains "Waiting for"
s contains "running Main"
})
val cancelID = currentID.getAndIncrement()
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id":$cancelID, "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(11.seconds) { s =>
println(s)
s contains """"result":{"status":"Task cancelled""""
})
}
*/
/*
test("cancel on-going task with string id") { _ =>
import sbt.Exec
val id = Exec.newExecId
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "$id", "method": "sbt/exec", "params": { "commandLine": "run" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(20.seconds) { s =>
s contains "Compiled events"
})
assert(svr.waitForString(10.seconds) { s =>
s contains "Waiting for"
})
val cancelID = Exec.newExecId
svr.sendJsonRpc(
s"""{ "jsonrpc": "2.0", "id": "$cancelID", "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
)
assert(svr.waitForString(10.seconds) { s =>
assert(svr.waitForString(11.seconds) { s =>
println(s)
s contains """"result":{"status":"Task cancelled""""
})
}
*/
}