From 82ae293e3fcac7f3faa3a611fa38ccf062a216d6 Mon Sep 17 00:00:00 2001 From: Iulian Dragos Date: Fri, 7 Jul 2023 10:59:19 +0200 Subject: [PATCH 01/10] Add a new CommandProgress API. In addition to ExecuteProgress, this new interface allows builds and plugins to receive events when commands start and finish, including the State before and after each command. It also makes cancellation visible to clients by making the Cancelled type top-level. --- main-command/src/main/scala/sbt/Command.scala | 3 +- main/src/main/scala/sbt/CommandProgress.scala | 27 ++++++ main/src/main/scala/sbt/Defaults.scala | 1 + main/src/main/scala/sbt/EvaluateTask.scala | 7 +- main/src/main/scala/sbt/Keys.scala | 2 + main/src/main/scala/sbt/MainLoop.scala | 89 +++++++++++++------ 6 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 main/src/main/scala/sbt/CommandProgress.scala diff --git a/main-command/src/main/scala/sbt/Command.scala b/main-command/src/main/scala/sbt/Command.scala index f4a1ebc6f..3ef6ae3f2 100644 --- a/main-command/src/main/scala/sbt/Command.scala +++ b/main-command/src/main/scala/sbt/Command.scala @@ -184,12 +184,13 @@ object Command { } ) - def process(command: String, state: State): State = { + def process(command: String, state: State, onParseError: String => Unit = _ => ()): State = { (if (command.contains(";")) parse(command, state.combinedParser) else parse(command, state.nonMultiParser)) match { case Right(s) => s() // apply command. command side effects happen here case Left(errMsg) => state.log error errMsg + onParseError(errMsg) state.fail } } diff --git a/main/src/main/scala/sbt/CommandProgress.scala b/main/src/main/scala/sbt/CommandProgress.scala new file mode 100644 index 000000000..5b353d972 --- /dev/null +++ b/main/src/main/scala/sbt/CommandProgress.scala @@ -0,0 +1,27 @@ +package sbt + +/** + * Tracks command execution progress. In addition to ExecuteProgress, this interface + * adds command start and end events, and gives access to the sbt.State at the beginning + * and end of each command. + */ +trait CommandProgress extends ExecuteProgress[Task] { + + /** + * Called before a command starts processing. The command has not yet been parsed. + * + * @param cmd The command string + * @param state The sbt.State before the command starts executing. + */ + def beforeCommand(cmd: String, state: State): Unit + + /** + * Called after a command finished execution. + * + * @param cmd The command string. + * @param result Left in case of an error. If the command cannot be parsed, it will be + * signalled as a ParseException with a detailed message. If the command + * was cancelled by the user, as sbt.Cancelled. + */ + def afterCommand(cmd: String, result: Either[Throwable, State]): Unit +} diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index e7a601c30..8a4a1abcb 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -341,6 +341,7 @@ object Defaults extends BuildCommon { val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector rs map { Keys.TaskProgress(_) } }, + commandProgress := Seq(), // progressState is deprecated SettingKey[Option[ProgressState]]("progressState") := None, Previous.cache := new Previous( diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index 29749cfde..dfeaf9f18 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -289,7 +289,12 @@ object EvaluateTask { extracted, structure ) - val reporters = maker.map(_.progress) ++ state.get(Keys.taskProgress) ++ + val reporters = maker.map(_.progress) ++ state.get(Keys.taskProgress) ++ getSetting( + Keys.commandProgress, + Seq(), + extracted, + structure + ) ++ (if (SysProp.taskTimings) new TaskTimings(reportOnShutdown = false, state.globalLogging.full) :: Nil else Nil) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 0fb3c3bdc..e320f7147 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -600,6 +600,7 @@ object Keys { def apply(progress: ExecuteProgress[Task]): TaskProgress = new TaskProgress(progress) } private[sbt] val currentTaskProgress = AttributeKey[TaskProgress]("current-task-progress") + private[sbt] val currentCommandProgress = AttributeKey[Seq[CommandProgress]]("current-command-progress") private[sbt] val taskProgress = AttributeKey[sbt.internal.TaskProgress]("active-task-progress") val useSuperShell = settingKey[Boolean]("Enables (true) or disables the super shell.") val superShellMaxTasks = settingKey[Int]("The max number of tasks to display in the supershell progress report") @@ -613,6 +614,7 @@ object Keys { private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask) @deprecated("No longer used", "1.3.0") private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask) + val commandProgress = settingKey[Seq[CommandProgress]]("Command progress listeners receive events when commands start and end, in addition to task progress events.") val lintUnused = inputKey[Unit]("Check for keys unused by other settings and tasks.") val lintIncludeFilter = settingKey[String => Boolean]("Filters key names that should be included in the lint check.") val lintExcludeFilter = settingKey[String => Boolean]("Filters key names that should be excluded in the lint check.") diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 3c7a800a6..cf02ae2b4 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -11,17 +11,16 @@ package sbt import java.io.PrintWriter import java.util.concurrent.RejectedExecutionException import java.util.Properties - -import sbt.BasicCommandStrings.{ StashOnFailure, networkExecPrefix } +import sbt.BasicCommandStrings.{StashOnFailure, networkExecPrefix} import sbt.internal.ShutdownHooks import sbt.internal.langserver.ErrorCodes import sbt.internal.protocol.JsonRpcResponseError import sbt.internal.nio.CheckBuildSources.CheckBuildSourcesKey -import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Prompt, Terminal => ITerminal } -import sbt.internal.{ ShutdownHooks, TaskProgress } -import sbt.io.{ IO, Using } +import sbt.internal.util.{AttributeKey, ErrorHandling, GlobalLogBacking, Prompt, Terminal => ITerminal} +import sbt.internal.{ShutdownHooks, TaskProgress} +import sbt.io.{IO, Using} import sbt.protocol._ -import sbt.util.{ Logger, LoggerContext } +import sbt.util.{Logger, LoggerContext} import scala.annotation.tailrec import scala.concurrent.duration._ @@ -29,6 +28,8 @@ import scala.util.control.NonFatal import sbt.internal.FastTrackCommands import sbt.internal.SysProp +import java.text.ParseException + object MainLoop { /** Entry point to run the remaining commands in State with managed global logging.*/ @@ -212,16 +213,29 @@ object MainLoop { ) try { def process(): State = { - val progressState = state.get(sbt.Keys.currentTaskProgress) match { - case Some(_) => state - case _ => - if (state.get(Keys.stateBuildStructure).isDefined) { - val extracted = Project.extract(state) - val progress = EvaluateTask.executeProgress(extracted, extracted.structure, state) - state.put(sbt.Keys.currentTaskProgress, new Keys.TaskProgress(progress)) - } else state + def getOrSet[T](state: State, key: AttributeKey[T], value: Extracted => T): State = { + state.get(key) match { + case Some(_) => state + case _ => + if (state.get(Keys.stateBuildStructure).isDefined) { + val extracted = Project.extract(state) + state.put(key, value(extracted)) + } else state + } } - exchange.setState(progressState) + val progressState = getOrSet( + state, + sbt.Keys.currentTaskProgress, + extracted => + new Keys.TaskProgress( + EvaluateTask.executeProgress(extracted, extracted.structure, state) + ) + ) + + val cmdProgressState = + getOrSet(progressState, sbt.Keys.currentCommandProgress, _.get(Keys.commandProgress)) + + exchange.setState(cmdProgressState) exchange.setExec(Some(exec)) val (restoreTerminal, termState) = channelName.flatMap(exchange.channelForName) match { case Some(c) => @@ -231,9 +245,13 @@ object MainLoop { (() => { ITerminal.set(prevTerminal) c.terminal.flush() - }) -> progressState.put(Keys.terminalKey, Terminal(c.terminal)) - case _ => (() => ()) -> progressState.put(Keys.terminalKey, Terminal(ITerminal.get)) + }) -> cmdProgressState.put(Keys.terminalKey, Terminal(c.terminal)) + case _ => (() => ()) -> cmdProgressState.put(Keys.terminalKey, Terminal(ITerminal.get)) } + + val currentCmdProgress = + cmdProgressState.get(sbt.Keys.currentCommandProgress).getOrElse(Nil) + currentCmdProgress.foreach(_.beforeCommand(exec.commandLine, progressState)) /* * FastTrackCommands.evaluate can be significantly faster than Command.process because * it avoids an expensive parsing step for internal commands that are easy to parse. @@ -241,16 +259,29 @@ object MainLoop { * but slower. */ val newState = try { - FastTrackCommands + var errorMsg: Option[String] = None + val res = FastTrackCommands .evaluate(termState, exec.commandLine) - .getOrElse(Command.process(exec.commandLine, termState)) + .getOrElse(Command.process(exec.commandLine, termState, m => errorMsg = Some(m))) + errorMsg match { + case Some(msg) => + currentCmdProgress.foreach( + _.afterCommand(exec.commandLine, Left(new ParseException(msg, 0))) + ) + case None => currentCmdProgress.foreach(_.afterCommand(exec.commandLine, Right(res))) + } + res } catch { case _: RejectedExecutionException => - // No stack trace since this is just to notify the user which command they cancelled - object Cancelled extends Throwable(exec.commandLine, null, true, false) { - override def toString: String = s"Cancelled: ${exec.commandLine}" - } - throw Cancelled + val cancelled = new Cancelled(exec.commandLine) + currentCmdProgress + .foreach(_.afterCommand(exec.commandLine, Left(cancelled))) + throw cancelled + + case e: Throwable => + currentCmdProgress + .foreach(_.afterCommand(exec.commandLine, Left(e))) + throw e } finally { // Flush the terminal output after command evaluation to ensure that all output // is displayed in the thin client before we report the command status. Also @@ -270,7 +301,10 @@ object MainLoop { } exchange.setExec(None) newState.get(sbt.Keys.currentTaskProgress).foreach(_.progress.stop()) - newState.remove(sbt.Keys.currentTaskProgress).remove(Keys.terminalKey) + newState + .remove(sbt.Keys.currentTaskProgress) + .remove(Keys.terminalKey) + .remove(Keys.currentCommandProgress) } state.get(CheckBuildSourcesKey) match { case Some(cbs) => @@ -341,3 +375,8 @@ object MainLoop { ExitCode(ErrorCodes.UnknownError) } else ExitCode.Success } + +// No stack trace since this is just to notify the user which command they cancelled +class Cancelled(cmdLine: String) extends Throwable(cmdLine, null, true, false) { + override def toString: String = s"Cancelled: $cmdLine" +} From 0f53349ce93181f70d381df9972dbcd2daad5384 Mon Sep 17 00:00:00 2001 From: Iulian Dragos Date: Tue, 8 Aug 2023 09:54:09 +0200 Subject: [PATCH 02/10] Add method overload to keep binary compatibility --- main-command/src/main/scala/sbt/Command.scala | 4 +++- main/src/main/scala/sbt/MainLoop.scala | 16 +++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/main-command/src/main/scala/sbt/Command.scala b/main-command/src/main/scala/sbt/Command.scala index 3ef6ae3f2..15db781c9 100644 --- a/main-command/src/main/scala/sbt/Command.scala +++ b/main-command/src/main/scala/sbt/Command.scala @@ -184,7 +184,9 @@ object Command { } ) - def process(command: String, state: State, onParseError: String => Unit = _ => ()): State = { + // overload instead of default parameter to keep binary compatibility + def process(command: String, state: State): State = process(command, state, _ => ()) + def process(command: String, state: State, onParseError: String => Unit): State = { (if (command.contains(";")) parse(command, state.combinedParser) else parse(command, state.nonMultiParser)) match { case Right(s) => s() // apply command. command side effects happen here diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index cf02ae2b4..4b293b550 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -11,16 +11,22 @@ package sbt import java.io.PrintWriter import java.util.concurrent.RejectedExecutionException import java.util.Properties -import sbt.BasicCommandStrings.{StashOnFailure, networkExecPrefix} +import sbt.BasicCommandStrings.{ StashOnFailure, networkExecPrefix } import sbt.internal.ShutdownHooks import sbt.internal.langserver.ErrorCodes import sbt.internal.protocol.JsonRpcResponseError import sbt.internal.nio.CheckBuildSources.CheckBuildSourcesKey -import sbt.internal.util.{AttributeKey, ErrorHandling, GlobalLogBacking, Prompt, Terminal => ITerminal} -import sbt.internal.{ShutdownHooks, TaskProgress} -import sbt.io.{IO, Using} +import sbt.internal.util.{ + AttributeKey, + ErrorHandling, + GlobalLogBacking, + Prompt, + Terminal => ITerminal +} +import sbt.internal.{ ShutdownHooks, TaskProgress } +import sbt.io.{ IO, Using } import sbt.protocol._ -import sbt.util.{Logger, LoggerContext} +import sbt.util.{ Logger, LoggerContext } import scala.annotation.tailrec import scala.concurrent.duration._ From 193dba708f400ba1f3fad597de785a1ede7490e7 Mon Sep 17 00:00:00 2001 From: Iulian Dragos Date: Tue, 8 Aug 2023 10:14:48 +0200 Subject: [PATCH 03/10] Add missing header. --- main/src/main/scala/sbt/CommandProgress.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main/src/main/scala/sbt/CommandProgress.scala b/main/src/main/scala/sbt/CommandProgress.scala index 5b353d972..757cd0a60 100644 --- a/main/src/main/scala/sbt/CommandProgress.scala +++ b/main/src/main/scala/sbt/CommandProgress.scala @@ -1,3 +1,11 @@ +/* + * sbt + * Copyright 2023, Scala center + * Copyright 2011 - 2022, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + package sbt /** From 51a774b081473feafe13db910d4ae906eb7839ce Mon Sep 17 00:00:00 2001 From: Iulian Dragos Date: Tue, 8 Aug 2023 11:29:32 +0200 Subject: [PATCH 04/10] Fix failing test case --- main/src/main/scala/sbt/MainLoop.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 4b293b550..cbee12e42 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -239,7 +239,11 @@ object MainLoop { ) val cmdProgressState = - getOrSet(progressState, sbt.Keys.currentCommandProgress, _.get(Keys.commandProgress)) + getOrSet( + progressState, + sbt.Keys.currentCommandProgress, + _.getOpt(Keys.commandProgress).getOrElse(Seq()) + ) exchange.setState(cmdProgressState) exchange.setExec(Some(exec)) From 18353f1ca8ab3b441f5154991645be666e3edfd5 Mon Sep 17 00:00:00 2001 From: Iulian Dragos Date: Fri, 11 Aug 2023 16:08:11 +0200 Subject: [PATCH 05/10] Reviewer's suggestions. --- build.sbt | 8 +- main/src/main/scala/sbt/CommandProgress.scala | 35 -------- main/src/main/scala/sbt/EvaluateTask.scala | 35 ++++---- .../src/main/scala/sbt/ExecuteProgress2.scala | 79 +++++++++++++++++++ main/src/main/scala/sbt/Keys.scala | 5 +- main/src/main/scala/sbt/MainLoop.scala | 19 ++--- 6 files changed, 109 insertions(+), 72 deletions(-) delete mode 100644 main/src/main/scala/sbt/CommandProgress.scala create mode 100644 main/src/main/scala/sbt/ExecuteProgress2.scala diff --git a/build.sbt b/build.sbt index ce6a0f709..3edd3c254 100644 --- a/build.sbt +++ b/build.sbt @@ -176,6 +176,8 @@ def mimaSettingsSince(versions: Seq[String]): Seq[Def.Setting[_]] = Def settings exclude[DirectMissingMethodProblem]("sbt.PluginData.apply"), exclude[DirectMissingMethodProblem]("sbt.PluginData.copy"), exclude[DirectMissingMethodProblem]("sbt.PluginData.this"), + exclude[IncompatibleResultTypeProblem]("sbt.EvaluateTask.executeProgress"), + exclude[DirectMissingMethodProblem]("sbt.Keys.currentTaskProgress") ), ) @@ -188,11 +190,11 @@ lazy val sbtRoot: Project = (project in file(".")) minimalSettings, onLoadMessage := { val version = sys.props("java.specification.version") - """ __ __ + """ __ __ | _____/ /_ / /_ | / ___/ __ \/ __/ - | (__ ) /_/ / /_ - | /____/_.___/\__/ + | (__ ) /_/ / /_ + | /____/_.___/\__/ |Welcome to the build for sbt. |""".stripMargin + (if (version != "1.8") diff --git a/main/src/main/scala/sbt/CommandProgress.scala b/main/src/main/scala/sbt/CommandProgress.scala deleted file mode 100644 index 757cd0a60..000000000 --- a/main/src/main/scala/sbt/CommandProgress.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * sbt - * Copyright 2023, Scala center - * Copyright 2011 - 2022, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt - -/** - * Tracks command execution progress. In addition to ExecuteProgress, this interface - * adds command start and end events, and gives access to the sbt.State at the beginning - * and end of each command. - */ -trait CommandProgress extends ExecuteProgress[Task] { - - /** - * Called before a command starts processing. The command has not yet been parsed. - * - * @param cmd The command string - * @param state The sbt.State before the command starts executing. - */ - def beforeCommand(cmd: String, state: State): Unit - - /** - * Called after a command finished execution. - * - * @param cmd The command string. - * @param result Left in case of an error. If the command cannot be parsed, it will be - * signalled as a ParseException with a detailed message. If the command - * was cancelled by the user, as sbt.Cancelled. - */ - def afterCommand(cmd: String, result: Either[Throwable, State]): Unit -} diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index dfeaf9f18..4c3477062 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -258,12 +258,15 @@ object EvaluateTask { extracted: Extracted, structure: BuildStructure, state: State - ): ExecuteProgress[Task] = { + ): ExecuteProgress2 = { state - .get(currentTaskProgress) - .map { tp => - new ExecuteProgress[Task] { - val progress = tp.progress + .get(currentCommandProgress) + .map { progress => + new ExecuteProgress2 { + override def beforeCommand(cmd: String, state: State): Unit = + progress.beforeCommand(cmd, state) + override def afterCommand(cmd: String, result: Either[Throwable, State]): Unit = + progress.afterCommand(cmd, result) override def initial(): Unit = progress.initial() override def afterRegistered( task: Task[_], @@ -279,7 +282,9 @@ object EvaluateTask { progress.afterCompleted(task, result) override def afterAllCompleted(results: RMap[Task, Result]): Unit = progress.afterAllCompleted(results) - override def stop(): Unit = {} + override def stop(): Unit = { + // TODO: this is not a typo, but a questionable decision in 6559c3a0 that is probably obsolete + } } } .getOrElse { @@ -289,20 +294,16 @@ object EvaluateTask { extracted, structure ) - val reporters = maker.map(_.progress) ++ state.get(Keys.taskProgress) ++ getSetting( - Keys.commandProgress, - Seq(), - extracted, - structure - ) ++ + val reporters = maker.map(_.progress) ++ state.get(Keys.taskProgress) ++ (if (SysProp.taskTimings) new TaskTimings(reportOnShutdown = false, state.globalLogging.full) :: Nil else Nil) - reporters match { - case xs if xs.isEmpty => ExecuteProgress.empty[Task] - case xs if xs.size == 1 => xs.head - case xs => ExecuteProgress.aggregate[Task](xs) - } + val cmdProgress = getSetting(Keys.commandProgress, Seq(), extracted, structure) + ExecuteProgress2.aggregate(reporters match { + case xs if xs.isEmpty => cmdProgress + case xs if xs.size == 1 => cmdProgress :+ new ExecuteProgressAdapter(xs.head) + case xs => cmdProgress :+ new ExecuteProgressAdapter(ExecuteProgress.aggregate[Task](xs)) + }) } } // TODO - Should this pull from Global or from the project itself? diff --git a/main/src/main/scala/sbt/ExecuteProgress2.scala b/main/src/main/scala/sbt/ExecuteProgress2.scala new file mode 100644 index 000000000..41ed6e658 --- /dev/null +++ b/main/src/main/scala/sbt/ExecuteProgress2.scala @@ -0,0 +1,79 @@ +/* + * sbt + * Copyright 2023, Scala center + * Copyright 2011 - 2022, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +import sbt.internal.util.RMap + +/** + * Tracks command execution progress. In addition to ExecuteProgress, this interface + * adds command start and end events, and gives access to the sbt.State at the beginning + * and end of each command. + */ +trait ExecuteProgress2 extends ExecuteProgress[Task] { + + /** + * Called before a command starts processing. The command has not yet been parsed. + * + * @param cmd The command string + * @param state The sbt.State before the command starts executing. + */ + def beforeCommand(cmd: String, state: State): Unit + + /** + * Called after a command finished execution. + * + * @param cmd The command string. + * @param result Left in case of an error. If the command cannot be parsed, it will be + * signalled as a ParseException with a detailed message. If the command + * was cancelled by the user, as sbt.Cancelled. + */ + def afterCommand(cmd: String, result: Either[Throwable, State]): Unit +} + +class ExecuteProgressAdapter(ep: ExecuteProgress[Task]) extends ExecuteProgress2 { + override def beforeCommand(cmd: String, state: State): Unit = {} + override def afterCommand(cmd: String, result: Either[Throwable, State]): Unit = {} + override def initial(): Unit = ep.initial() + override def afterRegistered( + task: Task[_], + allDeps: Iterable[Task[_]], + pendingDeps: Iterable[Task[_]] + ): Unit = ep.afterRegistered(task, allDeps, pendingDeps) + override def afterReady(task: Task[_]): Unit = ep.afterReady(task) + override def beforeWork(task: Task[_]): Unit = ep.beforeWork(task) + override def afterWork[A](task: Task[A], result: Either[Task[A], Result[A]]): Unit = + ep.afterWork(task, result) + override def afterCompleted[A](task: Task[A], result: Result[A]): Unit = + ep.afterCompleted(task, result) + override def afterAllCompleted(results: RMap[Task, Result]): Unit = ep.afterAllCompleted(results) + override def stop(): Unit = ep.stop() +} + +object ExecuteProgress2 { + def aggregate(xs: Seq[ExecuteProgress2]): ExecuteProgress2 = new ExecuteProgress2 { + override def beforeCommand(cmd: String, state: State): Unit = + xs.foreach(_.beforeCommand(cmd, state)) + override def afterCommand(cmd: String, result: Either[Throwable, State]): Unit = + xs.foreach(_.afterCommand(cmd, result)) + override def initial(): Unit = xs.foreach(_.initial()) + override def afterRegistered( + task: Task[_], + allDeps: Iterable[Task[_]], + pendingDeps: Iterable[Task[_]] + ): Unit = xs.foreach(_.afterRegistered(task, allDeps, pendingDeps)) + override def afterReady(task: Task[_]): Unit = xs.foreach(_.afterReady(task)) + override def beforeWork(task: Task[_]): Unit = xs.foreach(_.beforeWork(task)) + override def afterWork[A](task: Task[A], result: Either[Task[A], Result[A]]): Unit = + xs.foreach(_.afterWork(task, result)) + override def afterCompleted[A](task: Task[A], result: Result[A]): Unit = + xs.foreach(_.afterCompleted(task, result)) + override def afterAllCompleted(results: RMap[Task, Result]): Unit = + xs.foreach(_.afterAllCompleted(results)) + override def stop(): Unit = xs.foreach(_.stop()) + } +} diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index e320f7147..9b24eefc1 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -599,8 +599,7 @@ object Keys { object TaskProgress { def apply(progress: ExecuteProgress[Task]): TaskProgress = new TaskProgress(progress) } - private[sbt] val currentTaskProgress = AttributeKey[TaskProgress]("current-task-progress") - private[sbt] val currentCommandProgress = AttributeKey[Seq[CommandProgress]]("current-command-progress") + private[sbt] val currentCommandProgress = AttributeKey[ExecuteProgress2]("current-command-progress") private[sbt] val taskProgress = AttributeKey[sbt.internal.TaskProgress]("active-task-progress") val useSuperShell = settingKey[Boolean]("Enables (true) or disables the super shell.") val superShellMaxTasks = settingKey[Int]("The max number of tasks to display in the supershell progress report") @@ -614,7 +613,7 @@ object Keys { private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask) @deprecated("No longer used", "1.3.0") private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask) - val commandProgress = settingKey[Seq[CommandProgress]]("Command progress listeners receive events when commands start and end, in addition to task progress events.") + val commandProgress = settingKey[Seq[ExecuteProgress2]]("Command progress listeners receive events when commands start and end, in addition to task progress events.") val lintUnused = inputKey[Unit]("Check for keys unused by other settings and tasks.") val lintIncludeFilter = settingKey[String => Boolean]("Filters key names that should be included in the lint check.") val lintExcludeFilter = settingKey[String => Boolean]("Filters key names that should be excluded in the lint check.") diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index cbee12e42..aaa1d98ec 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -229,20 +229,12 @@ object MainLoop { } else state } } - val progressState = getOrSet( - state, - sbt.Keys.currentTaskProgress, - extracted => - new Keys.TaskProgress( - EvaluateTask.executeProgress(extracted, extracted.structure, state) - ) - ) val cmdProgressState = getOrSet( - progressState, + state, sbt.Keys.currentCommandProgress, - _.getOpt(Keys.commandProgress).getOrElse(Seq()) + extracted => EvaluateTask.executeProgress(extracted, extracted.structure, state) ) exchange.setState(cmdProgressState) @@ -260,8 +252,8 @@ object MainLoop { } val currentCmdProgress = - cmdProgressState.get(sbt.Keys.currentCommandProgress).getOrElse(Nil) - currentCmdProgress.foreach(_.beforeCommand(exec.commandLine, progressState)) + cmdProgressState.get(sbt.Keys.currentCommandProgress) + currentCmdProgress.foreach(_.beforeCommand(exec.commandLine, cmdProgressState)) /* * FastTrackCommands.evaluate can be significantly faster than Command.process because * it avoids an expensive parsing step for internal commands that are easy to parse. @@ -310,9 +302,8 @@ object MainLoop { exchange.respondStatus(doneEvent) } exchange.setExec(None) - newState.get(sbt.Keys.currentTaskProgress).foreach(_.progress.stop()) + newState.get(sbt.Keys.currentCommandProgress).foreach(_.stop()) newState - .remove(sbt.Keys.currentTaskProgress) .remove(Keys.terminalKey) .remove(Keys.currentCommandProgress) } From 8c9600249ebd91298c4b4d7c6d80b57127de63bd Mon Sep 17 00:00:00 2001 From: Iulian Dragos Date: Thu, 17 Aug 2023 15:56:04 +0200 Subject: [PATCH 06/10] Add scripted test --- .../reporter/command-progress/build.sbt | 39 +++++++++++++++++++ .../command-progress/project/EventLog.scala | 8 ++++ .../sbt-test/reporter/command-progress/test | 5 +++ 3 files changed, 52 insertions(+) create mode 100644 sbt-app/src/sbt-test/reporter/command-progress/build.sbt create mode 100644 sbt-app/src/sbt-test/reporter/command-progress/project/EventLog.scala create mode 100644 sbt-app/src/sbt-test/reporter/command-progress/test diff --git a/sbt-app/src/sbt-test/reporter/command-progress/build.sbt b/sbt-app/src/sbt-test/reporter/command-progress/build.sbt new file mode 100644 index 000000000..08f7a7492 --- /dev/null +++ b/sbt-app/src/sbt-test/reporter/command-progress/build.sbt @@ -0,0 +1,39 @@ +commandProgress += new ExecuteProgress2 { + override def beforeCommand(cmd: String, state: State): Unit = { + EventLog.logs += (s"BEFORE: $cmd") + // assert that `state` is the current state indeed + assert(state.currentCommand.isDefined) + assert(state.currentCommand.get.commandLine == cmd) + } + override def afterCommand(cmd: String, result: Either[Throwable, State]): Unit = { + EventLog.logs += (s"AFTER: $cmd") + result.left.foreach(EventLog.errors +=) + } + override def initial(): Unit = {} + override def afterRegistered( + task: Task[_], + allDeps: Iterable[Task[_]], + pendingDeps: Iterable[Task[_]] + ): Unit = {} + override def afterReady(task: Task[_]): Unit = {} + override def beforeWork(task: Task[_]): Unit = {} + override def afterWork[A](task: Task[A], result: Either[Task[A], Result[A]]): Unit = {} + override def afterCompleted[A](task: Task[A], result: Result[A]): Unit = {} + override def afterAllCompleted(results: RMap[Task, Result]): Unit = {} + override def stop(): Unit = {} +} + +val check = taskKey[Unit]("Check basic command events") +val checkParseError = taskKey[Unit]("Check that parse error is recorded") + +check := { + def hasEvent(cmd: String): Boolean = + EventLog.logs.contains(s"BEFORE: $cmd") && EventLog.logs.contains(s"AFTER: $cmd") + + assert(hasEvent("compile"), "Missing command event `compile`") + assert(hasEvent("+compile"), "Missing command event `+compile`") +} + +checkParseError := { + assert(EventLog.errors.exists(_.getMessage.toLowerCase.contains("not a valid command"))) +} diff --git a/sbt-app/src/sbt-test/reporter/command-progress/project/EventLog.scala b/sbt-app/src/sbt-test/reporter/command-progress/project/EventLog.scala new file mode 100644 index 000000000..88b00c491 --- /dev/null +++ b/sbt-app/src/sbt-test/reporter/command-progress/project/EventLog.scala @@ -0,0 +1,8 @@ +import scala.collection.mutable.ListBuffer + +object EventLog { + val logs = ListBuffer.empty[String] + val errors = ListBuffer.empty[Throwable] + + def reset(): Unit = logs.clear() +} diff --git a/sbt-app/src/sbt-test/reporter/command-progress/test b/sbt-app/src/sbt-test/reporter/command-progress/test new file mode 100644 index 000000000..9011065e1 --- /dev/null +++ b/sbt-app/src/sbt-test/reporter/command-progress/test @@ -0,0 +1,5 @@ +> compile +> +compile +> check +-> asdf +> checkParseError From 86df6e0606e41e6a7ad09367930bbc0a2f10cd23 Mon Sep 17 00:00:00 2001 From: Iulian Dragos Date: Mon, 28 Aug 2023 11:49:29 +0200 Subject: [PATCH 07/10] Deprecate overloaded process method --- main-command/src/main/scala/sbt/Command.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main-command/src/main/scala/sbt/Command.scala b/main-command/src/main/scala/sbt/Command.scala index 15db781c9..15e910bb1 100644 --- a/main-command/src/main/scala/sbt/Command.scala +++ b/main-command/src/main/scala/sbt/Command.scala @@ -185,7 +185,9 @@ object Command { ) // overload instead of default parameter to keep binary compatibility + @deprecated("Use overload that takes the onParseError callback", since = "1.9.4") def process(command: String, state: State): State = process(command, state, _ => ()) + def process(command: String, state: State, onParseError: String => Unit): State = { (if (command.contains(";")) parse(command, state.combinedParser) else parse(command, state.nonMultiParser)) match { From 807f4dfdd2db59879a9846b2f166020f0c73d015 Mon Sep 17 00:00:00 2001 From: adpi2 Date: Wed, 4 Oct 2023 09:50:24 +0200 Subject: [PATCH 08/10] Fix build.sbt --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b284d4a1f..f26ff9067 100644 --- a/build.sbt +++ b/build.sbt @@ -370,7 +370,7 @@ lazy val utilPosition = (project in file("internal") / "util-position") ) lazy val utilLogging = (project in file("internal") / "util-logging") - .enablePlugins(ContrabandPlugin, JsonCodecPlugin + .enablePlugins(ContrabandPlugin, JsonCodecPlugin) .dependsOn(utilInterface, collectionProj, coreMacrosProj) .settings( testedBaseSettings, From 2957f63244d495293f5a523993ddc7e11ee62656 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 11 Oct 2023 21:07:41 +0200 Subject: [PATCH 09/10] Fix build.sbt formatting --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index f26ff9067..17197a6e9 100644 --- a/build.sbt +++ b/build.sbt @@ -179,7 +179,6 @@ def mimaSettingsSince(versions: Seq[String]): Seq[Def.Setting[_]] = Def settings exclude[IncompatibleResultTypeProblem]("sbt.EvaluateTask.executeProgress"), exclude[DirectMissingMethodProblem]("sbt.Keys.currentTaskProgress"), exclude[IncompatibleResultTypeProblem]("sbt.PluginData.copy$default$10") - ), ) From 77e22e9a014c36f56e7862e1a190ea351425b354 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 11 Oct 2023 21:18:22 +0200 Subject: [PATCH 10/10] Small documentation fixes --- main/src/main/scala/sbt/ExecuteProgress2.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/ExecuteProgress2.scala b/main/src/main/scala/sbt/ExecuteProgress2.scala index 41ed6e658..006b8f829 100644 --- a/main/src/main/scala/sbt/ExecuteProgress2.scala +++ b/main/src/main/scala/sbt/ExecuteProgress2.scala @@ -13,6 +13,10 @@ import sbt.internal.util.RMap * Tracks command execution progress. In addition to ExecuteProgress, this interface * adds command start and end events, and gives access to the sbt.State at the beginning * and end of each command. + * + * Command progress callbacks are wrapping task progress callbacks. That is, the `beforeCommand` + * callback will be called before the `initial` callback from ExecuteProgress, and the + * `afterCommand` callback will be called after the `stop` callback from ExecuteProgress. */ trait ExecuteProgress2 extends ExecuteProgress[Task] { @@ -30,7 +34,9 @@ trait ExecuteProgress2 extends ExecuteProgress[Task] { * @param cmd The command string. * @param result Left in case of an error. If the command cannot be parsed, it will be * signalled as a ParseException with a detailed message. If the command - * was cancelled by the user, as sbt.Cancelled. + * was cancelled by the user, as sbt.Cancelled. If the command succeeded, + * Right with the new state after command execution. + * */ def afterCommand(cmd: String, result: Either[Throwable, State]): Unit }