move super shell rendering to ConsoleAppender

Ref https://github.com/sbt/sbt/issues/4583
This moves the super shell rendering to ConsoleAppender with several improvements.

Instead of scrolling up, supershell is now changed to normal scrolling down, with more traditional cursor position. Before printing out the logs, last known progress reports are wiped out. In addition, there's now 5 lines of blank lines to accomodate for `println(...)` by tasks.
This commit is contained in:
Eugene Yokota 2019-04-04 22:27:33 -04:00
parent 98ec0075f4
commit e28e052b5b
10 changed files with 266 additions and 25 deletions

View File

@ -124,6 +124,8 @@ lazy val utilLogging = (project in internalPath / "util-logging")
exclude[ReversedMissingMethodProblem]("sbt.internal.util.ConsoleOut.flush"), exclude[ReversedMissingMethodProblem]("sbt.internal.util.ConsoleOut.flush"),
// This affects Scala 2.11 only it seems, so it's ok? // This affects Scala 2.11 only it seems, so it's ok?
exclude[InheritedNewAbstractMethodProblem]("sbt.internal.util.codec.JsonProtocol.LogOptionFormat"), exclude[InheritedNewAbstractMethodProblem]("sbt.internal.util.codec.JsonProtocol.LogOptionFormat"),
exclude[InheritedNewAbstractMethodProblem]("sbt.internal.util.codec.JsonProtocol.ProgressItemFormat"),
exclude[InheritedNewAbstractMethodProblem]("sbt.internal.util.codec.JsonProtocol.ProgressEventFormat"),
), ),
) )
.configure(addSbtIO) .configure(addSbtIO)

View File

@ -0,0 +1,59 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.util
/** used by super shell */
final class ProgressEvent private (
val level: String,
val items: Vector[sbt.internal.util.ProgressItem],
val lastTaskCount: Option[Int],
channelName: Option[String],
execId: Option[String]) extends sbt.internal.util.AbstractEntry(channelName, execId) with Serializable {
override def equals(o: Any): Boolean = o match {
case x: ProgressEvent => (this.level == x.level) && (this.items == x.items) && (this.lastTaskCount == x.lastTaskCount) && (this.channelName == x.channelName) && (this.execId == x.execId)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.ProgressEvent".##) + level.##) + items.##) + lastTaskCount.##) + channelName.##) + execId.##)
}
override def toString: String = {
"ProgressEvent(" + level + ", " + items + ", " + lastTaskCount + ", " + channelName + ", " + execId + ")"
}
private[this] def copy(level: String = level, items: Vector[sbt.internal.util.ProgressItem] = items, lastTaskCount: Option[Int] = lastTaskCount, channelName: Option[String] = channelName, execId: Option[String] = execId): ProgressEvent = {
new ProgressEvent(level, items, lastTaskCount, channelName, execId)
}
def withLevel(level: String): ProgressEvent = {
copy(level = level)
}
def withItems(items: Vector[sbt.internal.util.ProgressItem]): ProgressEvent = {
copy(items = items)
}
def withLastTaskCount(lastTaskCount: Option[Int]): ProgressEvent = {
copy(lastTaskCount = lastTaskCount)
}
def withLastTaskCount(lastTaskCount: Int): ProgressEvent = {
copy(lastTaskCount = Option(lastTaskCount))
}
def withChannelName(channelName: Option[String]): ProgressEvent = {
copy(channelName = channelName)
}
def withChannelName(channelName: String): ProgressEvent = {
copy(channelName = Option(channelName))
}
def withExecId(execId: Option[String]): ProgressEvent = {
copy(execId = execId)
}
def withExecId(execId: String): ProgressEvent = {
copy(execId = Option(execId))
}
}
object ProgressEvent {
def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Option[Int], channelName: Option[String], execId: Option[String]): ProgressEvent = new ProgressEvent(level, items, lastTaskCount, channelName, execId)
def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Int, channelName: String, execId: String): ProgressEvent = new ProgressEvent(level, items, Option(lastTaskCount), Option(channelName), Option(execId))
}

View File

