diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala index 3b3b4d125..d6b594578 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala @@ -26,14 +26,16 @@ private[sbt] final class ProgressState( val padding: AtomicInteger, val blankZone: Int, val currentLineBytes: AtomicReference[ArrayBuffer[Byte]], + val maxItems: Int, ) { - def this(blankZone: Int) = - this( - new AtomicReference(Nil), - new AtomicInteger(0), - blankZone, - new AtomicReference(new ArrayBuffer[Byte]), - ) + def this(blankZone: Int, maxItems: Int) = this( + new AtomicReference(Nil), + new AtomicInteger(0), + blankZone, + new AtomicReference(new ArrayBuffer[Byte]), + maxItems, + ) + def this(blankZone: Int) = this(blankZone, 8) def currentLine: Option[String] = new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption .map(EscHelpers.stripColorsAndMoves) @@ -162,14 +164,18 @@ private[sbt] object ProgressState { terminal.withPrintStream { ps => val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name) val info = if (commandFromThisTerminal) { - pe.items.map { item => + val base = pe.items.map { item => val elapsed = item.elapsedMicros / 1000000L s" | => ${item.name} ${elapsed}s" } + val limit = state.maxItems + if (base.size > limit) + s" | ... (${base.size - limit} other tasks)" +: base.takeRight(limit) + else base } else { pe.command.toSeq.flatMap { cmd => val width = terminal.getWidth - val sanitized = if ((cmd.length + SERVER_IS_RUNNING_LENGTH) < width) { + val sanitized = if ((cmd.length + SERVER_IS_RUNNING_LENGTH) > width) { if (SERVER_IS_RUNNING_LENGTH + cmd.length < width) cmd else cmd.take(MIN_COMMAND_WIDTH) + "..." } else cmd diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala index 1137829d3..3c7d7a008 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -141,7 +141,7 @@ trait Terminal extends AutoCloseable { private[sbt] def withPrintStream[T](f: PrintStream => T): T private[sbt] def withRawOutput[R](f: => R): R private[sbt] def restore(): Unit = {} - private[sbt] val progressState = new ProgressState(1) + private[sbt] def progressState: ProgressState private[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Pending) private[sbt] final def prompt: Prompt = promptHolder.get private[sbt] final def setPrompt(newPrompt: Prompt): Unit = @@ -315,6 +315,7 @@ object Terminal { private[this] object ProxyTerminal extends Terminal { private def t: Terminal = activeTerminal.get + override private[sbt] def progressState: ProgressState = t.progressState override def getWidth: Int = t.getWidth override def getHeight: Int = t.getHeight override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line) @@ -755,6 +756,9 @@ object Terminal { private val capabilityMap = org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap + private val consoleProgressState = new AtomicReference[ProgressState](new ProgressState(1)) + private[sbt] def setConsoleProgressState(progressState: ProgressState): Unit = + consoleProgressState.set(progressState) @deprecated("For compatibility only", "1.4.0") private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine @@ -770,6 +774,7 @@ object Terminal { } private[this] val isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") override lazy val isAnsiSupported: Boolean = term.isAnsiSupported && !isCI + override private[sbt] def progressState: ProgressState = consoleProgressState.get override def isEchoEnabled: Boolean = system.echo() override def isSuccessEnabled: Boolean = true override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = @@ -912,6 +917,7 @@ object Terminal { new WriteableInputStream(nullInputStream, "null-writeable-input-stream") private[sbt] val NullTerminal = new Terminal { override def close(): Unit = {} + override private[sbt] def progressState: ProgressState = new ProgressState(1) override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false override def getHeight: Int = 0 override def getLastLine: Option[String] = None diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 7fbf1aec9..2f9a4498b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -97,7 +97,7 @@ import sjsonnew._ import sjsonnew.support.scalajson.unsafe.Converter import scala.collection.immutable.ListMap -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration._ import scala.util.control.NonFatal import scala.xml.NodeSeq @@ -393,11 +393,15 @@ object Defaults extends BuildCommon { else appConfiguration.value.provider.scalaProvider.launcher.topLoader.getParent }, useSuperShell := { if (insideCI.value) false else Terminal.console.isSupershellEnabled }, + superShellThreshold :== SysProp.supershellThreshold, + superShellMaxTasks :== SysProp.supershellMaxTasks, + superShellSleep :== SysProp.supershellSleep.millis, progressReports := { val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector rs map { Keys.TaskProgress(_) } }, - progressState := Some(new ProgressState(SysProp.supershellBlankZone)), + // progressState is deprecated + SettingKey[Option[ProgressState]]("progressState") := None, Previous.cache := new Previous( Def.streamsManagerKey.value, Previous.references.value.getReferences diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index bdc25000c..30cfe2425 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -557,9 +557,13 @@ object Keys { private[sbt] val currentTaskProgress = AttributeKey[TaskProgress]("current-task-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") + val superShellSleep = settingKey[FiniteDuration]("The minimum duration to sleep between progress reports") + val superShellThreshold = settingKey[FiniteDuration]("The minimum amount of time a task must be running to appear in the supershell progress report") val turbo = settingKey[Boolean]("Enables (true) or disables optional performance features.") // This key can be used to add custom ExecuteProgress instances val progressReports = settingKey[Seq[TaskProgress]]("A function that returns a list of progress reporters.").withRank(DTask) + @deprecated("unused", "1.4.0") private[sbt] val progressState = settingKey[Option[ProgressState]]("The optional progress state if supershell is enabled.").withRank(Invisible) private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask) @deprecated("No longer used", "1.3.0") diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 8361d1492..34e6333c6 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -935,13 +935,25 @@ object BuiltinCommands { // This is a workaround for the console task in dotty which uses the classloader cache. // We need to override the top loader in that case so that it gets the forked jline. s5.extendedClassLoaderCache.setParent(Project.extract(s5).get(Keys.scalaInstanceTopLoader)) - CheckBuildSources.init(LintUnused.lintUnusedFunc(s5)) + addSuperShellParams(CheckBuildSources.init(LintUnused.lintUnusedFunc(s5))) } private val setupGlobalFileTreeRepository: State => State = { state => state.get(sbt.nio.Keys.globalFileTreeRepository).foreach(_.close()) state.put(sbt.nio.Keys.globalFileTreeRepository, FileTreeRepository.default) } + private val addSuperShellParams: State => State = (s: State) => { + val extracted = Project.extract(s) + import scala.concurrent.duration._ + val sleep = extracted.getOpt(Keys.superShellSleep).getOrElse(SysProp.supershellSleep.millis) + val threshold = + extracted.getOpt(Keys.superShellThreshold).getOrElse(SysProp.supershellThreshold) + val maxItems = extracted.getOpt(Keys.superShellMaxTasks).getOrElse(SysProp.supershellMaxTasks) + Terminal.setConsoleProgressState(new ProgressState(1, maxItems)) + s.put(Keys.superShellSleep.key, sleep) + .put(Keys.superShellThreshold.key, threshold) + .put(Keys.superShellMaxTasks.key, maxItems) + } private val addCacheStoreFactoryFactory: State => State = (s: State) => { val size = Project .extract(s) diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 801465b8d..7bcea26ce 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -22,8 +22,10 @@ import sbt.protocol._ import sbt.util.{ Logger, LoggerContext } import scala.annotation.tailrec +import scala.concurrent.duration._ import scala.util.control.NonFatal import sbt.internal.FastTrackCommands +import sbt.internal.SysProp object MainLoop { @@ -150,7 +152,11 @@ object MainLoop { def next(state: State): State = { val context = LoggerContext(useLog4J = state.get(Keys.useLog4J.key).getOrElse(false)) - val taskProgress = new TaskProgress + val superShellSleep = + state.get(Keys.superShellSleep.key).getOrElse(SysProp.supershellSleep.millis) + val superShellThreshold = + state.get(Keys.superShellThreshold.key).getOrElse(SysProp.supershellThreshold) + val taskProgress = new TaskProgress(superShellSleep, superShellThreshold) try { ErrorHandling.wideConvert { state diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 6cb4f4ece..505c913c2 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -201,7 +201,8 @@ private[sbt] final class CommandExchange { instance, handlers, s.log, - mkAskUser(name) + mkAskUser(name), + Option(lastState.get), ) subscribe(channel) } diff --git a/main/src/main/scala/sbt/internal/SysProp.scala b/main/src/main/scala/sbt/internal/SysProp.scala index 737485965..2f4e95953 100644 --- a/main/src/main/scala/sbt/internal/SysProp.scala +++ b/main/src/main/scala/sbt/internal/SysProp.scala @@ -104,7 +104,9 @@ object SysProp { def dumbTerm: Boolean = sys.env.get("TERM").contains("dumb") def supershell: Boolean = booleanOpt("sbt.supershell").getOrElse(!dumbTerm && color) - def supershellSleep: Long = long("sbt.supershell.sleep", 100L) + def supershellMaxTasks: Int = int("sbt.supershell.maxitems", 8) + def supershellSleep: Long = long("sbt.supershell.sleep", 500.millis.toMillis) + def supershellThreshold: FiniteDuration = long("sbt.supershell.threshold", 100L).millis def supershellBlankZone: Int = int("sbt.supershell.blankzone", 1) def defaultUseCoursier: Boolean = { diff --git a/main/src/main/scala/sbt/internal/TaskProgress.scala b/main/src/main/scala/sbt/internal/TaskProgress.scala index f292db125..0c3cfbc72 100644 --- a/main/src/main/scala/sbt/internal/TaskProgress.scala +++ b/main/src/main/scala/sbt/internal/TaskProgress.scala @@ -20,13 +20,11 @@ import java.util.concurrent.{ ConcurrentHashMap, Executors, TimeoutException } /** * implements task progress display on the shell. */ -private[sbt] class TaskProgress +private[sbt] class TaskProgress(sleepDuration: FiniteDuration, threshold: FiniteDuration) extends AbstractTaskExecuteProgress with ExecuteProgress[Task] with AutoCloseable { private[this] val lastTaskCount = new AtomicInteger(0) - private[this] val sleepDuration = SysProp.supershellSleep.millis - private[this] val threshold = 10.millis private[this] val reportLoop = new AtomicReference[AutoCloseable] private[this] val active = new ConcurrentHashMap[Task[_], AutoCloseable] private[this] val nextReport = new AtomicReference(Deadline.now) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index ca75b369a..1c7d93484 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -49,6 +49,7 @@ import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter } import BasicJsonProtocol._ import Serialization.{ attach, promptChannel } +import sbt.internal.util.ProgressState final class NetworkChannel( val name: String, @@ -58,7 +59,8 @@ final class NetworkChannel( instance: ServerInstance, handlers: Seq[ServerHandler], val log: Logger, - mkUIThreadImpl: (State, CommandChannel) => UITask + mkUIThreadImpl: (State, CommandChannel) => UITask, + state: Option[State], ) extends CommandChannel { self => def this( name: String, @@ -77,7 +79,8 @@ final class NetworkChannel( instance, handlers, log, - new UITask.AskUserTask(_, _) + new UITask.AskUserTask(_, _), + None ) private val running = new AtomicBoolean(true) @@ -787,6 +790,10 @@ final class NetworkChannel( ) } private[this] val blockedThreads = ConcurrentHashMap.newKeySet[Thread] + override private[sbt] val progressState: ProgressState = new ProgressState( + 1, + state.flatMap(_.get(Keys.superShellMaxTasks.key)).getOrElse(SysProp.supershellMaxTasks) + ) override def getWidth: Int = getProperty(_.width, 0).getOrElse(0) override def getHeight: Int = getProperty(_.height, 0).getOrElse(0) override def isAnsiSupported: Boolean = getProperty(_.isAnsiSupported, false).getOrElse(false)