From 2686acd99326d99bf2f17a488858d6df5e27d52a Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 7 Apr 2014 16:42:08 -0400 Subject: [PATCH 1/6] Part #1 of cancel-task-hooks - Hooks EvaluateTask. * Create a new EvaluateTaskConfig which gives us a bit more freedom over changign config options to EvaluateTask in the future. * Create adapted from old EvaluateTask to new EvaluateTask * Add hooks into signals class to register/remote a signal listener directly, rather than in an "arm" block. * Create TaskEvaluationCancelHandler to control the strategy of who/whom can cancel (sbt-server vs. sbt-terminal). * Create a null-object for the "can't cancel" scenario so the code path is exactly the same. This commit does not wire settings into the build yet, nor does it fix the config extractio methods. --- main/src/main/scala/sbt/EvaluateTask.scala | 98 ++++++++++++++++--- .../src/main/scala/sbt/Signal.scala | 32 ++++++ 2 files changed, 118 insertions(+), 12 deletions(-) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 4c8fe8756..59010eeb7 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -14,7 +14,73 @@ package sbt import std.Transform.{DummyTaskMap,TaskAndValue} import TaskName._ +@deprecated("Use EvaluateTaskConfig instead.", "0.13.2") final case class EvaluateConfig(cancelable: Boolean, restrictions: Seq[Tags.Rule], checkCycles: Boolean = false, progress: ExecuteProgress[Task] = EvaluateTask.defaultProgress) + + + +/** Represents something that can be cancelled. */ +trait TaskCancel { + /** cancels whatever this points at. */ + def cancel(): Unit +} +/** A handler for registering/remmoving listeners that allow you to cancel tasks. */ +trait TaskEvaluationCancelHandler { + /* Evaluation is starting, here's a mechanism to cancel things. */ + def start(canceller: TaskCancel): Unit + /* Task Evaluation is complete, whether success or failure. */ + def finish(): Unit +} +object TaskEvaluationCancelHandler { + /** An empty handler that does nothing. */ + object Null extends TaskEvaluationCancelHandler { + def start(canceller: TaskCancel): Unit = () + def finish(): Unit = () + } + /** Cancel handler which registers for SIGINT and cancels tasks when it is received. */ + object Signal extends TaskEvaluationCancelHandler { + private var registration: Option[Signals.Registration] = None + def start(canceller: TaskCancel): Unit = { + registration = Some(Signals.register(() => canceller.cancel())) + } + def finish(): Unit = + registration match { + case Some(value) => + value.remove() + registration = None + case None => + } + } +} + + +/** The new API for running tasks. + * + * This represents all the hooks possible when running the task engine. + * We expose this trait so that we can, in a binary compatible way, modify what is used + * inside this configuration and how to construct it. + */ +sealed trait EvaluateTaskConfig { + def restrictions: Seq[Tags.Rule] + def checkCycles: Boolean + def progressReporter: ExecuteProgress[Task] + def cancelHandler: TaskEvaluationCancelHandler +} +final object EvaluateTaskConfig { + /** Pulls in the old configuration format. */ + def apply(old: EvaluateConfig): EvaluateTaskConfig = { + object AdaptedTaskConfig extends EvaluateTaskConfig { + def restrictions: Seq[Tags.Rule] = old.restrictions + def checkCycles: Boolean = old.checkCycles + def progressReporter: ExecuteProgress[Task] = old.progress + def cancelHandler: TaskEvaluationCancelHandler = + if(old.cancelable) TaskEvaluationCancelHandler.Signal + else TaskEvaluationCancelHandler.Null + } + AdaptedTaskConfig + } +} + final case class PluginData(dependencyClasspath: Seq[Attributed[File]], definitionClasspath: Seq[Attributed[File]], resolvers: Option[Seq[Resolver]], report: Option[UpdateReport], scalacOptions: Seq[String]) { val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath @@ -176,12 +242,18 @@ object EvaluateTask def nodeView[HL <: HList](state: State, streams: Streams, roots: Seq[ScopedKey[_]], dummies: DummyTaskMap = DummyTaskMap(Nil)): NodeView[Task] = Transform((dummyRoots, roots) :: (dummyStreamsManager, streams) :: (dummyState, state) :: dummies ) - def runTask[T](root: Task[T], state: State, streams: Streams, triggers: Triggers[Task], config: EvaluateConfig)(implicit taskToNode: NodeView[Task]): (State, Result[T]) = + @deprecated("Use new EvalauteTaskConfig option to runTask", "0.13.2") + def runTask[T](root: Task[T], state: State, streams: Streams, triggers: Triggers[Task], config: EvaluateConfig)(implicit taskToNode: NodeView[Task]): (State, Result[T]) = + { + val newConfig = EvaluateTaskConfig(config) + runTask(root, state, streams, triggers, config)(taskToNode) + } + def runTask[T](root: Task[T], state: State, streams: Streams, triggers: Triggers[Task], config: EvaluateTaskConfig)(implicit taskToNode: NodeView[Task]): (State, Result[T]) = { - import ConcurrentRestrictions.{completionService, TagMap, Tag, tagged, tagsKey} + import ConcurrentRestrictions.{completionService, TagMap, Tag, tagged, tagsKey} val log = state.log - log.debug("Running task... Cancelable: " + config.cancelable + ", check cycles: " + config.checkCycles) + log.debug("Running task... Cancel: " + config.cancelHandler + ", check cycles: " + config.checkCycles) val tags = tagged[Task[_]](_.info get tagsKey getOrElse Map.empty, Tags.predicate(config.restrictions)) val (service, shutdown) = completionService[Task[_], Completed](tags, (s: String) => log.warn(s)) @@ -191,7 +263,7 @@ object EvaluateTask case _ => true } def run() = { - val x = new Execute[Task]( Execute.config(config.checkCycles, overwriteNode), triggers, config.progress)(taskToNode) + val x = new Execute[Task]( Execute.config(config.checkCycles, overwriteNode), triggers, config.progressReporter)(taskToNode) val (newState, result) = try { val results = x.runKeep(root)(service) @@ -203,15 +275,17 @@ object EvaluateTask logIncResult(replaced, state, streams) (newState, replaced) } - val cancel = () => { - println("") - log.warn("Canceling execution...") - shutdown() + object taskCancel extends TaskCancel { + def cancel(): Unit = { + println("") + log.warn("Canceling execution...") + shutdown() + } } - if(config.cancelable) - Signals.withHandler(cancel) { run } - else - run() + // Register with our cancel handler we're about to start. + config.cancelHandler.start(taskCancel) + try run() + finally config.cancelHandler.finish() } private[this] def storeValuesForPrevious(results: RMap[Task, Result], state: State, streams: Streams): Unit = diff --git a/util/collection/src/main/scala/sbt/Signal.scala b/util/collection/src/main/scala/sbt/Signal.scala index 8bad472cd..0069e4b53 100644 --- a/util/collection/src/main/scala/sbt/Signal.scala +++ b/util/collection/src/main/scala/sbt/Signal.scala @@ -19,6 +19,38 @@ object Signals case Right(v) => v } } + + /** Helper interface so we can expose internals of signal-isms to others. */ + sealed trait Registration { + def remove(): Unit + } + /** Register a signal handler that can be removed later. + * NOTE: Does not stack with other signal handlers!!!! + */ + def register(handler: () => Unit, signal: String = INT): Registration = + // TODO - Maybe we can just ignore things if not is-supported. + if(supported(signal)) { + import sun.misc.{Signal,SignalHandler} + val intSignal = new Signal(signal) + val newHandler = new SignalHandler { + def handle(sig: Signal) { handler() } + } + val oldHandler = Signal.handle(intSignal, newHandler) + object unregisterNewHandler extends Registration { + override def remove(): Unit = { + Signal.handle(intSignal, oldHandler) + } + } + unregisterNewHandler + } else { + // TODO - Maybe we should just throw an exception if we don't support signals... + object NullUnregisterNewHandler extends Registration { + override def remove(): Unit = () + } + NullUnregisterNewHandler + } + + def supported(signal: String): Boolean = try { From 6e480fc2b6685861ab78aa5f48292633900d9e50 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Mon, 7 Apr 2014 16:54:32 -0400 Subject: [PATCH 2/6] Fix deprecated 'since' clauses to reflect our actual release versions. --- main/src/main/scala/sbt/EvaluateTask.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 59010eeb7..842ba403d 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -14,7 +14,7 @@ package sbt import std.Transform.{DummyTaskMap,TaskAndValue} import TaskName._ -@deprecated("Use EvaluateTaskConfig instead.", "0.13.2") +@deprecated("Use EvaluateTaskConfig instead.", "0.13.5") final case class EvaluateConfig(cancelable: Boolean, restrictions: Seq[Tags.Rule], checkCycles: Boolean = false, progress: ExecuteProgress[Task] = EvaluateTask.defaultProgress) @@ -242,7 +242,7 @@ object EvaluateTask def nodeView[HL <: HList](state: State, streams: Streams, roots: Seq[ScopedKey[_]], dummies: DummyTaskMap = DummyTaskMap(Nil)): NodeView[Task] = Transform((dummyRoots, roots) :: (dummyStreamsManager, streams) :: (dummyState, state) :: dummies ) - @deprecated("Use new EvalauteTaskConfig option to runTask", "0.13.2") + @deprecated("Use new EvalauteTaskConfig option to runTask", "0.13.5") def runTask[T](root: Task[T], state: State, streams: Streams, triggers: Triggers[Task], config: EvaluateConfig)(implicit taskToNode: NodeView[Task]): (State, Result[T]) = { val newConfig = EvaluateTaskConfig(config) From 171eb19b96361d5df1558fb01e75c19c27fda805 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 8 Apr 2014 09:23:49 -0400 Subject: [PATCH 3/6] Part #2 of task cancellation hooks. * Expose new EvaluateTaskConfig throughout all the APIs * Create a key for cancellation configuration * Add default values for cancellation in GlobalPlugin * Create a test to ensure that cancellation can cancel tasks. * Deprecate all the existing mechanisms of evaluating tasks which use the EvaluateConfig API. --- main/src/main/scala/sbt/Aggregation.scala | 2 +- main/src/main/scala/sbt/Defaults.scala | 4 ++ main/src/main/scala/sbt/EvaluateTask.scala | 39 ++++++++++++++++--- main/src/main/scala/sbt/Keys.scala | 1 + main/src/main/scala/sbt/Project.scala | 14 ++++++- .../sbt-test/actions/task-cancel/build.sbt | 8 ++++ .../actions/task-cancel/project/Build.scala | 5 +++ .../task-cancel/src/main/scala/test.scala | 5 +++ sbt/src/sbt-test/actions/task-cancel/test | 3 ++ 9 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 sbt/src/sbt-test/actions/task-cancel/build.sbt create mode 100644 sbt/src/sbt-test/actions/task-cancel/project/Build.scala create mode 100644 sbt/src/sbt-test/actions/task-cancel/src/main/scala/test.scala create mode 100644 sbt/src/sbt-test/actions/task-cancel/test diff --git a/main/src/main/scala/sbt/Aggregation.scala b/main/src/main/scala/sbt/Aggregation.scala index 57b7c8c4f..60e9b64bb 100644 --- a/main/src/main/scala/sbt/Aggregation.scala +++ b/main/src/main/scala/sbt/Aggregation.scala @@ -59,7 +59,7 @@ final object Aggregation import extracted.structure val toRun = ts map { case KeyValue(k,t) => t.map(v => KeyValue(k,v)) } join; val roots = ts map { case KeyValue(k,_) => k } - val config = extractedConfig(extracted, structure, s) + val config = extractedTaskConfig(extracted, structure, s) val start = System.currentTimeMillis val (newS, result) = withStreams(structure, s){ str => diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 04b73d5ed..ba14788e9 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -120,6 +120,10 @@ object Defaults extends BuildCommon trapExit :== true, connectInput :== false, cancelable :== false, + taskCancelHandler := { state: State => + if(cancelable.value) TaskEvaluationCancelHandler.Signal + else TaskEvaluationCancelHandler.Null + }, envVars :== Map.empty, sbtVersion := appConfiguration.value.provider.id.version, sbtBinaryVersion := binarySbtVersion(sbtVersion.value), diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 842ba403d..4931ef077 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -79,6 +79,19 @@ final object EvaluateTaskConfig { } AdaptedTaskConfig } + /** Raw constructor for EvaluateTaskConfig. */ + def apply(restrictions: Seq[Tags.Rule], + checkCycles: Boolean, + progressReporter: ExecuteProgress[Task], + cancelHandler: TaskEvaluationCancelHandler): EvaluateTaskConfig = { + object SimpleEvaluateTaskConfig extends EvaluateTaskConfig { + def restrictions = restrictions + def checkCycles = checkCycles + def progressReporter = progressReporter + def cancelHandler = cancelHandler + } + SimpleEvaluateTaskConfig + } } final case class PluginData(dependencyClasspath: Seq[Attributed[File]], definitionClasspath: Seq[Attributed[File]], resolvers: Option[Seq[Resolver]], report: Option[UpdateReport], scalacOptions: Seq[String]) @@ -106,24 +119,25 @@ object EvaluateTask val SystemProcessors = Runtime.getRuntime.availableProcessors - @deprecated("Use extractedConfig.", "0.13.0") + @deprecated("Use extractedTaskConfig.", "0.13.0") def defaultConfig(state: State): EvaluateConfig = { val extracted = Project.extract(state) extractedConfig(extracted, extracted.structure, state) } - @deprecated("Use extractedConfig.", "0.13.0") + @deprecated("Use extractedTaskConfig.", "0.13.0") def defaultConfig(extracted: Extracted, structure: BuildStructure) = EvaluateConfig(false, restrictions(extracted, structure), progress = defaultProgress) - @deprecated("Use other extractedConfig", "0.13.2") + @deprecated("Use other extractedTaskConfig", "0.13.2") def extractedConfig(extracted: Extracted, structure: BuildStructure): EvaluateConfig = { val workers = restrictions(extracted, structure) val canCancel = cancelable(extracted, structure) EvaluateConfig(cancelable = canCancel, restrictions = workers, progress = defaultProgress) } + @deprecated("Use other extractedTaskConfig", "0.13.5") def extractedConfig(extracted: Extracted, structure: BuildStructure, state: State): EvaluateConfig = { val workers = restrictions(extracted, structure) @@ -131,6 +145,13 @@ object EvaluateTask val progress = executeProgress(extracted, structure, state) EvaluateConfig(cancelable = canCancel, restrictions = workers, progress = progress) } + def extractedTaskConfig(extracted: Extracted, structure: BuildStructure, state: State): EvaluateTaskConfig = + { + val rs = restrictions(extracted, structure) + val canceller = cancelHandler(extracted, structure, state) + val progress = executeProgress(extracted, structure, state) + EvaluateTaskConfig(rs, false, progress, canceller) + } def defaultRestrictions(maxWorkers: Int) = Tags.limitAll(maxWorkers) :: Nil def defaultRestrictions(extracted: Extracted, structure: BuildStructure): Seq[Tags.Rule] = @@ -150,11 +171,13 @@ object EvaluateTask 1 def cancelable(extracted: Extracted, structure: BuildStructure): Boolean = getSetting(Keys.cancelable, false, extracted, structure) + def cancelHandler(extracted: Extracted, structure: BuildStructure, state: State): TaskEvaluationCancelHandler = + getSetting(Keys.taskCancelHandler, {(_: State) => TaskEvaluationCancelHandler.Null}, extracted, structure)(state) private[sbt] def executeProgress(extracted: Extracted, structure: BuildStructure, state: State): ExecuteProgress[Task] = { import Types.const - val maker: State => Keys.TaskProgress = getSetting(Keys.executeProgress, const(new Keys.TaskProgress(defaultProgress)), extracted, structure) - maker(state).progress + val maker: State => Keys.TaskProgress = getSetting(Keys.executeProgress, const(new Keys.TaskProgress(defaultProgress)), extracted, structure) + maker(state).progress } def getSetting[T](key: SettingKey[T], default: T, extracted: Extracted, structure: BuildStructure): T = @@ -185,16 +208,20 @@ object EvaluateTask * If the task is not defined, None is returned. The provided task key is resolved against the current project `ref`. * Task execution is configured according to settings defined in the loaded project.*/ def apply[T](structure: BuildStructure, taskKey: ScopedKey[Task[T]], state: State, ref: ProjectRef): Option[(State, Result[T])] = - apply[T](structure, taskKey, state, ref, extractedConfig(Project.extract(state), structure)) + apply[T](structure, taskKey, state, ref, extractedTaskConfig(Project.extract(state), structure, state)) /** Evaluates `taskKey` and returns the new State and the result of the task wrapped in Some. * If the task is not defined, None is returned. The provided task key is resolved against the current project `ref`. * `config` configures concurrency and canceling of task execution. */ + @deprecated("Use EvalauteTaskConfig option instead.", "0.13.5") def apply[T](structure: BuildStructure, taskKey: ScopedKey[Task[T]], state: State, ref: ProjectRef, config: EvaluateConfig): Option[(State, Result[T])] = + apply(structure, taskKey, state, ref, EvaluateTaskConfig(config)) + def apply[T](structure: BuildStructure, taskKey: ScopedKey[Task[T]], state: State, ref: ProjectRef, config: EvaluateTaskConfig): Option[(State, Result[T])] = { withStreams(structure, state) { str => for( (task, toNode) <- getTask(structure, taskKey, state, str, ref) ) yield runTask(task, state, str, structure.index.triggers, config)(toNode) } + } def logIncResult(result: Result[_], state: State, streams: Streams) = result match { case Inc(i) => logIncomplete(i, state, streams); case _ => () } def logIncomplete(result: Incomplete, state: State, streams: Streams) { diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 53ccdfc97..2f2f9b09e 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -346,6 +346,7 @@ object Keys // wrapper to work around SI-2915 private[sbt] final class TaskProgress(val progress: ExecuteProgress[Task]) private[sbt] val executeProgress = SettingKey[State => TaskProgress]("executeProgress", "Experimental task execution listener.", DTask) + private[sbt] val taskCancelHandler = SettingKey[State => TaskEvaluationCancelHandler]("taskCancelHandler", "Experimental task cancellation handler.", DTask) // Experimental in sbt 0.13.2 to enable grabing semantic compile failures. private[sbt] val compilerReporter = TaskKey[Option[xsbti.Reporter]]("compilerReporter", "Experimental hook to listen (or send) compilation failure messages.", DTask) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index faea25e13..de8b6d311 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -498,13 +498,25 @@ object Project extends ProjectExtra @deprecated("This method does not apply state changes requested during task execution. Use 'runTask' instead, which does.", "0.11.1") def evaluateTask[T](taskKey: ScopedKey[Task[T]], state: State, checkCycles: Boolean = false, maxWorkers: Int = EvaluateTask.SystemProcessors): Option[Result[T]] = runTask(taskKey, state, EvaluateConfig(true, EvaluateTask.defaultRestrictions(maxWorkers), checkCycles)).map(_._2) + def runTask[T](taskKey: ScopedKey[Task[T]], state: State, checkCycles: Boolean = false): Option[(State, Result[T])] = - runTask(taskKey, state, EvaluateConfig(true, EvaluateTask.restrictions(state), checkCycles)) + { + val extracted = Project.extract(state) + val ch = EvaluateTask.cancelHandler(extracted, extracted.structure, state) + val p = EvaluateTask.executeProgress(extracted, extracted.structure, state) + val r = EvaluateTask.restrictions(state) + runTask(taskKey, state, EvaluateTaskConfig(r, checkCycles, p, ch)) + } + @deprecated("Use EvalauteTaskConfig option instead.", "0.13.5") def runTask[T](taskKey: ScopedKey[Task[T]], state: State, config: EvaluateConfig): Option[(State, Result[T])] = { val extracted = Project.extract(state) EvaluateTask(extracted.structure, taskKey, state, extracted.currentRef, config) } + def runTask[T](taskKey: ScopedKey[Task[T]], state: State, config: EvaluateTaskConfig): Option[(State, Result[T])] = { + val extracted = Project.extract(state) + EvaluateTask(extracted.structure, taskKey, state, extracted.currentRef, config) + } implicit def projectToRef(p: Project): ProjectReference = LocalProject(p.id) diff --git a/sbt/src/sbt-test/actions/task-cancel/build.sbt b/sbt/src/sbt-test/actions/task-cancel/build.sbt new file mode 100644 index 000000000..b621673ef --- /dev/null +++ b/sbt/src/sbt-test/actions/task-cancel/build.sbt @@ -0,0 +1,8 @@ +import sbt.ExposeYourself._ + +taskCancelHandler := { (state: State) => + new TaskEvaluationCancelHandler { + override def start(canceller: TaskCancel): Unit = canceller.cancel() + override def finish(): Unit = () + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/actions/task-cancel/project/Build.scala b/sbt/src/sbt-test/actions/task-cancel/project/Build.scala new file mode 100644 index 000000000..cd7055cf0 --- /dev/null +++ b/sbt/src/sbt-test/actions/task-cancel/project/Build.scala @@ -0,0 +1,5 @@ +package sbt // this API is private[sbt], so only exposed for trusted clients and folks who like breaking. + +object ExposeYourself { + val taskCancelHandler = sbt.Keys.taskCancelHandler +} \ No newline at end of file diff --git a/sbt/src/sbt-test/actions/task-cancel/src/main/scala/test.scala b/sbt/src/sbt-test/actions/task-cancel/src/main/scala/test.scala new file mode 100644 index 000000000..c3853a565 --- /dev/null +++ b/sbt/src/sbt-test/actions/task-cancel/src/main/scala/test.scala @@ -0,0 +1,5 @@ +import scala + +object Foo { + val x = "this should be long to compile or the test may fail." +} \ No newline at end of file diff --git a/sbt/src/sbt-test/actions/task-cancel/test b/sbt/src/sbt-test/actions/task-cancel/test new file mode 100644 index 000000000..87501cf8a --- /dev/null +++ b/sbt/src/sbt-test/actions/task-cancel/test @@ -0,0 +1,3 @@ +# All tasks should fail. +-> compile +-> test \ No newline at end of file From 8f1ef5395dc897e0fcff3778e3a6caf90628d2ba Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 8 Apr 2014 11:09:23 -0400 Subject: [PATCH 4/6] Improve scaladoc comments for the task cancellation api. --- main/src/main/scala/sbt/EvaluateTask.scala | 24 +++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 4931ef077..b58243b23 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -19,20 +19,34 @@ final case class EvaluateConfig(cancelable: Boolean, restrictions: Seq[Tags.Rule -/** Represents something that can be cancelled. */ +/** Represents something that can be cancelled. + * For example, this is implemented by the TaskEngine; invoking `cancel()` allows you + * to cancel the current task exeuction. A `TaskCancel` is passed to the + * [[TaskEvalautionCancelHandler]] which is responsible for calling `cancel()` when + * appropriate. + */ trait TaskCancel { /** cancels whatever this points at. */ def cancel(): Unit } -/** A handler for registering/remmoving listeners that allow you to cancel tasks. */ +/** + * A handler for registering/remmoving listeners that allow you to cancel task + * execution. + * + * Implementations of this trait determine what will trigger `cancel()` for + * the task engine, providing in the `start` method. + */ trait TaskEvaluationCancelHandler { - /* Evaluation is starting, here's a mechanism to cancel things. */ + /** Called when task evaluation starts. + * + * @param canceller An object that can cancel the current task evaluation session. + */ def start(canceller: TaskCancel): Unit - /* Task Evaluation is complete, whether success or failure. */ + /** Called when task evaluation completes, either in success or failure. */ def finish(): Unit } object TaskEvaluationCancelHandler { - /** An empty handler that does nothing. */ + /** An empty handler that does not cancel tasks. */ object Null extends TaskEvaluationCancelHandler { def start(canceller: TaskCancel): Unit = () def finish(): Unit = () From f42dee8090848919582aad51f8b366472b83aabb Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 8 Apr 2014 12:08:30 -0400 Subject: [PATCH 5/6] Naming fixes to TaskCancellationStrategy to be clearer. --- main/src/main/scala/sbt/Defaults.scala | 6 ++-- main/src/main/scala/sbt/EvaluateTask.scala | 40 ++++++++++++---------- main/src/main/scala/sbt/Keys.scala | 2 +- main/src/main/scala/sbt/Project.scala | 2 +- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index ba14788e9..d08c1e95b 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -120,9 +120,9 @@ object Defaults extends BuildCommon trapExit :== true, connectInput :== false, cancelable :== false, - taskCancelHandler := { state: State => - if(cancelable.value) TaskEvaluationCancelHandler.Signal - else TaskEvaluationCancelHandler.Null + taskCancelStrategy := { state: State => + if(cancelable.value) TaskCancellationStrategy.Signal + else TaskCancellationStrategy.Null }, envVars :== Map.empty, sbtVersion := appConfiguration.value.provider.id.version, diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index b58243b23..5b7883f45 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -19,7 +19,8 @@ final case class EvaluateConfig(cancelable: Boolean, restrictions: Seq[Tags.Rule -/** Represents something that can be cancelled. +/** An API that allows you to cancel executing tasks upon some signal. + * * For example, this is implemented by the TaskEngine; invoking `cancel()` allows you * to cancel the current task exeuction. A `TaskCancel` is passed to the * [[TaskEvalautionCancelHandler]] which is responsible for calling `cancel()` when @@ -30,13 +31,14 @@ trait TaskCancel { def cancel(): Unit } /** - * A handler for registering/remmoving listeners that allow you to cancel task - * execution. + * A handler for how to handle task cancellation. * * Implementations of this trait determine what will trigger `cancel()` for * the task engine, providing in the `start` method. + * + * All methods on this API are expected to be called from the same thread. */ -trait TaskEvaluationCancelHandler { +trait TaskCancellationStrategy { /** Called when task evaluation starts. * * @param canceller An object that can cancel the current task evaluation session. @@ -45,14 +47,14 @@ trait TaskEvaluationCancelHandler { /** Called when task evaluation completes, either in success or failure. */ def finish(): Unit } -object TaskEvaluationCancelHandler { +object TaskCancellationStrategy { /** An empty handler that does not cancel tasks. */ - object Null extends TaskEvaluationCancelHandler { + object Null extends TaskCancellationStrategy { def start(canceller: TaskCancel): Unit = () def finish(): Unit = () } /** Cancel handler which registers for SIGINT and cancels tasks when it is received. */ - object Signal extends TaskEvaluationCancelHandler { + object Signal extends TaskCancellationStrategy { private var registration: Option[Signals.Registration] = None def start(canceller: TaskCancel): Unit = { registration = Some(Signals.register(() => canceller.cancel())) @@ -78,7 +80,7 @@ sealed trait EvaluateTaskConfig { def restrictions: Seq[Tags.Rule] def checkCycles: Boolean def progressReporter: ExecuteProgress[Task] - def cancelHandler: TaskEvaluationCancelHandler + def cancelStrategy: TaskCancellationStrategy } final object EvaluateTaskConfig { /** Pulls in the old configuration format. */ @@ -87,9 +89,9 @@ final object EvaluateTaskConfig { def restrictions: Seq[Tags.Rule] = old.restrictions def checkCycles: Boolean = old.checkCycles def progressReporter: ExecuteProgress[Task] = old.progress - def cancelHandler: TaskEvaluationCancelHandler = - if(old.cancelable) TaskEvaluationCancelHandler.Signal - else TaskEvaluationCancelHandler.Null + def cancelStrategy: TaskCancellationStrategy = + if(old.cancelable) TaskCancellationStrategy.Signal + else TaskCancellationStrategy.Null } AdaptedTaskConfig } @@ -97,12 +99,12 @@ final object EvaluateTaskConfig { def apply(restrictions: Seq[Tags.Rule], checkCycles: Boolean, progressReporter: ExecuteProgress[Task], - cancelHandler: TaskEvaluationCancelHandler): EvaluateTaskConfig = { + cancelStrategy: TaskCancellationStrategy): EvaluateTaskConfig = { object SimpleEvaluateTaskConfig extends EvaluateTaskConfig { def restrictions = restrictions def checkCycles = checkCycles def progressReporter = progressReporter - def cancelHandler = cancelHandler + def cancelStrategy = cancelStrategy } SimpleEvaluateTaskConfig } @@ -162,7 +164,7 @@ object EvaluateTask def extractedTaskConfig(extracted: Extracted, structure: BuildStructure, state: State): EvaluateTaskConfig = { val rs = restrictions(extracted, structure) - val canceller = cancelHandler(extracted, structure, state) + val canceller = cancelStrategy(extracted, structure, state) val progress = executeProgress(extracted, structure, state) EvaluateTaskConfig(rs, false, progress, canceller) } @@ -185,8 +187,8 @@ object EvaluateTask 1 def cancelable(extracted: Extracted, structure: BuildStructure): Boolean = getSetting(Keys.cancelable, false, extracted, structure) - def cancelHandler(extracted: Extracted, structure: BuildStructure, state: State): TaskEvaluationCancelHandler = - getSetting(Keys.taskCancelHandler, {(_: State) => TaskEvaluationCancelHandler.Null}, extracted, structure)(state) + def cancelStrategy(extracted: Extracted, structure: BuildStructure, state: State): TaskCancellationStrategy = + getSetting(Keys.taskCancelStrategy, {(_: State) => TaskCancellationStrategy.Null}, extracted, structure)(state) private[sbt] def executeProgress(extracted: Extracted, structure: BuildStructure, state: State): ExecuteProgress[Task] = { import Types.const @@ -294,7 +296,7 @@ object EvaluateTask import ConcurrentRestrictions.{completionService, TagMap, Tag, tagged, tagsKey} val log = state.log - log.debug("Running task... Cancel: " + config.cancelHandler + ", check cycles: " + config.checkCycles) + log.debug("Running task... Cancel: " + config.cancelStrategy + ", check cycles: " + config.checkCycles) val tags = tagged[Task[_]](_.info get tagsKey getOrElse Map.empty, Tags.predicate(config.restrictions)) val (service, shutdown) = completionService[Task[_], Completed](tags, (s: String) => log.warn(s)) @@ -324,9 +326,9 @@ object EvaluateTask } } // Register with our cancel handler we're about to start. - config.cancelHandler.start(taskCancel) + config.cancelStrategy.start(taskCancel) try run() - finally config.cancelHandler.finish() + finally config.cancelStrategy.finish() } private[this] def storeValuesForPrevious(results: RMap[Task, Result], state: State, streams: Streams): Unit = diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 2f2f9b09e..16294ef80 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -346,7 +346,7 @@ object Keys // wrapper to work around SI-2915 private[sbt] final class TaskProgress(val progress: ExecuteProgress[Task]) private[sbt] val executeProgress = SettingKey[State => TaskProgress]("executeProgress", "Experimental task execution listener.", DTask) - private[sbt] val taskCancelHandler = SettingKey[State => TaskEvaluationCancelHandler]("taskCancelHandler", "Experimental task cancellation handler.", DTask) + private[sbt] val taskCancelStrategy = SettingKey[State => TaskCancellationStrategy]("taskCancelStrategy", "Experimental task cancellation handler.", DTask) // Experimental in sbt 0.13.2 to enable grabing semantic compile failures. private[sbt] val compilerReporter = TaskKey[Option[xsbti.Reporter]]("compilerReporter", "Experimental hook to listen (or send) compilation failure messages.", DTask) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index de8b6d311..06420cf84 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -502,7 +502,7 @@ object Project extends ProjectExtra def runTask[T](taskKey: ScopedKey[Task[T]], state: State, checkCycles: Boolean = false): Option[(State, Result[T])] = { val extracted = Project.extract(state) - val ch = EvaluateTask.cancelHandler(extracted, extracted.structure, state) + val ch = EvaluateTask.cancelStrategy(extracted, extracted.structure, state) val p = EvaluateTask.executeProgress(extracted, extracted.structure, state) val r = EvaluateTask.restrictions(state) runTask(taskKey, state, EvaluateTaskConfig(r, checkCycles, p, ch)) From 3890c98115f88d12f0665bb1c29e3fb224a0c364 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 8 Apr 2014 20:59:50 -0400 Subject: [PATCH 6/6] Remove mutability from the task cancel api. * Rename TaskCancel to RunningTaskEngine for clarity * Explicitly pass a state value in TaskCancellationStrategy * Update hooks to be immutable/safe. --- main/src/main/scala/sbt/EvaluateTask.scala | 44 +++++++++---------- .../sbt-test/actions/task-cancel/build.sbt | 5 ++- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 5b7883f45..60e0a44ad 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -26,12 +26,12 @@ final case class EvaluateConfig(cancelable: Boolean, restrictions: Seq[Tags.Rule * [[TaskEvalautionCancelHandler]] which is responsible for calling `cancel()` when * appropriate. */ -trait TaskCancel { - /** cancels whatever this points at. */ - def cancel(): Unit +trait RunningTaskEngine { + /** Attempts to kill and shutdown the running task engine.*/ + def cancelAndShutdown(): Unit } /** - * A handler for how to handle task cancellation. + * A startegy for being able to cancle tasks. * * Implementations of this trait determine what will trigger `cancel()` for * the task engine, providing in the `start` method. @@ -39,33 +39,32 @@ trait TaskCancel { * All methods on this API are expected to be called from the same thread. */ trait TaskCancellationStrategy { + /** The state used by this task. */ + type State /** Called when task evaluation starts. * * @param canceller An object that can cancel the current task evaluation session. + * @return Whatever state you need to cleanup in your finish method. */ - def start(canceller: TaskCancel): Unit + def onTaskEngineStart(canceller: RunningTaskEngine): State /** Called when task evaluation completes, either in success or failure. */ - def finish(): Unit + def onTaskEngineFinish(state: State): Unit } object TaskCancellationStrategy { /** An empty handler that does not cancel tasks. */ object Null extends TaskCancellationStrategy { - def start(canceller: TaskCancel): Unit = () - def finish(): Unit = () + type State = Unit + def onTaskEngineStart(canceller: RunningTaskEngine): Unit = () + def onTaskEngineFinish(state: Unit): Unit = () } /** Cancel handler which registers for SIGINT and cancels tasks when it is received. */ object Signal extends TaskCancellationStrategy { - private var registration: Option[Signals.Registration] = None - def start(canceller: TaskCancel): Unit = { - registration = Some(Signals.register(() => canceller.cancel())) + type State = Signals.Registration + def onTaskEngineStart(canceller: RunningTaskEngine): Signals.Registration = { + Signals.register(() => canceller.cancelAndShutdown()) } - def finish(): Unit = - registration match { - case Some(value) => - value.remove() - registration = None - case None => - } + def onTaskEngineFinish(registration: Signals.Registration): Unit = + registration.remove() } } @@ -318,17 +317,18 @@ object EvaluateTask logIncResult(replaced, state, streams) (newState, replaced) } - object taskCancel extends TaskCancel { - def cancel(): Unit = { + object runningEngine extends RunningTaskEngine { + def cancelAndShutdown(): Unit = { println("") log.warn("Canceling execution...") shutdown() } } // Register with our cancel handler we're about to start. - config.cancelStrategy.start(taskCancel) + val strat = config.cancelStrategy + val cancelState = strat.onTaskEngineStart(runningEngine) try run() - finally config.cancelStrategy.finish() + finally strat.onTaskEngineFinish(cancelState) } private[this] def storeValuesForPrevious(results: RMap[Task, Result], state: State, streams: Streams): Unit = diff --git a/sbt/src/sbt-test/actions/task-cancel/build.sbt b/sbt/src/sbt-test/actions/task-cancel/build.sbt index b621673ef..149e93163 100644 --- a/sbt/src/sbt-test/actions/task-cancel/build.sbt +++ b/sbt/src/sbt-test/actions/task-cancel/build.sbt @@ -2,7 +2,8 @@ import sbt.ExposeYourself._ taskCancelHandler := { (state: State) => new TaskEvaluationCancelHandler { - override def start(canceller: TaskCancel): Unit = canceller.cancel() - override def finish(): Unit = () + type State = Unit + override def onTaskEngineStart(canceller: TaskCancel): Unit = canceller.cancel() + override def finish(result: Unit): Unit = () } } \ No newline at end of file