@ -0,0 +1,41 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.util
/**
* used by super shell
* @param name name of a task
* @param elapsedMicros current elapsed time in micro seconds
*/
final class ProgressItem private (
val name: String,
val elapsedMicros: Long) extends Serializable {
override def equals(o: Any): Boolean = o match {
case x: ProgressItem => (this.name == x.name) && (this.elapsedMicros == x.elapsedMicros)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (17 + "sbt.internal.util.ProgressItem".##) + name.##) + elapsedMicros.##)
}
override def toString: String = {
"ProgressItem(" + name + ", " + elapsedMicros + ")"
}
private[this] def copy(name: String = name, elapsedMicros: Long = elapsedMicros): ProgressItem = {
new ProgressItem(name, elapsedMicros)
}
def withName(name: String): ProgressItem = {
copy(name = name)
}
def withElapsedMicros(elapsedMicros: Long): ProgressItem = {
copy(elapsedMicros = elapsedMicros)
}
}
object ProgressItem {
def apply(name: String, elapsedMicros: Long): ProgressItem = new ProgressItem(name, elapsedMicros)
}

View File

@ -6,6 +6,6 @@
package sbt.internal.util.codec package sbt.internal.util.codec
import _root_.sjsonnew.JsonFormat import _root_.sjsonnew.JsonFormat
trait AbstractEntryFormats { self: sjsonnew.BasicJsonProtocol with sbt.internal.util.codec.StringEventFormats with sbt.internal.util.codec.TraceEventFormats => trait AbstractEntryFormats { self: sjsonnew.BasicJsonProtocol with sbt.internal.util.codec.StringEventFormats with sbt.internal.util.codec.TraceEventFormats with sbt.internal.util.codec.ProgressItemFormats with sbt.internal.util.codec.ProgressEventFormats =>
implicit lazy val AbstractEntryFormat: JsonFormat[sbt.internal.util.AbstractEntry] = flatUnionFormat2[sbt.internal.util.AbstractEntry, sbt.internal.util.StringEvent, sbt.internal.util.TraceEvent]("type") implicit lazy val AbstractEntryFormat: JsonFormat[sbt.internal.util.AbstractEntry] = flatUnionFormat3[sbt.internal.util.AbstractEntry, sbt.internal.util.StringEvent, sbt.internal.util.TraceEvent, sbt.internal.util.ProgressEvent]("type")
} }

View File

@ -7,6 +7,8 @@ package sbt.internal.util.codec
trait JsonProtocol extends sjsonnew.BasicJsonProtocol trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.internal.util.codec.StringEventFormats with sbt.internal.util.codec.StringEventFormats
with sbt.internal.util.codec.TraceEventFormats with sbt.internal.util.codec.TraceEventFormats
with sbt.internal.util.codec.ProgressItemFormats
with sbt.internal.util.codec.ProgressEventFormats
with sbt.internal.util.codec.AbstractEntryFormats with sbt.internal.util.codec.AbstractEntryFormats
with sbt.internal.util.codec.SuccessEventFormats with sbt.internal.util.codec.SuccessEventFormats
with sbt.internal.util.codec.LogOptionFormats with sbt.internal.util.codec.LogOptionFormats

View File

@ -0,0 +1,35 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.util.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait ProgressEventFormats { self: sbt.internal.util.codec.ProgressItemFormats with sjsonnew.BasicJsonProtocol =>
implicit lazy val ProgressEventFormat: JsonFormat[sbt.internal.util.ProgressEvent] = new JsonFormat[sbt.internal.util.ProgressEvent] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.ProgressEvent = {
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val level = unbuilder.readField[String]("level")
val items = unbuilder.readField[Vector[sbt.internal.util.ProgressItem]]("items")
val lastTaskCount = unbuilder.readField[Option[Int]]("lastTaskCount")
val channelName = unbuilder.readField[Option[String]]("channelName")
val execId = unbuilder.readField[Option[String]]("execId")
unbuilder.endObject()
sbt.internal.util.ProgressEvent(level, items, lastTaskCount, channelName, execId)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.internal.util.ProgressEvent, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("level", obj.level)
builder.addField("items", obj.items)
builder.addField("lastTaskCount", obj.lastTaskCount)
builder.addField("channelName", obj.channelName)
builder.addField("execId", obj.execId)
builder.endObject()
}
}
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.util.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait ProgressItemFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val ProgressItemFormat: JsonFormat[sbt.internal.util.ProgressItem] = new JsonFormat[sbt.internal.util.ProgressItem] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.ProgressItem = {
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val name = unbuilder.readField[String]("name")
val elapsedMicros = unbuilder.readField[Long]("elapsedMicros")
unbuilder.endObject()
sbt.internal.util.ProgressItem(name, elapsedMicros)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.internal.util.ProgressItem, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("name", obj.name)
builder.addField("elapsedMicros", obj.elapsedMicros)
builder.endObject()
}
}
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.util.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TaskProgressFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TaskProgressFormat: JsonFormat[sbt.internal.util.TaskProgress] = new JsonFormat[sbt.internal.util.TaskProgress] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.util.TaskProgress = {
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val name = unbuilder.readField[String]("name")
val elapsedMicros = unbuilder.readField[Option[Long]]("elapsedMicros")
unbuilder.endObject()
sbt.internal.util.TaskProgress(name, elapsedMicros)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.internal.util.TaskProgress, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("name", obj.name)
builder.addField("elapsedMicros", obj.elapsedMicros)
builder.endObject()
}
}
}

View File

@ -22,6 +22,23 @@ type TraceEvent implements sbt.internal.util.AbstractEntry {
execId: String execId: String
} }
## used by super shell
type ProgressEvent implements sbt.internal.util.AbstractEntry {
level: String!
items: [sbt.internal.util.ProgressItem]
lastTaskCount: Int
channelName: String
execId: String
}
## used by super shell
type ProgressItem {
## name of a task
name: String!
## current elapsed time in micro seconds
elapsedMicros: Long!
}
type SuccessEvent { type SuccessEvent {
message: String! message: String!
} }

View File

@ -96,18 +96,19 @@ class ConsoleLogger private[ConsoleLogger] (
} }
object ConsoleAppender { object ConsoleAppender {
private[sbt] final val ScrollUp = "\u001B[S"
private[sbt] def cursorUp(n: Int): String = s"\u001B[${n}A" private[sbt] def cursorUp(n: Int): String = s"\u001B[${n}A"
private[sbt] def cursorDown(n: Int): String = s"\u001B[${n}B" private[sbt] def cursorDown(n: Int): String = s"\u001B[${n}B"
private[sbt] def scrollUp(n: Int): String = s"\u001B[${n}S" private[sbt] def scrollUp(n: Int): String = s"\u001B[${n}S"
private[sbt] final val DeleteLine = "\u001B[2K" private[sbt] final val DeleteLine = "\u001B[2K"
private[sbt] final val CursorLeft1000 = "\u001B[1000D" private[sbt] final val CursorLeft1000 = "\u001B[1000D"
private[sbt] final val CursorDown1 = cursorDown(1)
private[this] val widthHolder: AtomicInteger = new AtomicInteger private[this] val widthHolder: AtomicInteger = new AtomicInteger
private[sbt] def terminalWidth = widthHolder.get private[sbt] def terminalWidth = widthHolder.get
private[sbt] def setTerminalWidth(n: Int): Unit = widthHolder.set(n) private[sbt] def setTerminalWidth(n: Int): Unit = widthHolder.set(n)
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false) private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b) def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
def showProgress: Boolean = showProgressHolder.get def showProgress: Boolean = showProgressHolder.get
private[sbt] val lastTaskCount = new AtomicInteger(0)
/** Hide stack trace altogether. */ /** Hide stack trace altogether. */
val noSuppressedMessage = (_: SuppressedTraceContext) => None val noSuppressedMessage = (_: SuppressedTraceContext) => None
@ -454,23 +455,18 @@ class ConsoleAppender private[ConsoleAppender] (
appendLog(SUCCESS_LABEL_COLOR, Level.SuccessLabel, SUCCESS_MESSAGE_COLOR, message) appendLog(SUCCESS_LABEL_COLOR, Level.SuccessLabel, SUCCESS_MESSAGE_COLOR, message)
} }
// leave some blank lines for tasks that might use println(...)
private val blankZone = 5
private def write(msg: String): Unit = { private def write(msg: String): Unit = {
if (!useFormat || !ansiCodesSupported) { if (!useFormat || !ansiCodesSupported) {
out.println(EscHelpers.removeEscapeSequences(msg)) out.println(EscHelpers.removeEscapeSequences(msg))
} else if (ConsoleAppender.showProgress) { } else if (ConsoleAppender.showProgress) {
val textLength = msg.length - 5 val clearNum = lastTaskCount.get + blankZone
val scrollNum = if (clearNum > 1) {
if (ConsoleAppender.terminalWidth == 0) 1 deleteConsoleLines(clearNum)
else (textLength / ConsoleAppender.terminalWidth) + 1 out.print(s"${cursorUp(clearNum)}")
if (scrollNum > 1) {
out.print(s"${cursorDown(1)}$DeleteLine" * (scrollNum - 1) + s"${cursorUp(scrollNum - 1)}")
} }
out.print( out.println(msg)
s"$ScrollUp$DeleteLine$msg${CursorLeft1000}" + (
if (scrollNum <= 1) ""
else scrollUp(scrollNum - 1)
)
)
out.flush() out.flush()
} else { } else {
out.println(msg) out.println(msg)
@ -497,19 +493,50 @@ class ConsoleAppender private[ConsoleAppender] (
codec.showLines(te).toVector foreach { appendLog(Level.Error, _) } codec.showLines(te).toVector foreach { appendLog(Level.Error, _) }
} }
private def appendProgressEvent(pe: ProgressEvent): Unit =
if (ConsoleAppender.showProgress) {
out.lockObject.synchronized {
deleteConsoleLines(blankZone)
val currentTasksCount = pe.items.size
val ltc = pe.lastTaskCount.getOrElse(0)
val sorted = pe.items.sortBy(_.name).sortBy(x => -x.elapsedMicros)
sorted foreach { item =>
val elapsed = item.elapsedMicros / 1000000L
out.println(s"$DeleteLine | => ${item.name} ${elapsed}s")
}
if (ltc > currentTasksCount) deleteConsoleLines(ltc - currentTasksCount)
else ()
out.print(cursorUp(math.max(currentTasksCount, ltc) + blankZone))
out.flush()
lastTaskCount.set(ltc)
}
} else ()
private def deleteConsoleLines(n: Int): Unit = {
(1 to n) foreach { _ =>
out.println(DeleteLine)
}
}
private def appendMessageContent(level: Level.Value, o: AnyRef): Unit = { private def appendMessageContent(level: Level.Value, o: AnyRef): Unit = {
def appendEvent(oe: ObjectEvent[_]): Unit = { def appendEvent(oe: ObjectEvent[_]): Unit = {
val contentType = oe.contentType val contentType = oe.contentType
if (contentType == "sbt.internal.util.TraceEvent") { contentType match {
appendTraceEvent(oe.message.asInstanceOf[TraceEvent]) case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent])
} else case "sbt.internal.util.ProgressEvent" =>
LogExchange.stringCodec[AnyRef](contentType) match { appendProgressEvent(oe.message.asInstanceOf[ProgressEvent])
case Some(codec) if contentType == "sbt.internal.util.SuccessEvent" => case _ =>
codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach { success(_) } LogExchange.stringCodec[AnyRef](contentType) match {
case Some(codec) => case Some(codec) if contentType == "sbt.internal.util.SuccessEvent" =>
codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach (appendLog(level, _)) codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach { success(_) }
case _ => appendLog(level, oe.message.toString) case Some(codec) =>
} codec.showLines(oe.message.asInstanceOf[AnyRef]).toVector foreach (appendLog(
level,
_
))
case _ => appendLog(level, oe.message.toString)
}
}
} }
o match { o match {