From d532b498023aae4fae256632dc76de057e89168f Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 18 Sep 2024 10:06:24 +0800 Subject: [PATCH 001/116] wip --- .../src/mill/util/MultilineStatusLogger.scala | 70 +++++++++++++++++++ runner/src/mill/runner/MillMain.scala | 15 ++-- 2 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 main/util/src/mill/util/MultilineStatusLogger.scala diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala new file mode 100644 index 00000000000..1f449d71f25 --- /dev/null +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -0,0 +1,70 @@ +package mill.util + +import mill.api.SystemStreams + +import java.io._ + +class MultilineStatusLogger( + override val colored: Boolean, + val enableTicker: Boolean, + override val infoColor: fansi.Attrs, + override val errorColor: fansi.Attrs, + val systemStreams: SystemStreams, + override val debugEnabled: Boolean, +) extends ColorLogger { + import MultilineStatusLogger.Status + val p = new PrintWriter(systemStreams.err) + val nav = new AnsiNav(p) + + val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] + def currentPrompt: String = { + val now = System.currentTimeMillis() + current + .map{case (threadId, statuses) => + val statusesString = statuses + .map{status => + val runtimeSeconds = ((now - status.startTimeMillis) / 1000).toInt + s"${runtimeSeconds}s ${status.text}" + }.mkString(" / ") + s"#$threadId $statusesString" + } + .mkString("\n") + } + def currentHeight: Int = currentPrompt.linesIterator.length + + def info(s: String): Unit = synchronized { + nav.up(currentHeight) + nav.left(9999) + systemStreams.err.println(currentPrompt) + } + + def error(s: String): Unit = synchronized { + nav.up(currentHeight) + nav.left(9999) + systemStreams.err.println(currentPrompt) + } + + def threadStatus(threadId: Int, text: String) = text match { + case null => current(threadId) = current(threadId).init + case _ => current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), text) + } + + def ticker(s: String): Unit = ??? + + + def debug(s: String): Unit = synchronized { + if (debugEnabled) { + nav.up(currentHeight) + nav.left(9999) + systemStreams.err.println(currentPrompt) + } + } + + override def rawOutputStream: PrintStream = systemStreams.out +} + +object MultilineStatusLogger { + case class Status(startTimeMillis: Long, text: String) +} + + diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index d026eac3e04..cf9ba784be9 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -8,7 +8,7 @@ import mill.java9rtexport.Export import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal} import mill.bsp.{BspContext, BspServerResult} import mill.main.BuildInfo -import mill.util.PrintLogger +import mill.util.{MultilineStatusLogger, PrintLogger} import java.lang.reflect.InvocationTargetException import scala.util.control.NonFatal @@ -97,8 +97,7 @@ object MillMain { initialSystemProperties: Map[String, String], systemExit: Int => Nothing ): (Boolean, RunnerState) = { - val printLoggerState = new PrintLogger.State() - val streams = PrintLogger.wrapSystemStreams(streams0, printLoggerState) + val streams = streams0 SystemStreams.withStreams(streams) { os.SubProcess.env.withValue(env) { MillCliConfigParser.parse(args) match { @@ -164,7 +163,6 @@ object MillMain { config.ticker .orElse(config.enableTicker) .orElse(Option.when(config.disableTicker.value)(false)), - printLoggerState ) if (!config.silent.value) { checkMillVersionFromFile(WorkspaceRoot.workspaceRoot, streams.err) @@ -306,20 +304,17 @@ object MillMain { config: MillCliConfig, mainInteractive: Boolean, enableTicker: Option[Boolean], - printLoggerState: PrintLogger.State - ): PrintLogger = { + ): MultilineStatusLogger = { val colored = config.color.getOrElse(mainInteractive) val colors = if (colored) mill.util.Colors.Default else mill.util.Colors.BlackWhite - val logger = new mill.util.PrintLogger( + val logger = new MultilineStatusLogger( colored = colored, enableTicker = enableTicker.getOrElse(mainInteractive), infoColor = colors.info, errorColor = colors.error, systemStreams = streams, - debugEnabled = config.debugLog.value, - context = "", - printLoggerState + debugEnabled = config.debugLog.value ) logger } From 52d41631d46d7e38e3027b46eef6f0267b453f9c Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 18 Sep 2024 11:55:15 +0800 Subject: [PATCH 002/116] . --- main/api/src/mill/api/Logger.scala | 8 ++ main/eval/src/mill/eval/GroupEvaluator.scala | 126 +++++++++--------- .../src/mill/util/MultilineStatusLogger.scala | 59 +++++--- 3 files changed, 108 insertions(+), 85 deletions(-) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 3c6967558e9..8e09ad617e7 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,6 +44,14 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit + def withTicker[T](s: Option[String])(t: => T): T = s match{ + case None => t + case Some(s) => + ticker(s) + try t + finally ticker("") + } + def debug(s: String): Unit /** diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 7d1529801ea..67731ca79b5 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -242,83 +242,83 @@ private[mill] trait GroupEvaluator { inputResults.forall(_.result.isInstanceOf[Result.Success[_]]) } - val tickerPrefix = maybeTargetLabel.map { targetLabel => - val prefix = s"[$counterMsg] $targetLabel " - if (logRun && enableTicker) logger.ticker(prefix) - prefix + "| " + val tickerPrefix = maybeTargetLabel.collect { + case targetLabel if logRun && enableTicker => s"$counterMsg $targetLabel " } + logger.withTicker(tickerPrefix) { + val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { + override def ticker(s: String): Unit = { + if (enableTicker) super.ticker(tickerPrefix.getOrElse("") + s) + else () // do nothing + } - val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { - override def ticker(s: String): Unit = { - if (enableTicker) super.ticker(tickerPrefix.getOrElse("") + s) - else () // do nothing + override def rawOutputStream: PrintStream = logger.rawOutputStream } - override def rawOutputStream: PrintStream = logger.rawOutputStream - } - - var usedDest = Option.empty[os.Path] - for (task <- nonEvaluatedTargets) { - newEvaluated.append(task) - val targetInputValues = task.inputs - .map { x => newResults.getOrElse(x, results(x).result) } - .collect { case Result.Success((v, _)) => v } - - def makeDest() = this.synchronized { - paths match { - case Some(dest) => - if (usedDest.isEmpty) os.makeDir.all(dest.dest) - usedDest = Some(dest.dest) - dest.dest - - case None => throw new Exception("No `dest` folder available here") + var usedDest = Option.empty[os.Path] + for (task <- nonEvaluatedTargets) { + newEvaluated.append(task) + val targetInputValues = task.inputs + .map { x => newResults.getOrElse(x, results(x).result) } + .collect { case Result.Success((v, _)) => v } + + def makeDest() = this.synchronized { + paths match { + case Some(dest) => + if (usedDest.isEmpty) os.makeDir.all(dest.dest) + usedDest = Some(dest.dest) + dest.dest + + case None => throw new Exception("No `dest` folder available here") + } } - } - val res = { - if (targetInputValues.length != task.inputs.length) Result.Skipped - else { - val args = new mill.api.Ctx( - args = targetInputValues.map(_.value).toIndexedSeq, - dest0 = () => makeDest(), - log = multiLogger, - home = home, - env = env, - reporter = reporter, - testReporter = testReporter, - workspace = workspace, - systemExit = systemExit - ) with mill.api.Ctx.Jobs { - override def jobs: Int = effectiveThreadCount - } + val res = { + if (targetInputValues.length != task.inputs.length) Result.Skipped + else { + val args = new mill.api.Ctx( + args = targetInputValues.map(_.value).toIndexedSeq, + dest0 = () => makeDest(), + log = multiLogger, + home = home, + env = env, + reporter = reporter, + testReporter = testReporter, + workspace = workspace, + systemExit = systemExit + ) with mill.api.Ctx.Jobs { + override def jobs: Int = effectiveThreadCount + } - os.dynamicPwdFunction.withValue(() => makeDest()) { - mill.api.SystemStreams.withStreams(multiLogger.systemStreams) { - try task.evaluate(args).map(Val(_)) - catch { - case f: Result.Failing[Val] => f - case NonFatal(e) => - Result.Exception( - e, - new OuterStack(new Exception().getStackTrace.toIndexedSeq) - ) + os.dynamicPwdFunction.withValue(() => makeDest()) { + mill.api.SystemStreams.withStreams(multiLogger.systemStreams) { + try task.evaluate(args).map(Val(_)) + catch { + case f: Result.Failing[Val] => f + case NonFatal(e) => + Result.Exception( + e, + new OuterStack(new Exception().getStackTrace.toIndexedSeq) + ) + } } } } } - } - newResults(task) = for (v <- res) yield { - ( - v, - if (task.isInstanceOf[Worker[_]]) inputsHash - else v.## - ) + newResults(task) = for (v <- res) yield { + ( + v, + if (task.isInstanceOf[Worker[_]]) inputsHash + else v.## + ) + } + } - } - multiLogger.close() - (newResults, newEvaluated) + multiLogger.close() + (newResults, newEvaluated) + } } val (newResults, newEvaluated) = computeAll(enableTicker = true) diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 1f449d71f25..6f0ff266675 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -17,47 +17,62 @@ class MultilineStatusLogger( val nav = new AnsiNav(p) val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] - def currentPrompt: String = { + def currentPrompt: Seq[String] = { val now = System.currentTimeMillis() current - .map{case (threadId, statuses) => + .collect{case (threadId, statuses) if statuses.nonEmpty => val statusesString = statuses .map{status => val runtimeSeconds = ((now - status.startTimeMillis) / 1000).toInt s"${runtimeSeconds}s ${status.text}" }.mkString(" / ") - s"#$threadId $statusesString" + s"| $statusesString" } - .mkString("\n") + .toList } - def currentHeight: Int = currentPrompt.linesIterator.length + def currentHeight: Int = currentPrompt.length - def info(s: String): Unit = synchronized { + private def updateUI() = { +// println("-"*80 + currentHeight) nav.up(currentHeight) nav.left(9999) - systemStreams.err.println(currentPrompt) + p.flush() } - - def error(s: String): Unit = synchronized { - nav.up(currentHeight) - nav.left(9999) - systemStreams.err.println(currentPrompt) + private def log0(s: String) = { + updateUI() + printClear(s +: currentPrompt) } - - def threadStatus(threadId: Int, text: String) = text match { - case null => current(threadId) = current(threadId).init - case _ => current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), text) + private def printClear(lines: Seq[String]) = { + for(line <- lines){ + nav.clearLine(0) + p.println(line) + } } - def ticker(s: String): Unit = ??? + def info(s: String): Unit = synchronized { log0(s); p.flush() } + def error(s: String): Unit = synchronized { log0(s); p.flush() } - def debug(s: String): Unit = synchronized { - if (debugEnabled) { - nav.up(currentHeight) - nav.left(9999) - systemStreams.err.println(currentPrompt) + def ticker(s: String): Unit = synchronized { + updateUI() + val prevHeight = currentHeight + val threadId = Thread.currentThread().getId.toInt + if (s.contains("")) current(threadId) = current(threadId).init + else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + + val verticalSpacerHeight = prevHeight - currentHeight + printClear(currentPrompt) + if (verticalSpacerHeight > 0){ + val verticalSpacer = Seq.fill(verticalSpacerHeight)("") + printClear(verticalSpacer) + nav.up(verticalSpacerHeight) } + p.flush() + } + + + def debug(s: String): Unit = synchronized { + if (debugEnabled) log0(s) } override def rawOutputStream: PrintStream = systemStreams.out From 35fcf43b0a149911ff947e62efbdbc313bc1d825 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 18 Sep 2024 16:04:34 +0800 Subject: [PATCH 003/116] works when not scrolling, but scrolling breaks save/restore cursor --- main/util/src/mill/util/AnsiNav.scala | 9 +- .../src/mill/util/MultilineStatusLogger.scala | 92 +++++++++++++------ main/util/src/mill/util/PrintLogger.scala | 5 +- runner/src/mill/runner/MillMain.scala | 2 +- 4 files changed, 71 insertions(+), 37 deletions(-) diff --git a/main/util/src/mill/util/AnsiNav.scala b/main/util/src/mill/util/AnsiNav.scala index a2b49ed5520..b2f4348905b 100644 --- a/main/util/src/mill/util/AnsiNav.scala +++ b/main/util/src/mill/util/AnsiNav.scala @@ -1,9 +1,12 @@ package mill.util -import java.io.Writer +import java.io.{PrintStream, OutputStream, OutputStreamWriter, Writer} -class AnsiNav(output: Writer) { - def control(n: Int, c: Char): Unit = output.write("\u001b[" + n + c) +// Reference https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 +case class AnsiNav(output: PrintStream) { + def saveCursor() = output.print("\u001b7") + def restoreCursor() = output.print("\u001b8") + def control(n: Int, c: Char): Unit = output.print("\u001b[" + n + c) /** * Move up `n` squares diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 6f0ff266675..38dfbe323be 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -9,16 +9,17 @@ class MultilineStatusLogger( val enableTicker: Boolean, override val infoColor: fansi.Attrs, override val errorColor: fansi.Attrs, - val systemStreams: SystemStreams, + systemStreams0: SystemStreams, override val debugEnabled: Boolean, ) extends ColorLogger { - import MultilineStatusLogger.Status - val p = new PrintWriter(systemStreams.err) - val nav = new AnsiNav(p) + import MultilineStatusLogger._ + AnsiNav(systemStreams0.err).saveCursor() + val systemStreams = wrapSystemStreams(systemStreams0) val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] def currentPrompt: Seq[String] = { val now = System.currentTimeMillis() + List("="*80) ++ current .collect{case (threadId, statuses) if statuses.nonEmpty => val statusesString = statuses @@ -32,42 +33,27 @@ class MultilineStatusLogger( } def currentHeight: Int = currentPrompt.length - private def updateUI() = { -// println("-"*80 + currentHeight) - nav.up(currentHeight) - nav.left(9999) - p.flush() - } private def log0(s: String) = { - updateUI() - printClear(s +: currentPrompt) + systemStreams.err.println(s) } - private def printClear(lines: Seq[String]) = { - for(line <- lines){ - nav.clearLine(0) - p.println(line) - } + + private def printClear(nav: AnsiNav, lines: Seq[String]) = { + for(line <- lines) nav.output.println(line) + nav.output.flush() } - def info(s: String): Unit = synchronized { log0(s); p.flush() } + def info(s: String): Unit = synchronized { log0(s); systemStreams0.err.flush() } - def error(s: String): Unit = synchronized { log0(s); p.flush() } + def error(s: String): Unit = synchronized { log0(s); systemStreams0.err.flush() } def ticker(s: String): Unit = synchronized { - updateUI() - val prevHeight = currentHeight + val threadId = Thread.currentThread().getId.toInt + if (s.contains("")) current(threadId) = current(threadId).init else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) - val verticalSpacerHeight = prevHeight - currentHeight - printClear(currentPrompt) - if (verticalSpacerHeight > 0){ - val verticalSpacer = Seq.fill(verticalSpacerHeight)("") - printClear(verticalSpacer) - nav.up(verticalSpacerHeight) - } - p.flush() + systemStreams.err.write(Array[Byte]()) } @@ -75,7 +61,53 @@ class MultilineStatusLogger( if (debugEnabled) log0(s) } - override def rawOutputStream: PrintStream = systemStreams.out + + override def rawOutputStream: PrintStream = systemStreams0.out + def wrapSystemStreams(systemStreams0: SystemStreams): SystemStreams = { + new SystemStreams( + new PrintStream(new StateStream(systemStreams0.out)), + new PrintStream(new StateStream(systemStreams0.err)), + systemStreams0.in + ) + } + + class StateStream(wrapped: PrintStream) extends OutputStream { + + def wrapWrite[T](t: => T): T = { + AnsiNav(wrapped).restoreCursor() + AnsiNav(wrapped).saveCursor() + AnsiNav(wrapped).down(1) + AnsiNav(wrapped).clearScreen(0) + AnsiNav(wrapped).restoreCursor() + val res = t + AnsiNav(wrapped).saveCursor() + AnsiNav(wrapped).down(1) + printClear(AnsiNav(wrapped), currentPrompt) + AnsiNav(wrapped).restoreCursor() + AnsiNav(wrapped).saveCursor() + flush() + res + } + + override def write(b: Array[Byte]): Unit = synchronized { + wrapWrite(write(b)) + flush() + } + + override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { + wrapWrite(wrapped.write(b, off, len)) + flush() + } + + override def write(b: Int): Unit = synchronized { + wrapWrite(wrapped.write(b)) + flush() + } + + override def flush(): Unit = synchronized { + wrapped.flush() + } + } } object MultilineStatusLogger { diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index f59f65d66cd..060927fea5a 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -33,13 +33,12 @@ class PrintLogger( systemStreams.err.println() systemStreams.err.println(infoColor(s)) case PrintLogger.State.Ticker => - val p = new PrintWriter(systemStreams.err) // Need to make this more "atomic" - val nav = new AnsiNav(p) + val nav = new AnsiNav(systemStreams.err) nav.up(1) nav.clearLine(2) nav.left(9999) - p.flush() + systemStreams.err.flush() systemStreams.err.println(infoColor(s)) } diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index cf9ba784be9..64af8b32095 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -313,7 +313,7 @@ object MillMain { enableTicker = enableTicker.getOrElse(mainInteractive), infoColor = colors.info, errorColor = colors.error, - systemStreams = streams, + systemStreams0 = streams, debugEnabled = config.debugLog.value ) logger From 8cdca12ec0d31bd73be4ffd20fa054ac709ca4e1 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 18 Sep 2024 19:50:09 +0800 Subject: [PATCH 004/116] . --- main/util/src/mill/util/AnsiNav.scala | 4 +- .../src/mill/util/MultilineStatusLogger.scala | 55 +++++++------------ 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/main/util/src/mill/util/AnsiNav.scala b/main/util/src/mill/util/AnsiNav.scala index b2f4348905b..9dac3590858 100644 --- a/main/util/src/mill/util/AnsiNav.scala +++ b/main/util/src/mill/util/AnsiNav.scala @@ -1,11 +1,9 @@ package mill.util -import java.io.{PrintStream, OutputStream, OutputStreamWriter, Writer} +import java.io.PrintStream // Reference https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 case class AnsiNav(output: PrintStream) { - def saveCursor() = output.print("\u001b7") - def restoreCursor() = output.print("\u001b8") def control(n: Int, c: Char): Unit = output.print("\u001b[" + n + c) /** diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 38dfbe323be..7d11fcd6aa1 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -13,7 +13,6 @@ class MultilineStatusLogger( override val debugEnabled: Boolean, ) extends ColorLogger { import MultilineStatusLogger._ - AnsiNav(systemStreams0.err).saveCursor() val systemStreams = wrapSystemStreams(systemStreams0) val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] @@ -32,16 +31,11 @@ class MultilineStatusLogger( .toList } def currentHeight: Int = currentPrompt.length - + var previousHeight: Int = currentHeight private def log0(s: String) = { systemStreams.err.println(s) } - private def printClear(nav: AnsiNav, lines: Seq[String]) = { - for(line <- lines) nav.output.println(line) - nav.output.flush() - } - def info(s: String): Unit = synchronized { log0(s); systemStreams0.err.flush() } def error(s: String): Unit = synchronized { log0(s); systemStreams0.err.flush() } @@ -49,11 +43,10 @@ class MultilineStatusLogger( def ticker(s: String): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt - - if (s.contains("")) current(threadId) = current(threadId).init - else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) - - systemStreams.err.write(Array[Byte]()) + writeAndUpdatePrompt(systemStreams0.err) { + if (s.contains("")) current(threadId) = current(threadId).init + else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + } } @@ -71,37 +64,29 @@ class MultilineStatusLogger( ) } - class StateStream(wrapped: PrintStream) extends OutputStream { + def writeAndUpdatePrompt[T](wrapped: PrintStream)(t: => T): T = { + AnsiNav(wrapped).up(previousHeight) + AnsiNav(wrapped).left(9999) + AnsiNav(wrapped).clearScreen(0) + val res = t + for(line <- currentPrompt) wrapped.println(line) + previousHeight = currentHeight + wrapped.flush() + res + } - def wrapWrite[T](t: => T): T = { - AnsiNav(wrapped).restoreCursor() - AnsiNav(wrapped).saveCursor() - AnsiNav(wrapped).down(1) - AnsiNav(wrapped).clearScreen(0) - AnsiNav(wrapped).restoreCursor() - val res = t - AnsiNav(wrapped).saveCursor() - AnsiNav(wrapped).down(1) - printClear(AnsiNav(wrapped), currentPrompt) - AnsiNav(wrapped).restoreCursor() - AnsiNav(wrapped).saveCursor() - flush() - res - } + class StateStream(wrapped: PrintStream) extends OutputStream { override def write(b: Array[Byte]): Unit = synchronized { - wrapWrite(write(b)) - flush() + writeAndUpdatePrompt(wrapped)(write(b)) } override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { - wrapWrite(wrapped.write(b, off, len)) - flush() + writeAndUpdatePrompt(wrapped)(wrapped.write(b, off, len)) } override def write(b: Int): Unit = synchronized { - wrapWrite(wrapped.write(b)) - flush() + writeAndUpdatePrompt(wrapped)(wrapped.write(b)) } override def flush(): Unit = synchronized { @@ -113,5 +98,3 @@ class MultilineStatusLogger( object MultilineStatusLogger { case class Status(startTimeMillis: Long, text: String) } - - From 2c615ab943017f1f9c0fe698909d11fc18427b18 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 18 Sep 2024 21:49:46 +0800 Subject: [PATCH 005/116] . --- .../src/mill/util/MultilineStatusLogger.scala | 54 +++-- runner/src/mill/runner/MillMain.scala | 219 +++++++++--------- 2 files changed, 149 insertions(+), 124 deletions(-) diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 7d11fcd6aa1..76b58a77b5a 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -11,27 +11,50 @@ class MultilineStatusLogger( override val errorColor: fansi.Attrs, systemStreams0: SystemStreams, override val debugEnabled: Boolean, -) extends ColorLogger { +) extends ColorLogger with AutoCloseable{ + val startTimeMillis = System.currentTimeMillis() import MultilineStatusLogger._ - val systemStreams = wrapSystemStreams(systemStreams0) + val systemStreams = new SystemStreams( + new PrintStream(new StateStream(systemStreams0.out)), + new PrintStream(new StateStream(systemStreams0.err)), + systemStreams0.in + ) + + override def close() = running = false + @volatile var running = true + @volatile var dirty = false + + val thread = new Thread(new Runnable{ + def run(): Unit = { + while(running) { + Thread.sleep(1000) + synchronized{ + writeAndUpdatePrompt(systemStreams0.err) {/*donothing*/} + } + } + } + }) + + thread.start() + def renderSeconds(millis: Long) = (millis / 1000).toInt match{ + case 0 => "" + case n => s" ${n}s" + } val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] def currentPrompt: Seq[String] = { val now = System.currentTimeMillis() - List("="*80) ++ + List("="*80 + renderSeconds(now - startTimeMillis)) ++ current .collect{case (threadId, statuses) if statuses.nonEmpty => val statusesString = statuses - .map{status => - val runtimeSeconds = ((now - status.startTimeMillis) / 1000).toInt - s"${runtimeSeconds}s ${status.text}" - }.mkString(" / ") - s"| $statusesString" + .map{status => status.text + renderSeconds(now - status.startTimeMillis)} + .mkString(" / ") + statusesString } .toList } def currentHeight: Int = currentPrompt.length - var previousHeight: Int = currentHeight private def log0(s: String) = { systemStreams.err.println(s) } @@ -56,21 +79,14 @@ class MultilineStatusLogger( override def rawOutputStream: PrintStream = systemStreams0.out - def wrapSystemStreams(systemStreams0: SystemStreams): SystemStreams = { - new SystemStreams( - new PrintStream(new StateStream(systemStreams0.out)), - new PrintStream(new StateStream(systemStreams0.err)), - systemStreams0.in - ) - } - def writeAndUpdatePrompt[T](wrapped: PrintStream)(t: => T): T = { - AnsiNav(wrapped).up(previousHeight) + + private def writeAndUpdatePrompt[T](wrapped: PrintStream)(t: => T): T = { AnsiNav(wrapped).left(9999) AnsiNav(wrapped).clearScreen(0) val res = t for(line <- currentPrompt) wrapped.println(line) - previousHeight = currentHeight + AnsiNav(wrapped).up(currentHeight) wrapped.flush() res } diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 64af8b32095..5ece44dfd55 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -112,25 +112,32 @@ object MillMain { case Right(config) if config.showVersion.value => def p(k: String, d: String = "") = System.getProperty(k, d) + streams.out.println( s"""Mill Build Tool version ${BuildInfo.millVersion} - |Java version: ${p("java.version", "" - )} - |OS name: "${p("os.name")}", version: ${p("os.version")}, arch: ${p( + ) + } + |OS name: "${p("os.name")}", version: ${p("os.version")}, arch: ${ + p( "os.arch" - )}""".stripMargin + ) + }""".stripMargin ) (true, RunnerState.empty) case Right(config) - if ( - config.interactive.value || config.noServer.value || config.bsp.value + if ( + config.interactive.value || config.noServer.value || config.bsp.value ) && streams.in.getClass == classOf[PipedInputStream] => // because we have stdin as dummy, we assume we were already started in server process streams.err.println( @@ -139,11 +146,11 @@ object MillMain { (false, RunnerState.empty) case Right(config) - if Seq( - config.interactive.value, - config.noServer.value, - config.bsp.value - ).count(identity) > 1 => + if Seq( + config.interactive.value, + config.noServer.value, + config.bsp.value + ).count(identity) > 1 => streams.err.println( "Only one of -i/--interactive, --no-server or --bsp may be given" ) @@ -164,116 +171,118 @@ object MillMain { .orElse(config.enableTicker) .orElse(Option.when(config.disableTicker.value)(false)), ) - if (!config.silent.value) { - checkMillVersionFromFile(WorkspaceRoot.workspaceRoot, streams.err) - } + try { + if (!config.silent.value) { + checkMillVersionFromFile(WorkspaceRoot.workspaceRoot, streams.err) + } - // special BSP mode, in which we spawn a server and register the current evaluator when-ever we start to eval a dedicated command - val bspMode = config.bsp.value && config.leftoverArgs.value.isEmpty - val maybeThreadCount = - parseThreadCount(config.threadCountRaw, Runtime.getRuntime.availableProcessors()) + // special BSP mode, in which we spawn a server and register the current evaluator when-ever we start to eval a dedicated command + val bspMode = config.bsp.value && config.leftoverArgs.value.isEmpty + val maybeThreadCount = + parseThreadCount(config.threadCountRaw, Runtime.getRuntime.availableProcessors()) - val (success, nextStateCache) = { - if (config.repl.value) { - logger.error("The --repl mode is no longer supported.") - (false, stateCache) + val (success, nextStateCache) = { + if (config.repl.value) { + logger.error("The --repl mode is no longer supported.") + (false, stateCache) - } else if (!bspMode && config.leftoverArgs.value.isEmpty) { - println(MillCliConfigParser.shortUsageText) + } else if (!bspMode && config.leftoverArgs.value.isEmpty) { + println(MillCliConfigParser.shortUsageText) - (true, stateCache) + (true, stateCache) - } else if (maybeThreadCount.isLeft) { - logger.error(maybeThreadCount.swap.toOption.get) - (false, stateCache) + } else if (maybeThreadCount.isLeft) { + logger.error(maybeThreadCount.swap.toOption.get) + (false, stateCache) - } else { - val userSpecifiedProperties = - userSpecifiedProperties0 ++ config.extraSystemProperties + } else { + val userSpecifiedProperties = + userSpecifiedProperties0 ++ config.extraSystemProperties - val threadCount = Some(maybeThreadCount.toOption.get) + val threadCount = Some(maybeThreadCount.toOption.get) - if (mill.main.client.Util.isJava9OrAbove) { - val rt = config.home / Export.rtJarName - if (!os.exists(rt)) { - logger.errorStream.println( - s"Preparing Java ${System.getProperty("java.version")} runtime; this may take a minute or two ..." - ) - Export.rtTo(rt.toIO, false) + if (mill.main.client.Util.isJava9OrAbove) { + val rt = config.home / Export.rtJarName + if (!os.exists(rt)) { + logger.errorStream.println( + s"Preparing Java ${System.getProperty("java.version")} runtime; this may take a minute or two ..." + ) + Export.rtTo(rt.toIO, false) + } } - } - val bspContext = - if (bspMode) Some(new BspContext(streams, bspLog, config.home)) else None - - val bspCmd = "mill.bsp.BSP/startSession" - val targetsAndParams = - bspContext - .map(_ => Seq(bspCmd)) - .getOrElse(config.leftoverArgs.value.toList) - - var repeatForBsp = true - var loopRes: (Boolean, RunnerState) = (false, RunnerState.empty) - while (repeatForBsp) { - repeatForBsp = false - - val (isSuccess, evalStateOpt) = Watching.watchLoop( - logger = logger, - ringBell = config.ringBell.value, - watch = config.watch.value, - streams = streams, - setIdle = setIdle, - evaluate = (prevState: Option[RunnerState]) => { - adjustJvmProperties(userSpecifiedProperties, initialSystemProperties) - - new MillBuildBootstrap( - projectRoot = WorkspaceRoot.workspaceRoot, - home = config.home, - keepGoing = config.keepGoing.value, - imports = config.imports, - env = env, - threadCount = threadCount, - targetsAndParams = targetsAndParams, - prevRunnerState = prevState.getOrElse(stateCache), - logger = logger, - disableCallgraph = config.disableCallgraph.value, - needBuildSc = needBuildSc(config), - requestedMetaLevel = config.metaLevel, - config.allowPositional.value, - systemExit = systemExit - ).evaluate() - } - ) + val bspContext = + if (bspMode) Some(new BspContext(streams, bspLog, config.home)) else None + + val bspCmd = "mill.bsp.BSP/startSession" + val targetsAndParams = + bspContext + .map(_ => Seq(bspCmd)) + .getOrElse(config.leftoverArgs.value.toList) + + var repeatForBsp = true + var loopRes: (Boolean, RunnerState) = (false, RunnerState.empty) + while (repeatForBsp) { + repeatForBsp = false + + val (isSuccess, evalStateOpt) = Watching.watchLoop( + logger = logger, + ringBell = config.ringBell.value, + watch = config.watch.value, + streams = streams, + setIdle = setIdle, + evaluate = (prevState: Option[RunnerState]) => { + adjustJvmProperties(userSpecifiedProperties, initialSystemProperties) + + new MillBuildBootstrap( + projectRoot = WorkspaceRoot.workspaceRoot, + home = config.home, + keepGoing = config.keepGoing.value, + imports = config.imports, + env = env, + threadCount = threadCount, + targetsAndParams = targetsAndParams, + prevRunnerState = prevState.getOrElse(stateCache), + logger = logger, + disableCallgraph = config.disableCallgraph.value, + needBuildSc = needBuildSc(config), + requestedMetaLevel = config.metaLevel, + config.allowPositional.value, + systemExit = systemExit + ).evaluate() + } + ) + bspContext.foreach { ctx => + repeatForBsp = + BspContext.bspServerHandle.lastResult == Some(BspServerResult.ReloadWorkspace) + logger.error( + s"`$bspCmd` returned with ${BspContext.bspServerHandle.lastResult}" + ) + } + loopRes = (isSuccess, evalStateOpt) + } // while repeatForBsp bspContext.foreach { ctx => - repeatForBsp = - BspContext.bspServerHandle.lastResult == Some(BspServerResult.ReloadWorkspace) logger.error( - s"`$bspCmd` returned with ${BspContext.bspServerHandle.lastResult}" + s"Exiting BSP runner loop. Stopping BSP server. Last result: ${BspContext.bspServerHandle.lastResult}" ) + BspContext.bspServerHandle.stop() } - loopRes = (isSuccess, evalStateOpt) - } // while repeatForBsp - bspContext.foreach { ctx => - logger.error( - s"Exiting BSP runner loop. Stopping BSP server. Last result: ${BspContext.bspServerHandle.lastResult}" - ) - BspContext.bspServerHandle.stop() - } - // return with evaluation result - loopRes + // return with evaluation result + loopRes + } } - } - if (config.ringBell.value) { - if (success) println("\u0007") - else { - println("\u0007") - Thread.sleep(250) - println("\u0007") + if (config.ringBell.value) { + if (success) println("\u0007") + else { + println("\u0007") + Thread.sleep(250) + println("\u0007") + } } - } - (success, nextStateCache) + (success, nextStateCache) + } finally logger.close() } } } From 277feef8f978fa71f15d1f0e7048188c302cc622 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 03:48:25 +0800 Subject: [PATCH 006/116] wip trying to buffer output --- .../src/mill/util/MultilineStatusLogger.scala | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 76b58a77b5a..9d3f8c9593f 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -14,9 +14,10 @@ class MultilineStatusLogger( ) extends ColorLogger with AutoCloseable{ val startTimeMillis = System.currentTimeMillis() import MultilineStatusLogger._ + val systemStreams = new SystemStreams( - new PrintStream(new StateStream(systemStreams0.out)), - new PrintStream(new StateStream(systemStreams0.err)), + new PrintStream(new BufferedOutputStream(new StateStream(systemStreams0.out))), + new PrintStream(new BufferedOutputStream(new StateStream(systemStreams0.err))), systemStreams0.in ) @@ -24,7 +25,7 @@ class MultilineStatusLogger( @volatile var running = true @volatile var dirty = false - val thread = new Thread(new Runnable{ + val secondsTickerThread = new Thread(new Runnable{ def run(): Unit = { while(running) { Thread.sleep(1000) @@ -35,7 +36,20 @@ class MultilineStatusLogger( } }) - thread.start() + secondsTickerThread.start() + val bufferFlusherThread = new Thread(new Runnable{ + def run(): Unit = { + while(running) { + Thread.sleep(10) + synchronized{ + systemStreams.err.flush() + systemStreams.out.flush() + } + } + } + }) + + bufferFlusherThread.start() def renderSeconds(millis: Long) = (millis / 1000).toInt match{ case 0 => "" From c9bc44a380b3696ac330b981a86d1a3c535a76aa Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 09:13:06 +0800 Subject: [PATCH 007/116] track newlines in wrapped multiline prompt stream --- main/util/src/mill/util/MultilineStatusLogger.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 9d3f8c9593f..486e3bdc466 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -108,15 +108,18 @@ class MultilineStatusLogger( class StateStream(wrapped: PrintStream) extends OutputStream { override def write(b: Array[Byte]): Unit = synchronized { - writeAndUpdatePrompt(wrapped)(write(b)) + if (b.last == '\n') writeAndUpdatePrompt(wrapped)(write(b)) + else write(b) } override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { - writeAndUpdatePrompt(wrapped)(wrapped.write(b, off, len)) + if (b(off + len - 1) == '\n') writeAndUpdatePrompt(wrapped)(wrapped.write(b, off, len)) + else wrapped.write(b, off, len) } override def write(b: Int): Unit = synchronized { - writeAndUpdatePrompt(wrapped)(wrapped.write(b)) + if (b == '\n') writeAndUpdatePrompt(wrapped)(wrapped.write(b)) + else wrapped.write(b) } override def flush(): Unit = synchronized { From 6c8f3c1e1e342ba21c76954801d5feb65bc28c01 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 09:53:03 +0800 Subject: [PATCH 008/116] wip --- main/api/src/mill/api/Logger.scala | 1 + .../src/mill/util/MultilineStatusLogger.scala | 58 +++++++++++----- .../util/MultilineStatusLoggerTests.scala | 69 +++++++++++++++++++ scalalib/src/mill/scalalib/ScalaModule.scala | 32 +++++---- 4 files changed, 128 insertions(+), 32 deletions(-) create mode 100644 main/util/test/src/mill/util/MultilineStatusLoggerTests.scala diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 8e09ad617e7..2553682fdb4 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -61,4 +61,5 @@ trait Logger { def debugEnabled: Boolean = false def close(): Unit = () + def withPaused[T](t: => T) = t } diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 486e3bdc466..1208e8ac1cd 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -21,16 +21,23 @@ class MultilineStatusLogger( systemStreams0.in ) - override def close() = running = false - @volatile var running = true - @volatile var dirty = false + override def close() = stopped = true + @volatile var stopped = true + @volatile var paused = false + override def withPaused[T](t: => T) = { + paused = true + try t + finally paused = false + } val secondsTickerThread = new Thread(new Runnable{ def run(): Unit = { - while(running) { + while(!stopped) { Thread.sleep(1000) - synchronized{ - writeAndUpdatePrompt(systemStreams0.err) {/*donothing*/} + if (!paused) { + synchronized{ + writeAndUpdatePrompt(systemStreams0.err) {/*donothing*/} + } } } } @@ -39,11 +46,13 @@ class MultilineStatusLogger( secondsTickerThread.start() val bufferFlusherThread = new Thread(new Runnable{ def run(): Unit = { - while(running) { + while(!stopped) { Thread.sleep(10) - synchronized{ - systemStreams.err.flush() - systemStreams.out.flush() + if (!paused) { + synchronized { + systemStreams.err.flush() + systemStreams.out.flush() + } } } } @@ -68,6 +77,7 @@ class MultilineStatusLogger( } .toList } + var currentPromptString = "" def currentHeight: Int = currentPrompt.length private def log0(s: String) = { systemStreams.err.println(s) @@ -83,6 +93,8 @@ class MultilineStatusLogger( writeAndUpdatePrompt(systemStreams0.err) { if (s.contains("")) current(threadId) = current(threadId).init else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + + currentPromptString = currentPrompt.mkString("\n") } } @@ -99,7 +111,7 @@ class MultilineStatusLogger( AnsiNav(wrapped).left(9999) AnsiNav(wrapped).clearScreen(0) val res = t - for(line <- currentPrompt) wrapped.println(line) + wrapped.println(currentPromptString) AnsiNav(wrapped).up(currentHeight) wrapped.flush() res @@ -107,14 +119,15 @@ class MultilineStatusLogger( class StateStream(wrapped: PrintStream) extends OutputStream { - override def write(b: Array[Byte]): Unit = synchronized { - if (b.last == '\n') writeAndUpdatePrompt(wrapped)(write(b)) - else write(b) - } + override def write(b: Array[Byte]): Unit = synchronized { write(b, 0, b.length) } override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { - if (b(off + len - 1) == '\n') writeAndUpdatePrompt(wrapped)(wrapped.write(b, off, len)) - else wrapped.write(b, off, len) + lastIndexOfNewline(b, off, len) match{ + case -1 => write(b) + case n => + writeAndUpdatePrompt(wrapped)(write(b, 0, n)) + write(b, n, b.length - n) + } } override def write(b: Int): Unit = synchronized { @@ -130,4 +143,15 @@ class MultilineStatusLogger( object MultilineStatusLogger { case class Status(startTimeMillis: Long, text: String) + + def lastIndexOfNewline(b: Array[Byte], off: Int, len: Int): Int = { + var index = off + len - 1 + while (true) { + if (index < off) return -1 + else if (b(index) == '\n') return index + else index -= 1 + } + ??? + } + } diff --git a/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala b/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala new file mode 100644 index 00000000000..88b33fe6dc7 --- /dev/null +++ b/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala @@ -0,0 +1,69 @@ +package mill.util + +import utest._ + +object MultilineStatusLoggerTests extends TestSuite { + + val tests = Tests { + test { + // Fuzz test to make sure our custom fast `lastIndexOfNewline` logic behaves + // the same as a slower generic implementation using `.slice.lastIndexOf` + val allSampleByteArrays = Seq[Array[Byte]]( + Array(1), + Array('\n'), + + Array(1, 1), + Array(1, '\n'), + Array('\n', 1), + Array('\n', '\n'), + + Array(1, 1, 1), + + Array(1, 1, '\n'), + Array(1, '\n', 1), + Array('\n', 1, 1), + + Array(1, '\n', '\n'), + Array('\n', 1, '\n'), + Array('\n', '\n', 1), + + Array('\n', '\n', '\n'), + + Array(1, 1, 1, 1), + + Array(1, 1, 1, '\n'), + Array(1, 1, '\n', 1), + Array(1, '\n', 1, 1), + Array('\n', 1, 1, 1), + + Array(1, 1, '\n', '\n'), + Array(1, '\n', '\n', 1), + Array('\n', '\n', 1, 1), + Array(1, '\n', 1, '\n'), + Array('\n', 1, '\n', 1), + Array('\n', 1, 1, '\n'), + + Array('\n', '\n', '\n', 1), + Array('\n', '\n', 1, '\n'), + Array('\n', 1, '\n', '\n'), + Array(1, '\n', '\n', '\n'), + + Array('\n', '\n', '\n', '\n'), + ) + + for(sample <- allSampleByteArrays){ + for(start <- Range(0, sample.length)){ + for(len <- Range(0, sample.length - start)){ + val found = MultilineStatusLogger.lastIndexOfNewline(sample, start, len) + val expected0 = sample.slice(start, start + len).lastIndexOf('\n') + val expected = expected0 + start + def assertMsg = s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" + if (expected0 == -1) Predef.assert(found == -1, assertMsg) + else Predef.assert(found == expected, assertMsg) + } + } + } + } + + } +} diff --git a/scalalib/src/mill/scalalib/ScalaModule.scala b/scalalib/src/mill/scalalib/ScalaModule.scala index 027dd9d38cd..ed20f824ae5 100644 --- a/scalalib/src/mill/scalalib/ScalaModule.scala +++ b/scalalib/src/mill/scalalib/ScalaModule.scala @@ -437,21 +437,23 @@ trait ScalaModule extends JavaModule with TestModule.ScalaModuleBase { outer => Result.Failure("console needs to be run with the -i/--interactive flag") } else { val useJavaCp = "-usejavacp" - SystemStreams.withStreams(SystemStreams.original) { - Jvm.runSubprocess( - mainClass = - if (ZincWorkerUtil.isDottyOrScala3(scalaVersion())) - "dotty.tools.repl.Main" - else - "scala.tools.nsc.MainGenericRunner", - classPath = runClasspath().map(_.path) ++ scalaCompilerClasspath().map( - _.path - ), - jvmArgs = forkArgs(), - envArgs = forkEnv(), - mainArgs = Seq(useJavaCp) ++ consoleScalacOptions().filterNot(Set(useJavaCp)), - workingDir = forkWorkingDir() - ) + T.log.withPaused { + SystemStreams.withStreams(SystemStreams.original) { + Jvm.runSubprocess( + mainClass = + if (ZincWorkerUtil.isDottyOrScala3(scalaVersion())) + "dotty.tools.repl.Main" + else + "scala.tools.nsc.MainGenericRunner", + classPath = runClasspath().map(_.path) ++ scalaCompilerClasspath().map( + _.path + ), + jvmArgs = forkArgs(), + envArgs = forkEnv(), + mainArgs = Seq(useJavaCp) ++ consoleScalacOptions().filterNot(Set(useJavaCp)), + workingDir = forkWorkingDir() + ) + } } Result.Success(()) } From da050ab0b162ca03b310a5faff2fb43d5b3262a0 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 10:14:12 +0800 Subject: [PATCH 009/116] works --- main/util/src/mill/util/MultilineStatusLogger.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 1208e8ac1cd..87d9af0b715 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -122,14 +122,17 @@ class MultilineStatusLogger( override def write(b: Array[Byte]): Unit = synchronized { write(b, 0, b.length) } override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { + lastIndexOfNewline(b, off, len) match{ - case -1 => write(b) - case n => - writeAndUpdatePrompt(wrapped)(write(b, 0, n)) - write(b, n, b.length - n) + case -1 => wrapped.write(b, off, len) + case lastNewlineIndex => + val indexOfCharAfterNewline = lastNewlineIndex + 1 + writeAndUpdatePrompt(wrapped)(wrapped.write(b, off, indexOfCharAfterNewline - off)) + wrapped.write(b, indexOfCharAfterNewline, off + len - indexOfCharAfterNewline) } } + override def write(b: Int): Unit = synchronized { if (b == '\n') writeAndUpdatePrompt(wrapped)(wrapped.write(b)) else wrapped.write(b) From 4ff88de1fd8c1723440a84f17166dcf4c4903d83 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 10:39:15 +0800 Subject: [PATCH 010/116] wip --- main/util/src/mill/util/MultilineStatusLogger.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 87d9af0b715..dece55e127a 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -16,8 +16,8 @@ class MultilineStatusLogger( import MultilineStatusLogger._ val systemStreams = new SystemStreams( - new PrintStream(new BufferedOutputStream(new StateStream(systemStreams0.out))), - new PrintStream(new BufferedOutputStream(new StateStream(systemStreams0.err))), + new PrintStream(new StateStream(systemStreams0.out)), + new PrintStream(new StateStream(systemStreams0.err)), systemStreams0.in ) @@ -108,8 +108,8 @@ class MultilineStatusLogger( private def writeAndUpdatePrompt[T](wrapped: PrintStream)(t: => T): T = { - AnsiNav(wrapped).left(9999) AnsiNav(wrapped).clearScreen(0) + AnsiNav(wrapped).left(9999) val res = t wrapped.println(currentPromptString) AnsiNav(wrapped).up(currentHeight) From 771be38e3c9f69d421c2a68008afa3668caac465 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 11:04:47 +0800 Subject: [PATCH 011/116] wip --- main/util/src/mill/util/AnsiNav.scala | 21 ++++-- .../src/mill/util/MultilineStatusLogger.scala | 65 ++++++++----------- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/main/util/src/mill/util/AnsiNav.scala b/main/util/src/mill/util/AnsiNav.scala index 9dac3590858..3f142341b51 100644 --- a/main/util/src/mill/util/AnsiNav.scala +++ b/main/util/src/mill/util/AnsiNav.scala @@ -4,27 +4,28 @@ import java.io.PrintStream // Reference https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 case class AnsiNav(output: PrintStream) { - def control(n: Int, c: Char): Unit = output.print("\u001b[" + n + c) + def control(n: Int, c: Char): Unit = output.print(AnsiNav.control(n, c)) /** * Move up `n` squares */ - def up(n: Int): Any = if (n == 0) "" else control(n, 'A') + def up(n: Int): Any = if (n != 0) control(n, 'A') /** * Move down `n` squares */ - def down(n: Int): Any = if (n == 0) "" else control(n, 'B') + def down(n: Int): Any = if (n != 0) control(n, 'B') /** * Move right `n` squares */ - def right(n: Int): Any = if (n == 0) "" else control(n, 'C') + def right(n: Int): Any = if (n != 0) control(n, 'C') + /** * Move left `n` squares */ - def left(n: Int): Any = if (n == 0) "" else control(n, 'D') + def left(n: Int): Any = if (n != 0) control(n, 'D') /** * Clear the screen @@ -43,4 +44,14 @@ case class AnsiNav(output: PrintStream) { * n=2: clear entire line */ def clearLine(n: Int): Unit = control(n, 'K') + +} +object AnsiNav{ + def control(n: Int, c: Char): String = "\u001b[" + n + c + def up(n: Int): String = if (n != 0) control(n, 'A') else "" + def down(n: Int): String = if (n != 0) control(n, 'B') else "" + def right(n: Int): String = if (n != 0) control(n, 'C') else "" + def left(n: Int): String = if (n != 0) control(n, 'D') else "" + def clearScreen(n: Int): String = control(n, 'J') + def clearLine(n: Int): String = control(n, 'K') } diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index dece55e127a..04fe1b50dd8 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -22,7 +22,7 @@ class MultilineStatusLogger( ) override def close() = stopped = true - @volatile var stopped = true + @volatile var stopped = false @volatile var paused = false override def withPaused[T](t: => T) = { paused = true @@ -36,7 +36,9 @@ class MultilineStatusLogger( Thread.sleep(1000) if (!paused) { synchronized{ + updatePromptBytes() writeAndUpdatePrompt(systemStreams0.err) {/*donothing*/} + systemStreams0.err.flush() } } } @@ -44,41 +46,15 @@ class MultilineStatusLogger( }) secondsTickerThread.start() - val bufferFlusherThread = new Thread(new Runnable{ - def run(): Unit = { - while(!stopped) { - Thread.sleep(10) - if (!paused) { - synchronized { - systemStreams.err.flush() - systemStreams.out.flush() - } - } - } - } - }) - - bufferFlusherThread.start() - def renderSeconds(millis: Long) = (millis / 1000).toInt match{ + private def renderSeconds(millis: Long) = (millis / 1000).toInt match{ case 0 => "" case n => s" ${n}s" } - val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] - def currentPrompt: Seq[String] = { - val now = System.currentTimeMillis() - List("="*80 + renderSeconds(now - startTimeMillis)) ++ - current - .collect{case (threadId, statuses) if statuses.nonEmpty => - val statusesString = statuses - .map{status => status.text + renderSeconds(now - status.startTimeMillis)} - .mkString(" / ") - statusesString - } - .toList - } - var currentPromptString = "" - def currentHeight: Int = currentPrompt.length + + private val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] + + var currentPromptBytes = Array[Byte]() private def log0(s: String) = { systemStreams.err.println(s) } @@ -94,26 +70,37 @@ class MultilineStatusLogger( if (s.contains("")) current(threadId) = current(threadId).init else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) - currentPromptString = currentPrompt.mkString("\n") + updatePromptBytes() } } + private def updatePromptBytes() = { + val now = System.currentTimeMillis() + val currentPrompt = List("="*80 + renderSeconds(now - startTimeMillis)) ++ + current + .collect{case (threadId, statuses) if statuses.nonEmpty => + val statusesString = statuses + .map{status => status.text + renderSeconds(now - status.startTimeMillis)} + .mkString(" / ") + statusesString + } + .toList + val currentHeight = currentPrompt.length + currentPromptBytes = (currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes + } def debug(s: String): Unit = synchronized { if (debugEnabled) log0(s) } - override def rawOutputStream: PrintStream = systemStreams0.out + private val writePreludeBytes = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes private def writeAndUpdatePrompt[T](wrapped: PrintStream)(t: => T): T = { - AnsiNav(wrapped).clearScreen(0) - AnsiNav(wrapped).left(9999) + wrapped.write(writePreludeBytes) val res = t - wrapped.println(currentPromptString) - AnsiNav(wrapped).up(currentHeight) - wrapped.flush() + wrapped.write(currentPromptBytes) res } From 81311f29ae904381c3184215f40d35f434a64f50 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 11:05:01 +0800 Subject: [PATCH 012/116] wip --- main/util/src/mill/util/MultilineStatusLogger.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala index 04fe1b50dd8..8605c70af9c 100644 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ b/main/util/src/mill/util/MultilineStatusLogger.scala @@ -54,7 +54,7 @@ class MultilineStatusLogger( private val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] - var currentPromptBytes = Array[Byte]() + private var currentPromptBytes = Array[Byte]() private def log0(s: String) = { systemStreams.err.println(s) } From 641f55ed88a87c9d03c64fa5b95e9377aed10118 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 11:47:55 +0800 Subject: [PATCH 013/116] . --- build.mill | 1 + main/package.mill | 2 +- .../src/mill/util/MultilinePromptLogger.scala | 153 ++++++++++++++++++ .../src/mill/util/MultilineStatusLogger.scala | 147 ----------------- .../util/MultilineStatusLoggerTests.scala | 2 +- .../mill/runner/client/MillClientMain.java | 4 +- runner/src/mill/runner/MillCliConfig.scala | 9 +- runner/src/mill/runner/MillMain.scala | 73 +++++---- 8 files changed, 208 insertions(+), 183 deletions(-) create mode 100644 main/util/src/mill/util/MultilinePromptLogger.scala delete mode 100644 main/util/src/mill/util/MultilineStatusLogger.scala diff --git a/build.mill b/build.mill index a4039a6288d..ff8b4367849 100644 --- a/build.mill +++ b/build.mill @@ -141,6 +141,7 @@ object Deps { ) val jline = ivy"org.jline:jline:3.26.3" + val jansi = ivy"org.fusesource.jansi:jansi:2.4.1" val jnaVersion = "5.14.0" val jna = ivy"net.java.dev.jna:jna:${jnaVersion}" val jnaPlatform = ivy"net.java.dev.jna:jna-platform:${jnaVersion}" diff --git a/main/package.mill b/main/package.mill index a2696ff9d48..7a670373f02 100644 --- a/main/package.mill +++ b/main/package.mill @@ -171,7 +171,7 @@ object `package` extends RootModule with build.MillStableScalaModule with BuildI object client extends build.MillPublishJavaModule with BuildInfo { def buildInfoPackageName = "mill.main.client" def buildInfoMembers = Seq(BuildInfo.Value("millVersion", build.millVersion(), "Mill version.")) - def ivyDeps = Agg(build.Deps.junixsocket) + def ivyDeps = Agg(build.Deps.junixsocket, build.Deps.jansi) object test extends JavaModuleTests with TestModule.Junit4 { def ivyDeps = Agg( diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala new file mode 100644 index 00000000000..f88095f6308 --- /dev/null +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -0,0 +1,153 @@ +package mill.util + +import mill.api.SystemStreams + +import java.io._ + +class MultilinePromptLogger( + override val colored: Boolean, + val enableTicker: Boolean, + override val infoColor: fansi.Attrs, + override val errorColor: fansi.Attrs, + systemStreams0: SystemStreams, + override val debugEnabled: Boolean, +) extends ColorLogger with AutoCloseable{ + import MultilinePromptLogger._ + private val state = new State(systemStreams0, System.currentTimeMillis()) + + val systemStreams = new SystemStreams( + new PrintStream(new StateStream(systemStreams0.out)), + new PrintStream(new StateStream(systemStreams0.err)), + systemStreams0.in + ) + + override def close() = stopped = true + + @volatile var stopped = false + @volatile var paused = false + + override def withPaused[T](t: => T) = { + paused = true + try t + finally paused = false + } + + val secondsTickerThread = new Thread(() => { + while(!stopped) { + Thread.sleep(1000) + if (!paused) { + synchronized{ + state.refreshPrompt() + } + } + } + }) + + secondsTickerThread.start() + + def info(s: String): Unit = synchronized { systemStreams.err.println(s) } + + def error(s: String): Unit = synchronized { systemStreams.err.println(s) } + + def ticker(s: String): Unit = synchronized { + state.writeWithPrompt(systemStreams0.err) { + state.updateCurrent(s) + } + } + + + def debug(s: String): Unit = synchronized { + if (debugEnabled) systemStreams.err.println(s) + } + + override def rawOutputStream: PrintStream = systemStreams0.out + + class StateStream(wrapped: PrintStream) extends OutputStream { + override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { + lastIndexOfNewline(b, off, len) match{ + case -1 => wrapped.write(b, off, len) + case lastNewlineIndex => + val indexOfCharAfterNewline = lastNewlineIndex + 1 + // We look for the last newline in the output and use that as an anchor, since + // we know that after a newline the cursor is at column zero, and column zero + // is the only place we can reliably position the cursor since the saveCursor and + // restoreCursor ANSI codes do not work well in the presence of scrolling + state.writeWithPrompt(wrapped){ + wrapped.write(b, off, indexOfCharAfterNewline - off) + } + wrapped.write(b, indexOfCharAfterNewline, off + len - indexOfCharAfterNewline) + } + } + + override def write(b: Int): Unit = synchronized { + if (b == '\n') state.writeWithPrompt(wrapped)(wrapped.write(b)) + else wrapped.write(b) + } + + override def flush(): Unit = synchronized { + wrapped.flush() + } + } +} + +object MultilinePromptLogger { + case class Status(startTimeMillis: Long, text: String) + + private class State(systemStreams0: SystemStreams, startTimeMillis: Long) { + val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] + + // Pre-compute the prelude and current prompt as byte arrays so that + // writing them out is fast, since they get written out very frequently + val writePreludeBytes = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes + var currentPromptBytes = Array[Byte]() + private def updatePromptBytes() = { + val now = System.currentTimeMillis() + val currentPrompt = List("="*80 + renderSeconds(now - startTimeMillis)) ++ + current + .collect{case (threadId, statuses) if statuses.nonEmpty => + val statusesString = statuses + .map{status => status.text + renderSeconds(now - status.startTimeMillis)} + .mkString(" / ") + statusesString + } + .toList + + val currentHeight = currentPrompt.length + currentPromptBytes = (currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes + } + + def updateCurrent(s: String) = synchronized{ + val threadId = Thread.currentThread().getId.toInt + if (s.endsWith("")) current(threadId) = current(threadId).init + else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + updatePromptBytes() + } + + def writeWithPrompt[T](wrapped: PrintStream)(t: => T): T = synchronized{ + wrapped.write(writePreludeBytes) + val res = t + wrapped.write(currentPromptBytes) + res + } + + def refreshPrompt() = synchronized{ + updatePromptBytes() + systemStreams0.err.write(currentPromptBytes) + } + + private def renderSeconds(millis: Long) = (millis / 1000).toInt match{ + case 0 => "" + case n => s" ${n}s" + } + } + + def lastIndexOfNewline(b: Array[Byte], off: Int, len: Int): Int = { + var index = off + len - 1 + while (true) { + if (index < off) return -1 + else if (b(index) == '\n') return index + else index -= 1 + } + ??? + } +} diff --git a/main/util/src/mill/util/MultilineStatusLogger.scala b/main/util/src/mill/util/MultilineStatusLogger.scala deleted file mode 100644 index 8605c70af9c..00000000000 --- a/main/util/src/mill/util/MultilineStatusLogger.scala +++ /dev/null @@ -1,147 +0,0 @@ -package mill.util - -import mill.api.SystemStreams - -import java.io._ - -class MultilineStatusLogger( - override val colored: Boolean, - val enableTicker: Boolean, - override val infoColor: fansi.Attrs, - override val errorColor: fansi.Attrs, - systemStreams0: SystemStreams, - override val debugEnabled: Boolean, -) extends ColorLogger with AutoCloseable{ - val startTimeMillis = System.currentTimeMillis() - import MultilineStatusLogger._ - - val systemStreams = new SystemStreams( - new PrintStream(new StateStream(systemStreams0.out)), - new PrintStream(new StateStream(systemStreams0.err)), - systemStreams0.in - ) - - override def close() = stopped = true - @volatile var stopped = false - @volatile var paused = false - override def withPaused[T](t: => T) = { - paused = true - try t - finally paused = false - } - - val secondsTickerThread = new Thread(new Runnable{ - def run(): Unit = { - while(!stopped) { - Thread.sleep(1000) - if (!paused) { - synchronized{ - updatePromptBytes() - writeAndUpdatePrompt(systemStreams0.err) {/*donothing*/} - systemStreams0.err.flush() - } - } - } - } - }) - - secondsTickerThread.start() - - private def renderSeconds(millis: Long) = (millis / 1000).toInt match{ - case 0 => "" - case n => s" ${n}s" - } - - private val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] - - private var currentPromptBytes = Array[Byte]() - private def log0(s: String) = { - systemStreams.err.println(s) - } - - def info(s: String): Unit = synchronized { log0(s); systemStreams0.err.flush() } - - def error(s: String): Unit = synchronized { log0(s); systemStreams0.err.flush() } - - def ticker(s: String): Unit = synchronized { - - val threadId = Thread.currentThread().getId.toInt - writeAndUpdatePrompt(systemStreams0.err) { - if (s.contains("")) current(threadId) = current(threadId).init - else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) - - updatePromptBytes() - } - } - private def updatePromptBytes() = { - val now = System.currentTimeMillis() - val currentPrompt = List("="*80 + renderSeconds(now - startTimeMillis)) ++ - current - .collect{case (threadId, statuses) if statuses.nonEmpty => - val statusesString = statuses - .map{status => status.text + renderSeconds(now - status.startTimeMillis)} - .mkString(" / ") - statusesString - } - .toList - - val currentHeight = currentPrompt.length - currentPromptBytes = (currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes - } - - def debug(s: String): Unit = synchronized { - if (debugEnabled) log0(s) - } - - override def rawOutputStream: PrintStream = systemStreams0.out - - private val writePreludeBytes = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes - - private def writeAndUpdatePrompt[T](wrapped: PrintStream)(t: => T): T = { - wrapped.write(writePreludeBytes) - val res = t - wrapped.write(currentPromptBytes) - res - } - - class StateStream(wrapped: PrintStream) extends OutputStream { - - override def write(b: Array[Byte]): Unit = synchronized { write(b, 0, b.length) } - - override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { - - lastIndexOfNewline(b, off, len) match{ - case -1 => wrapped.write(b, off, len) - case lastNewlineIndex => - val indexOfCharAfterNewline = lastNewlineIndex + 1 - writeAndUpdatePrompt(wrapped)(wrapped.write(b, off, indexOfCharAfterNewline - off)) - wrapped.write(b, indexOfCharAfterNewline, off + len - indexOfCharAfterNewline) - } - } - - - override def write(b: Int): Unit = synchronized { - if (b == '\n') writeAndUpdatePrompt(wrapped)(wrapped.write(b)) - else wrapped.write(b) - } - - override def flush(): Unit = synchronized { - wrapped.flush() - } - } -} - -object MultilineStatusLogger { - case class Status(startTimeMillis: Long, text: String) - - def lastIndexOfNewline(b: Array[Byte], off: Int, len: Int): Int = { - var index = off + len - 1 - while (true) { - if (index < off) return -1 - else if (b(index) == '\n') return index - else index -= 1 - } - ??? - } - -} diff --git a/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala b/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala index 88b33fe6dc7..bcc97c89809 100644 --- a/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala @@ -54,7 +54,7 @@ object MultilineStatusLoggerTests extends TestSuite { for(sample <- allSampleByteArrays){ for(start <- Range(0, sample.length)){ for(len <- Range(0, sample.length - start)){ - val found = MultilineStatusLogger.lastIndexOfNewline(sample, start, len) + val found = MultilinePromptLogger.lastIndexOfNewline(sample, start, len) val expected0 = sample.slice(start, start + len).lastIndexOf('\n') val expected = expected0 + start def assertMsg = s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" diff --git a/runner/client/src/mill/runner/client/MillClientMain.java b/runner/client/src/mill/runner/client/MillClientMain.java index d22b87a1274..240e4faadbf 100644 --- a/runner/client/src/mill/runner/client/MillClientMain.java +++ b/runner/client/src/mill/runner/client/MillClientMain.java @@ -1,9 +1,8 @@ package mill.runner.client; import java.util.Arrays; -import java.util.function.BiConsumer; import mill.main.client.ServerLauncher; -import mill.main.client.ServerFiles; +import org.fusesource.jansi.AnsiConsole; import mill.main.client.Util; import mill.main.client.lock.Locks; import mill.main.client.OutFiles; @@ -15,6 +14,7 @@ */ public class MillClientMain { public static void main(String[] args) throws Exception { + AnsiConsole.systemInstall(); boolean runNoServer = false; if (args.length > 0) { String firstArg = args[0]; diff --git a/runner/src/mill/runner/MillCliConfig.scala b/runner/src/mill/runner/MillCliConfig.scala index 0c23cc8cca3..a57875e1c17 100644 --- a/runner/src/mill/runner/MillCliConfig.scala +++ b/runner/src/mill/runner/MillCliConfig.scala @@ -123,7 +123,14 @@ case class MillCliConfig( ) metaLevel: Option[Int] = None, @arg(doc = "Allows command args to be passed positionally without `--arg` by default") - allowPositional: Flag = Flag() + allowPositional: Flag = Flag(), + @arg( + doc = """ + Disables the new multi-line status prompt used for showing thread + status at the command line and falls back to the legacy ticker + """ + ) + disablePrompt: Flag = Flag(), ) import mainargs.ParserForClass diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 5ece44dfd55..6a2a2a1b02a 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -8,7 +8,8 @@ import mill.java9rtexport.Export import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal} import mill.bsp.{BspContext, BspServerResult} import mill.main.BuildInfo -import mill.util.{MultilineStatusLogger, PrintLogger} +import mill.util.{MultilinePromptLogger, PrintLogger} +import org.fusesource.jansi.AnsiConsole import java.lang.reflect.InvocationTargetException import scala.util.control.NonFatal @@ -34,6 +35,7 @@ object MillMain { } def main(args: Array[String]): Unit = { + AnsiConsole.systemInstall() val initialSystemStreams = new SystemStreams(System.out, System.err, System.in) // setup streams val (runnerStreams, cleanupStreams, bspLog) = @@ -97,6 +99,7 @@ object MillMain { initialSystemProperties: Map[String, String], systemExit: Int => Nothing ): (Boolean, RunnerState) = { + val printLoggerState = new PrintLogger.State() val streams = streams0 SystemStreams.withStreams(streams) { os.SubProcess.env.withValue(env) { @@ -111,33 +114,25 @@ object MillMain { (true, RunnerState.empty) case Right(config) if config.showVersion.value => - def p(k: String, d: String = "") = System.getProperty(k, d) - + def prop(k: String) = System.getProperty(k, s"") + val javaVersion = prop("java.version") + val javaVendor = prop("java.vendor") + val javaHome = prop("java.home") + val fileEncoding = prop("file.encoding") + val osName = prop("os.name") + val osVersion = prop("os.version") + val osArch = prop("os.arch") streams.out.println( s"""Mill Build Tool version ${BuildInfo.millVersion} - |Java version: ${p("java.version", "" - ) - } - |OS name: "${p("os.name")}", version: ${p("os.version")}, arch: ${ - p( - "os.arch" - ) - }""".stripMargin + |Java version: $javaVersion, vendor: $javaVendor, runtime: $javaHome + |Default locale: ${Locale.getDefault()}, platform encoding: $fileEncoding + |OS name: "$osName", version: $osVersion, arch: $osArch""".stripMargin ) (true, RunnerState.empty) case Right(config) - if ( - config.interactive.value || config.noServer.value || config.bsp.value + if ( + config.interactive.value || config.noServer.value || config.bsp.value ) && streams.in.getClass == classOf[PipedInputStream] => // because we have stdin as dummy, we assume we were already started in server process streams.err.println( @@ -170,6 +165,7 @@ object MillMain { config.ticker .orElse(config.enableTicker) .orElse(Option.when(config.disableTicker.value)(false)), + printLoggerState ) try { if (!config.silent.value) { @@ -313,18 +309,33 @@ object MillMain { config: MillCliConfig, mainInteractive: Boolean, enableTicker: Option[Boolean], - ): MultilineStatusLogger = { + printLoggerState: PrintLogger.State + ): mill.util.ColorLogger = { val colored = config.color.getOrElse(mainInteractive) val colors = if (colored) mill.util.Colors.Default else mill.util.Colors.BlackWhite - val logger = new MultilineStatusLogger( - colored = colored, - enableTicker = enableTicker.getOrElse(mainInteractive), - infoColor = colors.info, - errorColor = colors.error, - systemStreams0 = streams, - debugEnabled = config.debugLog.value - ) + val logger = if (config.disablePrompt.value) { + new mill.util.PrintLogger( + colored = colored, + enableTicker = enableTicker.getOrElse(mainInteractive), + infoColor = colors.info, + errorColor = colors.error, + systemStreams = streams, + debugEnabled = config.debugLog.value, + context = "", + printLoggerState + ) + }else { + new MultilinePromptLogger( + colored = colored, + enableTicker = enableTicker.getOrElse(mainInteractive), + infoColor = colors.info, + errorColor = colors.error, + systemStreams0 = streams, + debugEnabled = config.debugLog.value + ) + } + logger } From 64f8973f5e1cde034219e65591f5e8a1b8357154 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 11:49:31 +0800 Subject: [PATCH 014/116] . --- main/api/src/mill/api/Logger.scala | 2 +- main/util/src/mill/util/AnsiNav.scala | 13 +++-- .../src/mill/util/MultilinePromptLogger.scala | 52 ++++++++++--------- .../util/MultilineStatusLoggerTests.scala | 21 +++----- runner/src/mill/runner/MillCliConfig.scala | 2 +- runner/src/mill/runner/MillMain.scala | 16 +++--- 6 files changed, 51 insertions(+), 55 deletions(-) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 2553682fdb4..ad62f9461b4 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,7 +44,7 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit - def withTicker[T](s: Option[String])(t: => T): T = s match{ + def withTicker[T](s: Option[String])(t: => T): T = s match { case None => t case Some(s) => ticker(s) diff --git a/main/util/src/mill/util/AnsiNav.scala b/main/util/src/mill/util/AnsiNav.scala index 3f142341b51..2c9a40ee881 100644 --- a/main/util/src/mill/util/AnsiNav.scala +++ b/main/util/src/mill/util/AnsiNav.scala @@ -9,7 +9,7 @@ case class AnsiNav(output: PrintStream) { /** * Move up `n` squares */ - def up(n: Int): Any = if (n != 0) control(n, 'A') + def up(n: Int): Any = if (n != 0) control(n, 'A') /** * Move down `n` squares @@ -19,13 +19,12 @@ case class AnsiNav(output: PrintStream) { /** * Move right `n` squares */ - def right(n: Int): Any = if (n != 0) control(n, 'C') - + def right(n: Int): Any = if (n != 0) control(n, 'C') /** * Move left `n` squares */ - def left(n: Int): Any = if (n != 0) control(n, 'D') + def left(n: Int): Any = if (n != 0) control(n, 'D') /** * Clear the screen @@ -46,12 +45,12 @@ case class AnsiNav(output: PrintStream) { def clearLine(n: Int): Unit = control(n, 'K') } -object AnsiNav{ +object AnsiNav { def control(n: Int, c: Char): String = "\u001b[" + n + c def up(n: Int): String = if (n != 0) control(n, 'A') else "" def down(n: Int): String = if (n != 0) control(n, 'B') else "" - def right(n: Int): String = if (n != 0) control(n, 'C') else "" - def left(n: Int): String = if (n != 0) control(n, 'D') else "" + def right(n: Int): String = if (n != 0) control(n, 'C') else "" + def left(n: Int): String = if (n != 0) control(n, 'D') else "" def clearScreen(n: Int): String = control(n, 'J') def clearLine(n: Int): String = control(n, 'K') } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index f88095f6308..60a739f079f 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -3,6 +3,7 @@ package mill.util import mill.api.SystemStreams import java.io._ +import scala.collection.mutable class MultilinePromptLogger( override val colored: Boolean, @@ -10,8 +11,8 @@ class MultilinePromptLogger( override val infoColor: fansi.Attrs, override val errorColor: fansi.Attrs, systemStreams0: SystemStreams, - override val debugEnabled: Boolean, -) extends ColorLogger with AutoCloseable{ + override val debugEnabled: Boolean +) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ private val state = new State(systemStreams0, System.currentTimeMillis()) @@ -21,22 +22,22 @@ class MultilinePromptLogger( systemStreams0.in ) - override def close() = stopped = true + override def close(): Unit = stopped = true @volatile var stopped = false @volatile var paused = false - override def withPaused[T](t: => T) = { + override def withPaused[T](t: => T): T = { paused = true try t finally paused = false } val secondsTickerThread = new Thread(() => { - while(!stopped) { + while (!stopped) { Thread.sleep(1000) if (!paused) { - synchronized{ + synchronized { state.refreshPrompt() } } @@ -55,7 +56,6 @@ class MultilinePromptLogger( } } - def debug(s: String): Unit = synchronized { if (debugEnabled) systemStreams.err.println(s) } @@ -64,7 +64,7 @@ class MultilinePromptLogger( class StateStream(wrapped: PrintStream) extends OutputStream { override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { - lastIndexOfNewline(b, off, len) match{ + lastIndexOfNewline(b, off, len) match { case -1 => wrapped.write(b, off, len) case lastNewlineIndex => val indexOfCharAfterNewline = lastNewlineIndex + 1 @@ -72,7 +72,7 @@ class MultilinePromptLogger( // we know that after a newline the cursor is at column zero, and column zero // is the only place we can reliably position the cursor since the saveCursor and // restoreCursor ANSI codes do not work well in the presence of scrolling - state.writeWithPrompt(wrapped){ + state.writeWithPrompt(wrapped) { wrapped.write(b, off, indexOfCharAfterNewline - off) } wrapped.write(b, indexOfCharAfterNewline, off + len - indexOfCharAfterNewline) @@ -94,48 +94,52 @@ object MultilinePromptLogger { case class Status(startTimeMillis: Long, text: String) private class State(systemStreams0: SystemStreams, startTimeMillis: Long) { - val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] + val current: mutable.SortedMap[Int, Seq[Status]] = + collection.mutable.SortedMap.empty[Int, Seq[Status]] // Pre-compute the prelude and current prompt as byte arrays so that // writing them out is fast, since they get written out very frequently - val writePreludeBytes = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes - var currentPromptBytes = Array[Byte]() + val writePreludeBytes: Array[Byte] = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes + var currentPromptBytes: Array[Byte] = Array[Byte]() private def updatePromptBytes() = { val now = System.currentTimeMillis() - val currentPrompt = List("="*80 + renderSeconds(now - startTimeMillis)) ++ + val currentPrompt = List("=" * 80 + renderSeconds(now - startTimeMillis)) ++ current - .collect{case (threadId, statuses) if statuses.nonEmpty => - val statusesString = statuses - .map{status => status.text + renderSeconds(now - status.startTimeMillis)} - .mkString(" / ") - statusesString + .collect { + case (threadId, statuses) if statuses.nonEmpty => + val statusesString = statuses + .map { status => status.text + renderSeconds(now - status.startTimeMillis) } + .mkString(" / ") + statusesString } .toList val currentHeight = currentPrompt.length - currentPromptBytes = (currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes + currentPromptBytes = + (currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes } - def updateCurrent(s: String) = synchronized{ + def updateCurrent(s: String): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt if (s.endsWith("")) current(threadId) = current(threadId).init - else current(threadId) = current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + else current(threadId) = + current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) updatePromptBytes() } - def writeWithPrompt[T](wrapped: PrintStream)(t: => T): T = synchronized{ + def writeWithPrompt[T](wrapped: PrintStream)(t: => T): T = synchronized { wrapped.write(writePreludeBytes) val res = t wrapped.write(currentPromptBytes) res } - def refreshPrompt() = synchronized{ + def refreshPrompt(): Unit = synchronized { updatePromptBytes() systemStreams0.err.write(currentPromptBytes) } - private def renderSeconds(millis: Long) = (millis / 1000).toInt match{ + private def renderSeconds(millis: Long) = (millis / 1000).toInt match { case 0 => "" case n => s" ${n}s" } diff --git a/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala b/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala index bcc97c89809..4e8d5f1e1cb 100644 --- a/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala @@ -11,53 +11,44 @@ object MultilineStatusLoggerTests extends TestSuite { val allSampleByteArrays = Seq[Array[Byte]]( Array(1), Array('\n'), - Array(1, 1), Array(1, '\n'), Array('\n', 1), Array('\n', '\n'), - Array(1, 1, 1), - Array(1, 1, '\n'), Array(1, '\n', 1), Array('\n', 1, 1), - Array(1, '\n', '\n'), Array('\n', 1, '\n'), Array('\n', '\n', 1), - Array('\n', '\n', '\n'), - Array(1, 1, 1, 1), - Array(1, 1, 1, '\n'), Array(1, 1, '\n', 1), Array(1, '\n', 1, 1), Array('\n', 1, 1, 1), - Array(1, 1, '\n', '\n'), Array(1, '\n', '\n', 1), Array('\n', '\n', 1, 1), Array(1, '\n', 1, '\n'), Array('\n', 1, '\n', 1), Array('\n', 1, 1, '\n'), - Array('\n', '\n', '\n', 1), Array('\n', '\n', 1, '\n'), Array('\n', 1, '\n', '\n'), Array(1, '\n', '\n', '\n'), - - Array('\n', '\n', '\n', '\n'), + Array('\n', '\n', '\n', '\n') ) - for(sample <- allSampleByteArrays){ - for(start <- Range(0, sample.length)){ - for(len <- Range(0, sample.length - start)){ + for (sample <- allSampleByteArrays) { + for (start <- Range(0, sample.length)) { + for (len <- Range(0, sample.length - start)) { val found = MultilinePromptLogger.lastIndexOfNewline(sample, start, len) val expected0 = sample.slice(start, start + len).lastIndexOf('\n') val expected = expected0 + start - def assertMsg = s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" + def assertMsg = + s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" if (expected0 == -1) Predef.assert(found == -1, assertMsg) else Predef.assert(found == expected, assertMsg) } diff --git a/runner/src/mill/runner/MillCliConfig.scala b/runner/src/mill/runner/MillCliConfig.scala index a57875e1c17..61d1ef9fac4 100644 --- a/runner/src/mill/runner/MillCliConfig.scala +++ b/runner/src/mill/runner/MillCliConfig.scala @@ -130,7 +130,7 @@ case class MillCliConfig( status at the command line and falls back to the legacy ticker """ ) - disablePrompt: Flag = Flag(), + disablePrompt: Flag = Flag() ) import mainargs.ParserForClass diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 6a2a2a1b02a..1209a390417 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -141,11 +141,11 @@ object MillMain { (false, RunnerState.empty) case Right(config) - if Seq( - config.interactive.value, - config.noServer.value, - config.bsp.value - ).count(identity) > 1 => + if Seq( + config.interactive.value, + config.noServer.value, + config.bsp.value + ).count(identity) > 1 => streams.err.println( "Only one of -i/--interactive, --no-server or --bsp may be given" ) @@ -251,7 +251,9 @@ object MillMain { bspContext.foreach { ctx => repeatForBsp = - BspContext.bspServerHandle.lastResult == Some(BspServerResult.ReloadWorkspace) + BspContext.bspServerHandle.lastResult == Some( + BspServerResult.ReloadWorkspace + ) logger.error( s"`$bspCmd` returned with ${BspContext.bspServerHandle.lastResult}" ) @@ -325,7 +327,7 @@ object MillMain { context = "", printLoggerState ) - }else { + } else { new MultilinePromptLogger( colored = colored, enableTicker = enableTicker.getOrElse(mainInteractive), From 3c98ee210cb52e3bffe1318b1dba177ff5dd97c7 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 12:03:46 +0800 Subject: [PATCH 015/116] . --- main/api/src/mill/api/Logger.scala | 3 +-- main/util/src/mill/util/AnsiNav.scala | 20 +++++++++---------- .../src/mill/util/MultilinePromptLogger.scala | 8 +++++++- main/util/src/mill/util/PrintLogger.scala | 5 +++-- ...scala => MultilinePromptLoggerTests.scala} | 2 +- 5 files changed, 22 insertions(+), 16 deletions(-) rename main/util/test/src/mill/util/{MultilineStatusLoggerTests.scala => MultilinePromptLoggerTests.scala} (97%) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index ad62f9461b4..323e7d3c4aa 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -48,8 +48,7 @@ trait Logger { case None => t case Some(s) => ticker(s) - try t - finally ticker("") + t } def debug(s: String): Unit diff --git a/main/util/src/mill/util/AnsiNav.scala b/main/util/src/mill/util/AnsiNav.scala index 2c9a40ee881..378ed57056f 100644 --- a/main/util/src/mill/util/AnsiNav.scala +++ b/main/util/src/mill/util/AnsiNav.scala @@ -1,30 +1,30 @@ package mill.util -import java.io.PrintStream +import java.io.Writer // Reference https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 -case class AnsiNav(output: PrintStream) { - def control(n: Int, c: Char): Unit = output.print(AnsiNav.control(n, c)) +class AnsiNav(output: Writer) { + def control(n: Int, c: Char): Unit = output.write(AnsiNav.control(n, c)) /** * Move up `n` squares */ - def up(n: Int): Any = if (n != 0) control(n, 'A') + def up(n: Int): Any = if (n != 0) output.write(AnsiNav.up(n)) /** * Move down `n` squares */ - def down(n: Int): Any = if (n != 0) control(n, 'B') + def down(n: Int): Any = if (n != 0) output.write(AnsiNav.down(n)) /** * Move right `n` squares */ - def right(n: Int): Any = if (n != 0) control(n, 'C') + def right(n: Int): Any = if (n != 0) output.write(AnsiNav.right(n)) /** * Move left `n` squares */ - def left(n: Int): Any = if (n != 0) control(n, 'D') + def left(n: Int): Any = if (n != 0) output.write(AnsiNav.left(n)) /** * Clear the screen @@ -33,7 +33,7 @@ case class AnsiNav(output: PrintStream) { * n=1: clear from cursor to start of screen * n=2: clear entire screen */ - def clearScreen(n: Int): Unit = control(n, 'J') + def clearScreen(n: Int): Unit = output.write(AnsiNav.clearScreen(n)) /** * Clear the current line @@ -42,9 +42,9 @@ case class AnsiNav(output: PrintStream) { * n=1: clear from cursor to start of line * n=2: clear entire line */ - def clearLine(n: Int): Unit = control(n, 'K') - + def clearLine(n: Int): Unit = output.write(AnsiNav.clearLine(n)) } + object AnsiNav { def control(n: Int, c: Char): String = "\u001b[" + n + c def up(n: Int): String = if (n != 0) control(n, 'A') else "" diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 60a739f079f..65c16e94943 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -15,7 +15,13 @@ class MultilinePromptLogger( ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ private val state = new State(systemStreams0, System.currentTimeMillis()) - + override def withTicker[T](s: Option[String])(t: => T): T = s match { + case None => t + case Some(s) => + ticker(s) + try t + finally ticker("") + } val systemStreams = new SystemStreams( new PrintStream(new StateStream(systemStreams0.out)), new PrintStream(new StateStream(systemStreams0.err)), diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index 060927fea5a..f59f65d66cd 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -33,12 +33,13 @@ class PrintLogger( systemStreams.err.println() systemStreams.err.println(infoColor(s)) case PrintLogger.State.Ticker => + val p = new PrintWriter(systemStreams.err) // Need to make this more "atomic" - val nav = new AnsiNav(systemStreams.err) + val nav = new AnsiNav(p) nav.up(1) nav.clearLine(2) nav.left(9999) - systemStreams.err.flush() + p.flush() systemStreams.err.println(infoColor(s)) } diff --git a/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala similarity index 97% rename from main/util/test/src/mill/util/MultilineStatusLoggerTests.scala rename to main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 4e8d5f1e1cb..be0c4bfdda8 100644 --- a/main/util/test/src/mill/util/MultilineStatusLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -2,7 +2,7 @@ package mill.util import utest._ -object MultilineStatusLoggerTests extends TestSuite { +object MultilinePromptLoggerTests extends TestSuite { val tests = Tests { test { From 486be7d980ae189fb32ef133a2a0345c5ea08ecd Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 12:33:07 +0800 Subject: [PATCH 016/116] . --- bsp/src/mill/bsp/BspContext.scala | 1 + main/api/src/mill/api/Logger.scala | 7 +--- main/eval/src/mill/eval/GroupEvaluator.scala | 9 ++++- main/util/src/mill/util/DummyLogger.scala | 1 + main/util/src/mill/util/FileLogger.scala | 1 + main/util/src/mill/util/MultiLogger.scala | 5 +++ .../src/mill/util/MultilinePromptLogger.scala | 39 ++++++++++--------- main/util/src/mill/util/PrefixLogger.scala | 1 + main/util/src/mill/util/PrintLogger.scala | 1 + main/util/src/mill/util/ProxyLogger.scala | 1 + testkit/src/mill/testkit/ExampleTester.scala | 2 +- .../src/mill/testkit/IntegrationTester.scala | 2 +- 12 files changed, 42 insertions(+), 28 deletions(-) diff --git a/bsp/src/mill/bsp/BspContext.scala b/bsp/src/mill/bsp/BspContext.scala index b93ce2d187c..944e2ae711c 100644 --- a/bsp/src/mill/bsp/BspContext.scala +++ b/bsp/src/mill/bsp/BspContext.scala @@ -60,6 +60,7 @@ private[mill] class BspContext( override def debugEnabled: Boolean = true override def rawOutputStream: PrintStream = systemStreams.out + override def endTicker() = () } BspWorker(mill.api.WorkspaceRoot.workspaceRoot, home, log).flatMap { worker => diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 323e7d3c4aa..7758d2e8c09 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,12 +44,7 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit - def withTicker[T](s: Option[String])(t: => T): T = s match { - case None => t - case Some(s) => - ticker(s) - t - } + def endTicker(): Unit def debug(s: String): Unit diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 67731ca79b5..eaa152189ed 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -245,7 +245,14 @@ private[mill] trait GroupEvaluator { val tickerPrefix = maybeTargetLabel.collect { case targetLabel if logRun && enableTicker => s"$counterMsg $targetLabel " } - logger.withTicker(tickerPrefix) { + def withTicker[T](s: Option[String])(t: => T): T = s match { + case None => t + case Some(s) => + logger.ticker(s) + try t + finally logger.endTicker() + } + withTicker(tickerPrefix) { val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { override def ticker(s: String): Unit = { if (enableTicker) super.ticker(tickerPrefix.getOrElse("") + s) diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index 14685983ea5..4b39cdbfc53 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -19,4 +19,5 @@ object DummyLogger extends Logger { def ticker(s: String) = () def debug(s: String) = () override val debugEnabled: Boolean = false + def endTicker(): Unit = () } diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index b8ec7118205..83bfafaeda8 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -54,4 +54,5 @@ class FileLogger( } override def rawOutputStream: PrintStream = outputStream + def endTicker(): Unit = () } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 850bcedba6d..254c4d0cc5e 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -42,6 +42,11 @@ class MultiLogger( } override def rawOutputStream: PrintStream = systemStreams.out + + override def endTicker(): Unit = { + logger1.endTicker() + logger2.endTicker() + } } class MultiStream(stream1: OutputStream, stream2: OutputStream) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 65c16e94943..02b3acf6504 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -14,14 +14,7 @@ class MultilinePromptLogger( override val debugEnabled: Boolean ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ - private val state = new State(systemStreams0, System.currentTimeMillis()) - override def withTicker[T](s: Option[String])(t: => T): T = s match { - case None => t - case Some(s) => - ticker(s) - try t - finally ticker("") - } + private val state = new State(enableTicker, systemStreams0, System.currentTimeMillis()) val systemStreams = new SystemStreams( new PrintStream(new StateStream(systemStreams0.out)), new PrintStream(new StateStream(systemStreams0.err)), @@ -56,10 +49,14 @@ class MultilinePromptLogger( def error(s: String): Unit = synchronized { systemStreams.err.println(s) } + override def endTicker(): Unit = synchronized { + state.updateCurrent(None) + state.refreshPrompt() + } + def ticker(s: String): Unit = synchronized { - state.writeWithPrompt(systemStreams0.err) { - state.updateCurrent(s) - } + state.updateCurrent(Some(s)) + state.refreshPrompt() } def debug(s: String): Unit = synchronized { @@ -99,7 +96,7 @@ class MultilinePromptLogger( object MultilinePromptLogger { case class Status(startTimeMillis: Long, text: String) - private class State(systemStreams0: SystemStreams, startTimeMillis: Long) { + private class State(enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long) { val current: mutable.SortedMap[Int, Seq[Status]] = collection.mutable.SortedMap.empty[Int, Seq[Status]] @@ -125,24 +122,28 @@ object MultilinePromptLogger { (currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes } - def updateCurrent(s: String): Unit = synchronized { + def updateCurrent(sOpt: Option[String]): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt - if (s.endsWith("")) current(threadId) = current(threadId).init - else current(threadId) = - current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + + sOpt match { + case None => current.remove(threadId) + case Some(s) => + current(threadId) = + current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + } updatePromptBytes() } def writeWithPrompt[T](wrapped: PrintStream)(t: => T): T = synchronized { - wrapped.write(writePreludeBytes) + if (enableTicker) wrapped.write(writePreludeBytes) val res = t - wrapped.write(currentPromptBytes) + if (enableTicker) wrapped.write(currentPromptBytes) res } def refreshPrompt(): Unit = synchronized { updatePromptBytes() - systemStreams0.err.write(currentPromptBytes) + if (enableTicker) systemStreams0.err.write(currentPromptBytes) } private def renderSeconds(millis: Long) = (millis / 1000).toInt match { diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index a9aa641515e..4dc43fc75c7 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -51,6 +51,7 @@ class PrefixLogger( outStream0 = Some(outStream), errStream0 = Some(systemStreams.err) ) + def endTicker(): Unit = logger0.endTicker() } object PrefixLogger { diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index f59f65d66cd..f8f3ecf7ae4 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -78,6 +78,7 @@ class PrintLogger( } override def rawOutputStream: PrintStream = systemStreams.out + def endTicker(): Unit = () } object PrintLogger { diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index b7448ae6f04..01547ed57f8 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -23,4 +23,5 @@ class ProxyLogger(logger: Logger) extends Logger { override def close(): Unit = logger.close() override def rawOutputStream: PrintStream = logger.rawOutputStream + def endTicker(): Unit = logger.endTicker() } diff --git a/testkit/src/mill/testkit/ExampleTester.scala b/testkit/src/mill/testkit/ExampleTester.scala index 656d0bce69f..e4feeac583b 100644 --- a/testkit/src/mill/testkit/ExampleTester.scala +++ b/testkit/src/mill/testkit/ExampleTester.scala @@ -101,7 +101,7 @@ class ExampleTester( check: Boolean = true ): Unit = { val commandStr = commandStr0 match { - case s"mill $rest" => s"./mill $rest" + case s"mill $rest" => s"./mill --disable-ticker $rest" case s"curl $rest" => s"curl --retry 5 --retry-all-errors $rest" case s => s } diff --git a/testkit/src/mill/testkit/IntegrationTester.scala b/testkit/src/mill/testkit/IntegrationTester.scala index 0a4c4651a61..f0ae7bdbfc8 100644 --- a/testkit/src/mill/testkit/IntegrationTester.scala +++ b/testkit/src/mill/testkit/IntegrationTester.scala @@ -69,7 +69,7 @@ object IntegrationTester { val debugArgs = Option.when(debugLog)("--debug") - val shellable: os.Shellable = (millExecutable, serverArgs, debugArgs, cmd) + val shellable: os.Shellable = (millExecutable, "--disable-ticker", serverArgs, debugArgs, cmd) val res0 = os.call( cmd = shellable, env = env, From 038aae90519c5a043b258a9c25573b06613c627d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 12:44:18 +0800 Subject: [PATCH 017/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 02b3acf6504..691de69b710 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -113,7 +113,9 @@ object MultilinePromptLogger { val statusesString = statuses .map { status => status.text + renderSeconds(now - status.startTimeMillis) } .mkString(" / ") - statusesString + // Limit to 99 chars wide + if (statusesString.length <= 99) statusesString + else statusesString.take(43) + "..." + statusesString.takeRight(43) } .toList From 239f76a7100bd1fe12f6ebb47c3e7c4df3205d71 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 12:47:07 +0800 Subject: [PATCH 018/116] . --- build.mill | 1 - main/package.mill | 2 +- runner/client/src/mill/runner/client/MillClientMain.java | 2 -- runner/src/mill/runner/MillMain.scala | 2 -- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/build.mill b/build.mill index ff8b4367849..a4039a6288d 100644 --- a/build.mill +++ b/build.mill @@ -141,7 +141,6 @@ object Deps { ) val jline = ivy"org.jline:jline:3.26.3" - val jansi = ivy"org.fusesource.jansi:jansi:2.4.1" val jnaVersion = "5.14.0" val jna = ivy"net.java.dev.jna:jna:${jnaVersion}" val jnaPlatform = ivy"net.java.dev.jna:jna-platform:${jnaVersion}" diff --git a/main/package.mill b/main/package.mill index 7a670373f02..a2696ff9d48 100644 --- a/main/package.mill +++ b/main/package.mill @@ -171,7 +171,7 @@ object `package` extends RootModule with build.MillStableScalaModule with BuildI object client extends build.MillPublishJavaModule with BuildInfo { def buildInfoPackageName = "mill.main.client" def buildInfoMembers = Seq(BuildInfo.Value("millVersion", build.millVersion(), "Mill version.")) - def ivyDeps = Agg(build.Deps.junixsocket, build.Deps.jansi) + def ivyDeps = Agg(build.Deps.junixsocket) object test extends JavaModuleTests with TestModule.Junit4 { def ivyDeps = Agg( diff --git a/runner/client/src/mill/runner/client/MillClientMain.java b/runner/client/src/mill/runner/client/MillClientMain.java index 240e4faadbf..8044da5a914 100644 --- a/runner/client/src/mill/runner/client/MillClientMain.java +++ b/runner/client/src/mill/runner/client/MillClientMain.java @@ -2,7 +2,6 @@ import java.util.Arrays; import mill.main.client.ServerLauncher; -import org.fusesource.jansi.AnsiConsole; import mill.main.client.Util; import mill.main.client.lock.Locks; import mill.main.client.OutFiles; @@ -14,7 +13,6 @@ */ public class MillClientMain { public static void main(String[] args) throws Exception { - AnsiConsole.systemInstall(); boolean runNoServer = false; if (args.length > 0) { String firstArg = args[0]; diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 1209a390417..fc92fd2e4a2 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -9,7 +9,6 @@ import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal} import mill.bsp.{BspContext, BspServerResult} import mill.main.BuildInfo import mill.util.{MultilinePromptLogger, PrintLogger} -import org.fusesource.jansi.AnsiConsole import java.lang.reflect.InvocationTargetException import scala.util.control.NonFatal @@ -35,7 +34,6 @@ object MillMain { } def main(args: Array[String]): Unit = { - AnsiConsole.systemInstall() val initialSystemStreams = new SystemStreams(System.out, System.err, System.in) // setup streams val (runnerStreams, cleanupStreams, bspLog) = From bd682518688fca9c0caf56b6966885fdc6150834 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 12:53:27 +0800 Subject: [PATCH 019/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 691de69b710..3c8a6ad5c16 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -113,15 +113,15 @@ object MultilinePromptLogger { val statusesString = statuses .map { status => status.text + renderSeconds(now - status.startTimeMillis) } .mkString(" / ") - // Limit to 99 chars wide - if (statusesString.length <= 99) statusesString - else statusesString.take(43) + "..." + statusesString.takeRight(43) + // Limit to <120 chars wide + if (statusesString.length <= 119) statusesString + else statusesString.take(58) + "..." + statusesString.takeRight(58) } .toList val currentHeight = currentPrompt.length currentPromptBytes = - (currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes + (AnsiNav.clearScreen(0) + currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes } def updateCurrent(sOpt: Option[String]): Unit = synchronized { From bd895a0850aa43fc4c42d6fbd46b2cfd89204445 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 12:59:17 +0800 Subject: [PATCH 020/116] 119 width, preserve ending header --- .../src/mill/util/MultilinePromptLogger.scala | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 3c8a6ad5c16..18738f3b582 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -21,7 +21,10 @@ class MultilinePromptLogger( systemStreams0.in ) - override def close(): Unit = stopped = true + override def close(): Unit = { + state.refreshPrompt() + stopped = true + } @volatile var stopped = false @volatile var paused = false @@ -97,25 +100,32 @@ object MultilinePromptLogger { case class Status(startTimeMillis: Long, text: String) private class State(enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long) { - val current: mutable.SortedMap[Int, Seq[Status]] = - collection.mutable.SortedMap.empty[Int, Seq[Status]] + val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] // Pre-compute the prelude and current prompt as byte arrays so that // writing them out is fast, since they get written out very frequently val writePreludeBytes: Array[Byte] = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes var currentPromptBytes: Array[Byte] = Array[Byte]() + private def updatePromptBytes() = { + // Limit to <120 chars wide + val maxWidth = 119 val now = System.currentTimeMillis() - val currentPrompt = List("=" * 80 + renderSeconds(now - startTimeMillis)) ++ + val totalSecondsStr = renderSeconds(now - startTimeMillis) + val currentPrompt = List("=" * (maxWidth - totalSecondsStr.length)) ++ current .collect { case (threadId, statuses) if statuses.nonEmpty => val statusesString = statuses .map { status => status.text + renderSeconds(now - status.startTimeMillis) } .mkString(" / ") - // Limit to <120 chars wide - if (statusesString.length <= 119) statusesString - else statusesString.take(58) + "..." + statusesString.takeRight(58) + + if (statusesString.length <= maxWidth) statusesString + else { + val ellipses = "..." + val halfWidth = (maxWidth - ellipses.length) / 2 + statusesString.take(halfWidth) + ellipses + statusesString.takeRight(halfWidth) + } } .toList From 36b97f8e999d40ab983bc41e758ce48242ba61c1 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 13:02:57 +0800 Subject: [PATCH 021/116] . --- build.mill | 2 +- main/util/src/mill/util/MultilinePromptLogger.scala | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/build.mill b/build.mill index a4039a6288d..2be5f92276c 100644 --- a/build.mill +++ b/build.mill @@ -20,7 +20,7 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload - +import $packages._ object Settings { val pomOrg = "com.lihaoyi" val githubOrg = "com-lihaoyi" diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 18738f3b582..18b12112873 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -21,10 +21,7 @@ class MultilinePromptLogger( systemStreams0.in ) - override def close(): Unit = { - state.refreshPrompt() - stopped = true - } + override def close(): Unit = stopped = true @volatile var stopped = false @volatile var paused = false @@ -112,7 +109,7 @@ object MultilinePromptLogger { val maxWidth = 119 val now = System.currentTimeMillis() val totalSecondsStr = renderSeconds(now - startTimeMillis) - val currentPrompt = List("=" * (maxWidth - totalSecondsStr.length)) ++ + val currentPrompt = List("=" * (maxWidth - totalSecondsStr.length - 1) + totalSecondsStr) ++ current .collect { case (threadId, statuses) if statuses.nonEmpty => From 990f7d1bc81a3361e8cca23afd13807d39756faa Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 13:03:01 +0800 Subject: [PATCH 022/116] . --- build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.mill b/build.mill index 2be5f92276c..a4039a6288d 100644 --- a/build.mill +++ b/build.mill @@ -20,7 +20,7 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload -import $packages._ + object Settings { val pomOrg = "com.lihaoyi" val githubOrg = "com-lihaoyi" From c447df022c4480c65cc1a305076d0a85deaa2102 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 13:12:21 +0800 Subject: [PATCH 023/116] wip --- bsp/src/mill/bsp/BspContext.scala | 1 + main/api/src/mill/api/Logger.scala | 1 + main/eval/src/mill/eval/EvaluatorCore.scala | 4 ++- main/eval/src/mill/eval/GroupEvaluator.scala | 3 +- main/util/src/mill/util/DummyLogger.scala | 1 + main/util/src/mill/util/FileLogger.scala | 1 + main/util/src/mill/util/MultiLogger.scala | 4 +++ .../src/mill/util/MultilinePromptLogger.scala | 28 +++++++++++++------ main/util/src/mill/util/PrefixLogger.scala | 1 + main/util/src/mill/util/PrintLogger.scala | 1 + main/util/src/mill/util/ProxyLogger.scala | 1 + 11 files changed, 36 insertions(+), 10 deletions(-) diff --git a/bsp/src/mill/bsp/BspContext.scala b/bsp/src/mill/bsp/BspContext.scala index 944e2ae711c..63545fe7f0f 100644 --- a/bsp/src/mill/bsp/BspContext.scala +++ b/bsp/src/mill/bsp/BspContext.scala @@ -61,6 +61,7 @@ private[mill] class BspContext( override def rawOutputStream: PrintStream = systemStreams.out override def endTicker() = () + def globalTicker(s: String): Unit = () } BspWorker(mill.api.WorkspaceRoot.workspaceRoot, home, log).flatMap { worker => diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 7758d2e8c09..a95d004b472 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,6 +44,7 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit + def globalTicker(s: String): Unit def endTicker(): Unit def debug(s: String): Unit diff --git a/main/eval/src/mill/eval/EvaluatorCore.scala b/main/eval/src/mill/eval/EvaluatorCore.scala index 777640d7eba..24e592d6929 100644 --- a/main/eval/src/mill/eval/EvaluatorCore.scala +++ b/main/eval/src/mill/eval/EvaluatorCore.scala @@ -101,6 +101,8 @@ private[mill] trait EvaluatorCore extends GroupEvaluator { for (terminal <- terminals) { val deps = interGroupDeps(terminal) futures(terminal) = Future.sequence(deps.map(futures)).map { upstreamValues => + val counterMsg = s"${count.getAndIncrement()}/${terminals.size}" + logger.globalTicker(counterMsg) if (failed.get()) None else { val upstreamResults = upstreamValues @@ -110,7 +112,7 @@ private[mill] trait EvaluatorCore extends GroupEvaluator { val startTime = System.nanoTime() / 1000 val threadId = threadNumberer.getThreadId(Thread.currentThread()) - val counterMsg = s"${count.getAndIncrement()}/${terminals.size}" + val contextLogger = PrefixLogger( out = logger, context = contextLoggerMsg(threadId), diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index eaa152189ed..79591605f27 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -243,7 +243,8 @@ private[mill] trait GroupEvaluator { } val tickerPrefix = maybeTargetLabel.collect { - case targetLabel if logRun && enableTicker => s"$counterMsg $targetLabel " +// case targetLabel if logRun && enableTicker => s"$counterMsg $targetLabel " + case targetLabel if logRun && enableTicker => targetLabel } def withTicker[T](s: Option[String])(t: => T): T = s match { case None => t diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index 4b39cdbfc53..ef379df3908 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -20,4 +20,5 @@ object DummyLogger extends Logger { def debug(s: String) = () override val debugEnabled: Boolean = false def endTicker(): Unit = () + def globalTicker(s: String): Unit = () } diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index 83bfafaeda8..748deaa24fc 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -55,4 +55,5 @@ class FileLogger( override def rawOutputStream: PrintStream = outputStream def endTicker(): Unit = () + def globalTicker(s: String): Unit = () } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 254c4d0cc5e..96d88716276 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -47,6 +47,10 @@ class MultiLogger( logger1.endTicker() logger2.endTicker() } + def globalTicker(s: String): Unit = { + logger1.globalTicker(s) + logger2.globalTicker(s) + } } class MultiStream(stream1: OutputStream, stream2: OutputStream) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 18b12112873..1e64e0ec6ca 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -21,7 +21,10 @@ class MultilinePromptLogger( systemStreams0.in ) - override def close(): Unit = stopped = true + override def close(): Unit = { + state.refreshPrompt() + stopped = true + } @volatile var stopped = false @volatile var paused = false @@ -48,7 +51,10 @@ class MultilinePromptLogger( def info(s: String): Unit = synchronized { systemStreams.err.println(s) } def error(s: String): Unit = synchronized { systemStreams.err.println(s) } - + def globalTicker(s: String): Unit = { + state.updateGlobal(s) + state.refreshPrompt() + } override def endTicker(): Unit = synchronized { state.updateCurrent(None) state.refreshPrompt() @@ -97,24 +103,26 @@ object MultilinePromptLogger { case class Status(startTimeMillis: Long, text: String) private class State(enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long) { - val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] + private val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] + private var header = "" // Pre-compute the prelude and current prompt as byte arrays so that // writing them out is fast, since they get written out very frequently - val writePreludeBytes: Array[Byte] = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes - var currentPromptBytes: Array[Byte] = Array[Byte]() + private val writePreludeBytes: Array[Byte] = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes + private var currentPromptBytes: Array[Byte] = Array[Byte]() private def updatePromptBytes() = { // Limit to <120 chars wide val maxWidth = 119 val now = System.currentTimeMillis() val totalSecondsStr = renderSeconds(now - startTimeMillis) - val currentPrompt = List("=" * (maxWidth - totalSecondsStr.length - 1) + totalSecondsStr) ++ + val divider = "=" * (maxWidth - header.length - totalSecondsStr.length - 4) + val currentPrompt = List(s" $header $divider $totalSecondsStr") ++ current .collect { case (threadId, statuses) if statuses.nonEmpty => val statusesString = statuses - .map { status => status.text + renderSeconds(now - status.startTimeMillis) } + .map { status => status.text + " " + renderSeconds(now - status.startTimeMillis) } .mkString(" / ") if (statusesString.length <= maxWidth) statusesString @@ -131,6 +139,10 @@ object MultilinePromptLogger { (AnsiNav.clearScreen(0) + currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes } + def updateGlobal(s: String): Unit = synchronized{ + header = s + updatePromptBytes() + } def updateCurrent(sOpt: Option[String]): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt @@ -157,7 +169,7 @@ object MultilinePromptLogger { private def renderSeconds(millis: Long) = (millis / 1000).toInt match { case 0 => "" - case n => s" ${n}s" + case n => s"${n}s" } } diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 4dc43fc75c7..9b64e0aad71 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -52,6 +52,7 @@ class PrefixLogger( errStream0 = Some(systemStreams.err) ) def endTicker(): Unit = logger0.endTicker() + def globalTicker(s: String): Unit = logger0.globalTicker(s) } object PrefixLogger { diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index f8f3ecf7ae4..001ff9eec77 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -79,6 +79,7 @@ class PrintLogger( override def rawOutputStream: PrintStream = systemStreams.out def endTicker(): Unit = () + def globalTicker(s: String): Unit = () } object PrintLogger { diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 01547ed57f8..28bfd8c333f 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -24,4 +24,5 @@ class ProxyLogger(logger: Logger) extends Logger { override def rawOutputStream: PrintStream = logger.rawOutputStream def endTicker(): Unit = logger.endTicker() + def globalTicker(s: String): Unit = logger.globalTicker(s) } From 515e70400f6fd56fd77cbc43bccfa9150d12d497 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 13:45:09 +0800 Subject: [PATCH 024/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 1e64e0ec6ca..aff85880b6e 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -35,9 +35,9 @@ class MultilinePromptLogger( finally paused = false } - val secondsTickerThread = new Thread(() => { + val promptUpdaterThread = new Thread(() => { while (!stopped) { - Thread.sleep(1000) + Thread.sleep(100) if (!paused) { synchronized { state.refreshPrompt() @@ -46,23 +46,20 @@ class MultilinePromptLogger( } }) - secondsTickerThread.start() + promptUpdaterThread.start() def info(s: String): Unit = synchronized { systemStreams.err.println(s) } def error(s: String): Unit = synchronized { systemStreams.err.println(s) } def globalTicker(s: String): Unit = { state.updateGlobal(s) - state.refreshPrompt() } override def endTicker(): Unit = synchronized { state.updateCurrent(None) - state.refreshPrompt() } def ticker(s: String): Unit = synchronized { state.updateCurrent(Some(s)) - state.refreshPrompt() } def debug(s: String): Unit = synchronized { From cb6f9b6df5c5979aabe5a200663a1af223350a80 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 13:58:52 +0800 Subject: [PATCH 025/116] . --- .../src/mill/util/MultilinePromptLogger.scala | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index aff85880b6e..11077ab1006 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -37,7 +37,7 @@ class MultilinePromptLogger( val promptUpdaterThread = new Thread(() => { while (!stopped) { - Thread.sleep(100) + Thread.sleep(promptUpdateIntervalMillis) if (!paused) { synchronized { state.refreshPrompt() @@ -68,7 +68,7 @@ class MultilinePromptLogger( override def rawOutputStream: PrintStream = systemStreams0.out - class StateStream(wrapped: PrintStream) extends OutputStream { + private class StateStream(wrapped: PrintStream) extends OutputStream { override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { lastIndexOfNewline(b, off, len) match { case -1 => wrapped.write(b, off, len) @@ -97,10 +97,24 @@ class MultilinePromptLogger( } object MultilinePromptLogger { - case class Status(startTimeMillis: Long, text: String) + /** + * How often to update the multiline status prompt on the terminal. + * Too frequent is bad because it causes a lot of visual noise, + * but too infrequent results in latency. 10 times per second seems reasonable + */ + private val promptUpdateIntervalMillis = 100 + /** + * Add some extra latency delay to the process of removing an entry from the status + * prompt entirely, because removing an entry changes the height of the prompt, which + * is even more distracting than changing the contents of a line, so we want to minimize + * those occurrences even further. + */ + private val statusRemovalDelayMillis = 500 + + private case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) private class State(enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long) { - private val current = collection.mutable.SortedMap.empty[Int, Seq[Status]] + private val statuses = collection.mutable.SortedMap.empty[Int, Status] private var header = "" // Pre-compute the prelude and current prompt as byte arrays so that @@ -109,18 +123,23 @@ object MultilinePromptLogger { private var currentPromptBytes: Array[Byte] = Array[Byte]() private def updatePromptBytes() = { + val now = System.currentTimeMillis() + for(k <- statuses.keySet){ + val removedTime = statuses(k).removedTimeMillis + if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis){ + statuses.remove(k) + } + } // Limit to <120 chars wide val maxWidth = 119 - val now = System.currentTimeMillis() + val totalSecondsStr = renderSeconds(now - startTimeMillis) val divider = "=" * (maxWidth - header.length - totalSecondsStr.length - 4) val currentPrompt = List(s" $header $divider $totalSecondsStr") ++ - current + statuses .collect { - case (threadId, statuses) if statuses.nonEmpty => - val statusesString = statuses - .map { status => status.text + " " + renderSeconds(now - status.startTimeMillis) } - .mkString(" / ") + case (threadId, status) => + val statusesString = status.text + " " + renderSeconds(now - status.startTimeMillis) if (statusesString.length <= maxWidth) statusesString else { @@ -143,11 +162,10 @@ object MultilinePromptLogger { def updateCurrent(sOpt: Option[String]): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt + val now = System.currentTimeMillis() sOpt match { - case None => current.remove(threadId) - case Some(s) => - current(threadId) = - current.getOrElse(threadId, Nil) :+ Status(System.currentTimeMillis(), s) + case None => statuses.get(threadId).foreach(_.removedTimeMillis = now) + case Some(s) => statuses(threadId) = Status(now, s, -1) } updatePromptBytes() } From f2cd44078cf386c833c2d98c09749b538b03e1c4 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 14:23:36 +0800 Subject: [PATCH 026/116] . --- build.mill | 1 + .../src/mill/util/MultilinePromptLogger.scala | 77 +++++++++++++------ runner/src/mill/runner/MillMain.scala | 3 +- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/build.mill b/build.mill index a4039a6288d..2024a17ab46 100644 --- a/build.mill +++ b/build.mill @@ -20,6 +20,7 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload +//import $packages._ object Settings { val pomOrg = "com.lihaoyi" diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 11077ab1006..52f74834646 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -5,16 +5,17 @@ import mill.api.SystemStreams import java.io._ import scala.collection.mutable -class MultilinePromptLogger( +private[mill] class MultilinePromptLogger( override val colored: Boolean, val enableTicker: Boolean, override val infoColor: fansi.Attrs, override val errorColor: fansi.Attrs, systemStreams0: SystemStreams, - override val debugEnabled: Boolean + override val debugEnabled: Boolean, + titleText: String, ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ - private val state = new State(enableTicker, systemStreams0, System.currentTimeMillis()) + private val state = new State(titleText, enableTicker, systemStreams0, System.currentTimeMillis()) val systemStreams = new SystemStreams( new PrintStream(new StateStream(systemStreams0.out)), new PrintStream(new StateStream(systemStreams0.err)), @@ -96,7 +97,7 @@ class MultilinePromptLogger( } } -object MultilinePromptLogger { +private object MultilinePromptLogger { /** * How often to update the multiline status prompt on the terminal. * Too frequent is bad because it causes a lot of visual noise, @@ -113,10 +114,13 @@ object MultilinePromptLogger { private case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) - private class State(enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long) { + private class State(titleText: String, + enableTicker: Boolean, + systemStreams0: SystemStreams, + startTimeMillis: Long) { private val statuses = collection.mutable.SortedMap.empty[Int, Status] - private var header = "" + private var headerPrefix = "" // Pre-compute the prelude and current prompt as byte arrays so that // writing them out is fast, since they get written out very frequently private val writePreludeBytes: Array[Byte] = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes @@ -132,31 +136,27 @@ object MultilinePromptLogger { } // Limit to <120 chars wide val maxWidth = 119 + val headerSuffix = renderSeconds(now - startTimeMillis) + + val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) + val body = statuses + .collect { + case (threadId, status) => + splitShorten( + status.text + " " + renderSeconds(now - status.startTimeMillis), + maxWidth + ) + } + .toList - val totalSecondsStr = renderSeconds(now - startTimeMillis) - val divider = "=" * (maxWidth - header.length - totalSecondsStr.length - 4) - val currentPrompt = List(s" $header $divider $totalSecondsStr") ++ - statuses - .collect { - case (threadId, status) => - val statusesString = status.text + " " + renderSeconds(now - status.startTimeMillis) - - if (statusesString.length <= maxWidth) statusesString - else { - val ellipses = "..." - val halfWidth = (maxWidth - ellipses.length) / 2 - statusesString.take(halfWidth) + ellipses + statusesString.takeRight(halfWidth) - } - } - .toList - - val currentHeight = currentPrompt.length + val currentPrompt = header :: body + val currentHeight = body.length + 1 currentPromptBytes = (AnsiNav.clearScreen(0) + currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes } def updateGlobal(s: String): Unit = synchronized{ - header = s + headerPrefix = s updatePromptBytes() } def updateCurrent(sOpt: Option[String]): Unit = synchronized { @@ -188,6 +188,33 @@ object MultilinePromptLogger { } } + def renderHeader(headerPrefix0: String, titleText0: String, headerSuffix0: String, maxWidth: Int) = { + val headerPrefixStr = s" $headerPrefix0 " + val headerSuffixStr = s" $headerSuffix0" + val titleText = s" $titleText0 " + val maxTitleLength = maxWidth - headerPrefixStr.length - headerSuffixStr.length + val shortenedTitle = splitShorten(titleText, maxTitleLength) + + val nonDividerLength = headerPrefixStr.length + headerSuffixStr.length + shortenedTitle.length + val divider = "=" * (maxWidth - nonDividerLength) + val (divider1, divider2) = divider.splitAt(divider.length / 2) + val headerString = headerPrefixStr + divider1 + shortenedTitle + divider2 + headerSuffixStr + assert( + headerString.length == maxWidth, + s"${pprint.apply(headerString)} is length ${headerString.length}, requires $maxWidth" + ) + headerString + } + + def splitShorten(s: String, maxLength: Int) = { + if (s.length <= maxLength) s + else { + val ellipses = "..." + val halfWidth = (maxLength - ellipses.length) / 2 + s.take(halfWidth) + ellipses + s.takeRight(halfWidth) + } + } + def lastIndexOfNewline(b: Array[Byte], off: Int, len: Int): Int = { var index = off + len - 1 while (true) { diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index fc92fd2e4a2..9129d789471 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -332,7 +332,8 @@ object MillMain { infoColor = colors.info, errorColor = colors.error, systemStreams0 = streams, - debugEnabled = config.debugLog.value + debugEnabled = config.debugLog.value, + titleText = config.leftoverArgs.value.mkString(" ") ) } From d1c41d7b1b3fef64008f21ceffae2f8ea07ae77a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 14:34:29 +0800 Subject: [PATCH 027/116] . --- build.mill | 1 - .../src/mill/util/MultilinePromptLogger.scala | 37 ++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/build.mill b/build.mill index 2024a17ab46..a4039a6288d 100644 --- a/build.mill +++ b/build.mill @@ -20,7 +20,6 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload -//import $packages._ object Settings { val pomOrg = "com.lihaoyi" diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 52f74834646..217924e63bf 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -3,7 +3,6 @@ package mill.util import mill.api.SystemStreams import java.io._ -import scala.collection.mutable private[mill] class MultilinePromptLogger( override val colored: Boolean, @@ -12,7 +11,7 @@ private[mill] class MultilinePromptLogger( override val errorColor: fansi.Attrs, systemStreams0: SystemStreams, override val debugEnabled: Boolean, - titleText: String, + titleText: String ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ private val state = new State(titleText, enableTicker, systemStreams0, System.currentTimeMillis()) @@ -98,12 +97,14 @@ private[mill] class MultilinePromptLogger( } private object MultilinePromptLogger { + /** * How often to update the multiline status prompt on the terminal. * Too frequent is bad because it causes a lot of visual noise, * but too infrequent results in latency. 10 times per second seems reasonable */ private val promptUpdateIntervalMillis = 100 + /** * Add some extra latency delay to the process of removing an entry from the status * prompt entirely, because removing an entry changes the height of the prompt, which @@ -114,23 +115,26 @@ private object MultilinePromptLogger { private case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) - private class State(titleText: String, - enableTicker: Boolean, - systemStreams0: SystemStreams, - startTimeMillis: Long) { + private class State( + titleText: String, + enableTicker: Boolean, + systemStreams0: SystemStreams, + startTimeMillis: Long + ) { private val statuses = collection.mutable.SortedMap.empty[Int, Status] private var headerPrefix = "" // Pre-compute the prelude and current prompt as byte arrays so that // writing them out is fast, since they get written out very frequently - private val writePreludeBytes: Array[Byte] = (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes + private val writePreludeBytes: Array[Byte] = + (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes private var currentPromptBytes: Array[Byte] = Array[Byte]() private def updatePromptBytes() = { val now = System.currentTimeMillis() - for(k <- statuses.keySet){ + for (k <- statuses.keySet) { val removedTime = statuses(k).removedTimeMillis - if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis){ + if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis) { statuses.remove(k) } } @@ -152,10 +156,12 @@ private object MultilinePromptLogger { val currentPrompt = header :: body val currentHeight = body.length + 1 currentPromptBytes = - (AnsiNav.clearScreen(0) + currentPrompt.mkString("\n") + "\n" + AnsiNav.up(currentHeight)).getBytes + (AnsiNav.clearScreen(0) + currentPrompt.mkString("\n") + "\n" + AnsiNav.up( + currentHeight + )).getBytes } - def updateGlobal(s: String): Unit = synchronized{ + def updateGlobal(s: String): Unit = synchronized { headerPrefix = s updatePromptBytes() } @@ -188,7 +194,12 @@ private object MultilinePromptLogger { } } - def renderHeader(headerPrefix0: String, titleText0: String, headerSuffix0: String, maxWidth: Int) = { + def renderHeader( + headerPrefix0: String, + titleText0: String, + headerSuffix0: String, + maxWidth: Int + ): String = { val headerPrefixStr = s" $headerPrefix0 " val headerSuffixStr = s" $headerSuffix0" val titleText = s" $titleText0 " @@ -206,7 +217,7 @@ private object MultilinePromptLogger { headerString } - def splitShorten(s: String, maxLength: Int) = { + def splitShorten(s: String, maxLength: Int): String = { if (s.length <= maxLength) s else { val ellipses = "..." From 8ab79185ff6e14f5d81562072cd3c87dec3b510f Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 14:48:30 +0800 Subject: [PATCH 028/116] . --- bsp/src/mill/bsp/BspContext.scala | 2 -- main/api/src/mill/api/Logger.scala | 4 ++-- main/util/src/mill/util/DummyLogger.scala | 2 -- main/util/src/mill/util/FileLogger.scala | 2 -- main/util/src/mill/util/MultiLogger.scala | 2 +- main/util/src/mill/util/MultilinePromptLogger.scala | 2 +- main/util/src/mill/util/PrefixLogger.scala | 4 ++-- main/util/src/mill/util/PrintLogger.scala | 2 -- main/util/src/mill/util/ProxyLogger.scala | 4 ++-- testkit/src/mill/testkit/IntegrationTester.scala | 2 +- 10 files changed, 9 insertions(+), 17 deletions(-) diff --git a/bsp/src/mill/bsp/BspContext.scala b/bsp/src/mill/bsp/BspContext.scala index 63545fe7f0f..b93ce2d187c 100644 --- a/bsp/src/mill/bsp/BspContext.scala +++ b/bsp/src/mill/bsp/BspContext.scala @@ -60,8 +60,6 @@ private[mill] class BspContext( override def debugEnabled: Boolean = true override def rawOutputStream: PrintStream = systemStreams.out - override def endTicker() = () - def globalTicker(s: String): Unit = () } BspWorker(mill.api.WorkspaceRoot.workspaceRoot, home, log).flatMap { worker => diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index a95d004b472..eb5cf64764b 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,8 +44,8 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit - def globalTicker(s: String): Unit - def endTicker(): Unit + def globalTicker(s: String): Unit = () + def endTicker(): Unit = () def debug(s: String): Unit diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index ef379df3908..14685983ea5 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -19,6 +19,4 @@ object DummyLogger extends Logger { def ticker(s: String) = () def debug(s: String) = () override val debugEnabled: Boolean = false - def endTicker(): Unit = () - def globalTicker(s: String): Unit = () } diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index 748deaa24fc..b8ec7118205 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -54,6 +54,4 @@ class FileLogger( } override def rawOutputStream: PrintStream = outputStream - def endTicker(): Unit = () - def globalTicker(s: String): Unit = () } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 96d88716276..5dc6a855771 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -47,7 +47,7 @@ class MultiLogger( logger1.endTicker() logger2.endTicker() } - def globalTicker(s: String): Unit = { + override def globalTicker(s: String): Unit = { logger1.globalTicker(s) logger2.globalTicker(s) } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 217924e63bf..8d526f53602 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -51,7 +51,7 @@ private[mill] class MultilinePromptLogger( def info(s: String): Unit = synchronized { systemStreams.err.println(s) } def error(s: String): Unit = synchronized { systemStreams.err.println(s) } - def globalTicker(s: String): Unit = { + override def globalTicker(s: String): Unit = { state.updateGlobal(s) } override def endTicker(): Unit = synchronized { diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 9b64e0aad71..96bc7a16108 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -51,8 +51,8 @@ class PrefixLogger( outStream0 = Some(outStream), errStream0 = Some(systemStreams.err) ) - def endTicker(): Unit = logger0.endTicker() - def globalTicker(s: String): Unit = logger0.globalTicker(s) + override def endTicker(): Unit = logger0.endTicker() + override def globalTicker(s: String): Unit = logger0.globalTicker(s) } object PrefixLogger { diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index 001ff9eec77..f59f65d66cd 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -78,8 +78,6 @@ class PrintLogger( } override def rawOutputStream: PrintStream = systemStreams.out - def endTicker(): Unit = () - def globalTicker(s: String): Unit = () } object PrintLogger { diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 28bfd8c333f..4358f8a392a 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -23,6 +23,6 @@ class ProxyLogger(logger: Logger) extends Logger { override def close(): Unit = logger.close() override def rawOutputStream: PrintStream = logger.rawOutputStream - def endTicker(): Unit = logger.endTicker() - def globalTicker(s: String): Unit = logger.globalTicker(s) + override def endTicker(): Unit = logger.endTicker() + override def globalTicker(s: String): Unit = logger.globalTicker(s) } diff --git a/testkit/src/mill/testkit/IntegrationTester.scala b/testkit/src/mill/testkit/IntegrationTester.scala index f0ae7bdbfc8..309f2b957ac 100644 --- a/testkit/src/mill/testkit/IntegrationTester.scala +++ b/testkit/src/mill/testkit/IntegrationTester.scala @@ -69,7 +69,7 @@ object IntegrationTester { val debugArgs = Option.when(debugLog)("--debug") - val shellable: os.Shellable = (millExecutable, "--disable-ticker", serverArgs, debugArgs, cmd) + val shellable: os.Shellable = (millExecutable, serverArgs, "--disable-ticker", debugArgs, cmd) val res0 = os.call( cmd = shellable, env = env, From b6ad58c3dca02e774b8ff3360a25787eee5255ca Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 18:00:53 +0800 Subject: [PATCH 029/116] make use of org.jline.terminal --- .../src/mill/util/MultilinePromptLogger.scala | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 8d526f53602..9b6960bed32 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -138,12 +138,15 @@ private object MultilinePromptLogger { statuses.remove(k) } } - // Limit to <120 chars wide - val maxWidth = 119 + + // -1 to leave a bit of buffer + val maxWidth = ConsoleDim.width() - 1 + // -2 to account for 1 line header and 1 line `more threads` + val maxHeight = math.max(1, ConsoleDim.height() / 3 - 2) val headerSuffix = renderSeconds(now - startTimeMillis) val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) - val body = statuses + val body0 = statuses .collect { case (threadId, status) => splitShorten( @@ -153,6 +156,10 @@ private object MultilinePromptLogger { } .toList + val body = + if (body0.length < maxHeight) body0 + else body0.take(maxHeight) ++ Seq(s"... and ${body0.length - maxHeight} more threads") + val currentPrompt = header :: body val currentHeight = body.length + 1 currentPromptBytes = @@ -235,4 +242,10 @@ private object MultilinePromptLogger { } ??? } + + object ConsoleDim { + private val terminal = org.jline.terminal.TerminalBuilder.terminal() + def width(): Int = terminal.getWidth + def height(): Int = terminal.getHeight + } } From 7fda88fb70115094335e31c55dc4fc59da605608 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 18:20:38 +0800 Subject: [PATCH 030/116] prompt title centering --- main/util/src/mill/util/MultilinePromptLogger.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 9b6960bed32..53b9fbe44e9 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -210,13 +210,15 @@ private object MultilinePromptLogger { val headerPrefixStr = s" $headerPrefix0 " val headerSuffixStr = s" $headerSuffix0" val titleText = s" $titleText0 " - val maxTitleLength = maxWidth - headerPrefixStr.length - headerSuffixStr.length + // -12 just to ensure we always have some ==== divider on each side of the title + val maxTitleLength = maxWidth - math.max(headerPrefixStr.length, headerSuffixStr.length) * 2 - 12 val shortenedTitle = splitShorten(titleText, maxTitleLength) - val nonDividerLength = headerPrefixStr.length + headerSuffixStr.length + shortenedTitle.length - val divider = "=" * (maxWidth - nonDividerLength) - val (divider1, divider2) = divider.splitAt(divider.length / 2) - val headerString = headerPrefixStr + divider1 + shortenedTitle + divider2 + headerSuffixStr + // +2 to offset the title a bit to the right so it looks centered, as + // the `headerPrefixStr` is usually longer than `headerSuffixStr`, + val leftDivider = "=" * ((maxWidth / 2) - (titleText.length / 2) - headerPrefixStr.length + 2) + val rightDivider = "=" * (maxWidth - headerPrefixStr.length - leftDivider.length - shortenedTitle.length - headerSuffixStr.length) + val headerString = headerPrefixStr + leftDivider + shortenedTitle + rightDivider + headerSuffixStr assert( headerString.length == maxWidth, s"${pprint.apply(headerString)} is length ${headerString.length}, requires $maxWidth" From 4c5efd5d3d4f8fe93973cc31418d3eabe72da83b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 18:27:36 +0800 Subject: [PATCH 031/116] preserve prompt header for final render during shutdown --- .../src/mill/util/MultilinePromptLogger.scala | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 53b9fbe44e9..0b7183e3580 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -22,7 +22,7 @@ private[mill] class MultilinePromptLogger( ) override def close(): Unit = { - state.refreshPrompt() + state.refreshPrompt(ending = true) stopped = true } @@ -130,15 +130,19 @@ private object MultilinePromptLogger { (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes private var currentPromptBytes: Array[Byte] = Array[Byte]() - private def updatePromptBytes() = { + private def updatePromptBytes(ending: Boolean = false) = { val now = System.currentTimeMillis() for (k <- statuses.keySet) { val removedTime = statuses(k).removedTimeMillis - if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis) { + if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis){ statuses.remove(k) } } + // For the ending prompt, make sure we clear out all + // the statuses to only show the header alone + if (ending) statuses.clear() + // -1 to leave a bit of buffer val maxWidth = ConsoleDim.width() - 1 // -2 to account for 1 line header and 1 line `more threads` @@ -162,10 +166,15 @@ private object MultilinePromptLogger { val currentPrompt = header :: body val currentHeight = body.length + 1 - currentPromptBytes = - (AnsiNav.clearScreen(0) + currentPrompt.mkString("\n") + "\n" + AnsiNav.up( - currentHeight - )).getBytes + // For the ending prompt, leave the cursor at the bottom rather than scrolling back up. + // We do not want further output to overwrite the header as it will no longer re-render + val backUp = if (ending) "" else AnsiNav.up(currentHeight) + currentPromptBytes = ( + AnsiNav.clearScreen(0) + + currentPrompt.mkString("\n") + + "\n" + + backUp + ).getBytes } def updateGlobal(s: String): Unit = synchronized { @@ -190,8 +199,8 @@ private object MultilinePromptLogger { res } - def refreshPrompt(): Unit = synchronized { - updatePromptBytes() + def refreshPrompt(ending: Boolean = false): Unit = synchronized { + updatePromptBytes(ending) if (enableTicker) systemStreams0.err.write(currentPromptBytes) } From 3269c72bd7655c51e085943bfce99e0d455f0818 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 18:43:11 +0800 Subject: [PATCH 032/116] renderHeader tests --- .../src/mill/util/MultilinePromptLogger.scala | 6 ++- .../util/MultilinePromptLoggerTests.scala | 51 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 0b7183e3580..7b1f6e3263a 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -223,8 +223,10 @@ private object MultilinePromptLogger { val maxTitleLength = maxWidth - math.max(headerPrefixStr.length, headerSuffixStr.length) * 2 - 12 val shortenedTitle = splitShorten(titleText, maxTitleLength) - // +2 to offset the title a bit to the right so it looks centered, as - // the `headerPrefixStr` is usually longer than `headerSuffixStr`, + // +2 to offset the title a bit to the right so it looks centered, as the `headerPrefixStr` + // is usually longer than `headerSuffixStr`. We use a fixed offset rather than dynamically + // offsetting by `headerPrefixStr.length` to prevent the title from shifting left and right + // as the `headerPrefixStr` changes, even at the expense of it not being perfectly centered. val leftDivider = "=" * ((maxWidth / 2) - (titleText.length / 2) - headerPrefixStr.length + 2) val rightDivider = "=" * (maxWidth - headerPrefixStr.length - leftDivider.length - shortenedTitle.length - headerSuffixStr.length) val headerString = headerPrefixStr + leftDivider + shortenedTitle + rightDivider + headerSuffixStr diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index be0c4bfdda8..a066f0d8348 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -5,7 +5,7 @@ import utest._ object MultilinePromptLoggerTests extends TestSuite { val tests = Tests { - test { + test("lastIndexOfNewline") { // Fuzz test to make sure our custom fast `lastIndexOfNewline` logic behaves // the same as a slower generic implementation using `.slice.lastIndexOf` val allSampleByteArrays = Seq[Array[Byte]]( @@ -55,6 +55,55 @@ object MultilinePromptLoggerTests extends TestSuite { } } } + test("renderHeader"){ + import MultilinePromptLogger.renderHeader + def check(prefix: String, title: String, suffix: String, maxWidth: Int, expected: String) = { + val rendered = renderHeader(prefix, title, suffix, maxWidth) + // leave two spaces open on the left so there's somewhere to park the cursor + assert(expected == rendered) + assert(rendered.length == maxWidth) + rendered + } + test("simple") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 60, + expected = " PREFIX ==================== TITLE ================= SUFFIX" + ) + + test("short") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 40, + expected = " PREFIX ========== TITLE ======= SUFFIX" + ) + + test("shorter") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 25, + expected = " PREFIX ==...==== SUFFIX" + ) + + test("truncateTitle") - check( + "PREFIX", + "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "SUFFIX", + 60, + expected = " PREFIX ====== TITLE_ABCDEF...OPQRSTUVWXYZ ========= SUFFIX" + ) + + test("asymmetricTruncateTitle") - check( + "PREFIX_LONG", + "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "SUFFIX", + 60, + expected = " PREFIX_LONG = TITLE_A...TUVWXYZ =================== SUFFIX" + ) + } } } From d67b456104049a30a7db72c9fba86e4fa949dc44 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 18:49:11 +0800 Subject: [PATCH 033/116] more debouncing on removal --- main/util/src/mill/util/MultilinePromptLogger.scala | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 7b1f6e3263a..285f318a2cb 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -112,8 +112,11 @@ private object MultilinePromptLogger { * those occurrences even further. */ private val statusRemovalDelayMillis = 500 + private val statusRemovalDelayMillis2 = 5000 - private case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) + private case class Status(startTimeMillis: Long, + text: String, + var removedTimeMillis: Long) private class State( titleText: String, @@ -134,7 +137,7 @@ private object MultilinePromptLogger { val now = System.currentTimeMillis() for (k <- statuses.keySet) { val removedTime = statuses(k).removedTimeMillis - if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis){ + if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis2){ statuses.remove(k) } } @@ -153,12 +156,14 @@ private object MultilinePromptLogger { val body0 = statuses .collect { case (threadId, status) => - splitShorten( + if (now - status.removedTimeMillis > statusRemovalDelayMillis) "" + else splitShorten( status.text + " " + renderSeconds(now - status.startTimeMillis), maxWidth ) } .toList + .sortBy(_.isEmpty) val body = if (body0.length < maxHeight) body0 From 616dea69359125baf7b18def6029a35bdcf41d88 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 18:54:02 +0800 Subject: [PATCH 034/116] Long.MaxValue --- build.mill | 1 + main/util/src/mill/util/MultilinePromptLogger.scala | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.mill b/build.mill index a4039a6288d..2024a17ab46 100644 --- a/build.mill +++ b/build.mill @@ -20,6 +20,7 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload +//import $packages._ object Settings { val pomOrg = "com.lihaoyi" diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 285f318a2cb..041ca30aa24 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -137,7 +137,7 @@ private object MultilinePromptLogger { val now = System.currentTimeMillis() for (k <- statuses.keySet) { val removedTime = statuses(k).removedTimeMillis - if (removedTime != -1 && now - removedTime > statusRemovalDelayMillis2){ + if (now - removedTime > statusRemovalDelayMillis2){ statuses.remove(k) } } @@ -192,7 +192,7 @@ private object MultilinePromptLogger { val now = System.currentTimeMillis() sOpt match { case None => statuses.get(threadId).foreach(_.removedTimeMillis = now) - case Some(s) => statuses(threadId) = Status(now, s, -1) + case Some(s) => statuses(threadId) = Status(now, s, Long.MaxValue) } updatePromptBytes() } From a5e1385f323cace9e1a7996c8449c51c9b079ed0 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 20:33:08 +0800 Subject: [PATCH 035/116] transfer terminfo from client --- .../client/src/mill/main/client/OutFiles.java | 3 +- .../src/mill/main/client/ServerFiles.java | 5 ++ .../src/mill/main/client/ServerLauncher.java | 16 ++--- main/package.mill | 5 +- .../src/mill/util/MultilinePromptLogger.scala | 34 +++++++---- .../mill/runner/client/MillClientMain.java | 4 +- .../runner/client/MillProcessLauncher.java | 59 +++++++++++++------ runner/src/mill/runner/CodeGen.scala | 2 + runner/src/mill/runner/MillMain.scala | 18 ++++-- runner/src/mill/runner/MillServerMain.scala | 5 +- .../mill/testkit/IntegrationTesterBase.scala | 4 +- 11 files changed, 105 insertions(+), 50 deletions(-) diff --git a/main/client/src/mill/main/client/OutFiles.java b/main/client/src/mill/main/client/OutFiles.java index 88fdfd28d44..ba7dce7bacf 100644 --- a/main/client/src/mill/main/client/OutFiles.java +++ b/main/client/src/mill/main/client/OutFiles.java @@ -41,11 +41,12 @@ public class OutFiles { * Subfolder of `out/` that contains the machinery necessary for a single Mill background * server: metadata files, pipes, logs, etc. */ - final public static String millWorker = "mill-worker-"; + final public static String millServer = "mill-server"; /** * Subfolder of `out/` used to contain the Mill subprocess when run in no-server mode */ final public static String millNoServer = "mill-no-server"; + } diff --git a/main/client/src/mill/main/client/ServerFiles.java b/main/client/src/mill/main/client/ServerFiles.java index 208237ae6e6..d303d22b349 100644 --- a/main/client/src/mill/main/client/ServerFiles.java +++ b/main/client/src/mill/main/client/ServerFiles.java @@ -67,4 +67,9 @@ public static String pipe(String base) { * Where the server's stderr is piped to */ final public static String stderr = "stderr"; + + /** + * Terminal information + */ + final public static String terminfo = "terminfo"; } diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 58fbb2ee1e2..ab162de1aee 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -14,6 +14,7 @@ import java.io.PrintStream; import java.net.Socket; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.function.BiConsumer; @@ -46,12 +47,12 @@ public abstract class ServerLauncher { public static class Result{ public int exitCode; - public String serverDir; + public Path serverDir; } final static int tailerRefreshIntervalMillis = 2; final int serverProcessesLimit = 5; final int serverInitWaitMillis = 10000; - public abstract void initServer(String serverDir, boolean b, Locks locks) throws Exception; + public abstract void initServer(Path serverDir, boolean b, Locks locks) throws Exception; InputStream stdin; PrintStream stdout; PrintStream stderr; @@ -92,12 +93,11 @@ public Result acquireLocksAndRun(String outDir) throws Exception { int serverIndex = 0; while (serverIndex < serverProcessesLimit) { // Try each possible server process (-1 to -5) serverIndex++; - final String serverDir = outDir + "/" + millWorker + versionAndJvmHomeEncoding + "-" + serverIndex; - java.io.File serverDirFile = new java.io.File(serverDir); - serverDirFile.mkdirs(); + final Path serverDir = Paths.get(outDir, millServer, versionAndJvmHomeEncoding + "-" + serverIndex); + Files.createDirectories(serverDir); try ( - Locks locks = memoryLocks != null ? memoryLocks[serverIndex-1] : Locks.files(serverDir); + Locks locks = memoryLocks != null ? memoryLocks[serverIndex-1] : Locks.files(serverDir.toString()); TryLocked clientLocked = locks.clientLock.tryLock() ) { if (clientLocked.isLocked()) { @@ -111,7 +111,7 @@ public Result acquireLocksAndRun(String outDir) throws Exception { throw new ServerCouldNotBeStarted("Reached max server processes limit: " + serverProcessesLimit); } - int run(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception { + int run(Path serverDir, boolean setJnaNoSys, Locks locks) throws Exception { try( final FileToStreamTailer stdoutTailer = new FileToStreamTailer( new java.io.File(serverDir + "/" + ServerFiles.stdout), @@ -139,7 +139,7 @@ int run(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception { while (locks.processLock.probe()) Thread.sleep(3); - String socketName = ServerFiles.pipe(serverDir); + String socketName = ServerFiles.pipe(serverDir.toString()); AFUNIXSocketAddress addr = AFUNIXSocketAddress.of(new File(socketName)); long retryStart = System.currentTimeMillis(); diff --git a/main/package.mill b/main/package.mill index a2696ff9d48..1453e882b25 100644 --- a/main/package.mill +++ b/main/package.mill @@ -171,7 +171,10 @@ object `package` extends RootModule with build.MillStableScalaModule with BuildI object client extends build.MillPublishJavaModule with BuildInfo { def buildInfoPackageName = "mill.main.client" def buildInfoMembers = Seq(BuildInfo.Value("millVersion", build.millVersion(), "Mill version.")) - def ivyDeps = Agg(build.Deps.junixsocket) + def ivyDeps = Agg( + build.Deps.junixsocket, + build.Deps.jline + ) object test extends JavaModuleTests with TestModule.Junit4 { def ivyDeps = Agg( diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 041ca30aa24..cf95f8b4e17 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -11,10 +11,21 @@ private[mill] class MultilinePromptLogger( override val errorColor: fansi.Attrs, systemStreams0: SystemStreams, override val debugEnabled: Boolean, - titleText: String + titleText: String, + terminfoPath: os.Path ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ - private val state = new State(titleText, enableTicker, systemStreams0, System.currentTimeMillis()) + + var termWidth = 119 + var termHeight = 50 + private val state = new State( + titleText, + enableTicker, + systemStreams0, + System.currentTimeMillis(), + () => termWidth, + () => termHeight + ) val systemStreams = new SystemStreams( new PrintStream(new StateStream(systemStreams0.out)), new PrintStream(new StateStream(systemStreams0.err)), @@ -40,6 +51,9 @@ private[mill] class MultilinePromptLogger( Thread.sleep(promptUpdateIntervalMillis) if (!paused) { synchronized { + val s"$termWidth0 $termHeight0" = os.read(terminfoPath) + termWidth = termWidth0.toInt + termHeight = termHeight0.toInt state.refreshPrompt() } } @@ -112,7 +126,7 @@ private object MultilinePromptLogger { * those occurrences even further. */ private val statusRemovalDelayMillis = 500 - private val statusRemovalDelayMillis2 = 5000 + private val statusRemovalDelayMillis2 = 2500 private case class Status(startTimeMillis: Long, text: String, @@ -122,7 +136,9 @@ private object MultilinePromptLogger { titleText: String, enableTicker: Boolean, systemStreams0: SystemStreams, - startTimeMillis: Long + startTimeMillis: Long, + consoleWidth: () => Int, + consoleHeight: () => Int, ) { private val statuses = collection.mutable.SortedMap.empty[Int, Status] @@ -147,9 +163,9 @@ private object MultilinePromptLogger { if (ending) statuses.clear() // -1 to leave a bit of buffer - val maxWidth = ConsoleDim.width() - 1 + val maxWidth = consoleWidth() - 1 // -2 to account for 1 line header and 1 line `more threads` - val maxHeight = math.max(1, ConsoleDim.height() / 3 - 2) + val maxHeight = math.max(1, consoleHeight() / 3 - 2) val headerSuffix = renderSeconds(now - startTimeMillis) val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) @@ -260,10 +276,4 @@ private object MultilinePromptLogger { } ??? } - - object ConsoleDim { - private val terminal = org.jline.terminal.TerminalBuilder.terminal() - def width(): Int = terminal.getWidth - def height(): Int = terminal.getHeight - } } diff --git a/runner/client/src/mill/runner/client/MillClientMain.java b/runner/client/src/mill/runner/client/MillClientMain.java index 8044da5a914..0e6b6fffbf6 100644 --- a/runner/client/src/mill/runner/client/MillClientMain.java +++ b/runner/client/src/mill/runner/client/MillClientMain.java @@ -1,5 +1,7 @@ package mill.runner.client; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import mill.main.client.ServerLauncher; import mill.main.client.Util; @@ -35,7 +37,7 @@ public static void main(String[] args) throws Exception { } else try { // start in client-server mode ServerLauncher launcher = new ServerLauncher(System.in, System.out, System.err, System.getenv(), args, null, -1){ - public void initServer(String serverDir, boolean setJnaNoSys, Locks locks) throws Exception{ + public void initServer(Path serverDir, boolean setJnaNoSys, Locks locks) throws Exception{ MillProcessLauncher.launchMillServer(serverDir, setJnaNoSys); } }; diff --git a/runner/client/src/mill/runner/client/MillProcessLauncher.java b/runner/client/src/mill/runner/client/MillProcessLauncher.java index f15139cc769..55d10299b91 100644 --- a/runner/client/src/mill/runner/client/MillProcessLauncher.java +++ b/runner/client/src/mill/runner/client/MillProcessLauncher.java @@ -6,9 +6,13 @@ import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.Terminal; + import mill.main.client.Util; import mill.main.client.ServerFiles; import mill.main.client.EnvVars; @@ -17,22 +21,23 @@ public class MillProcessLauncher { static int launchMillNoServer(String[] args) throws Exception { final boolean setJnaNoSys = System.getProperty("jna.nosys") == null; + final String sig = String.format("%08x", UUID.randomUUID().hashCode()); + final Path processDir = Paths.get(".").resolve(out).resolve(millNoServer).resolve(sig); final List l = new ArrayList<>(); l.addAll(millLaunchJvmCommand(setJnaNoSys)); l.add("mill.runner.MillMain"); + l.add(processDir.toAbsolutePath().toString()); l.addAll(Arrays.asList(args)); final ProcessBuilder builder = new ProcessBuilder() .command(l) .inheritIO(); - final String sig = String.format("%08x", UUID.randomUUID().hashCode()); - boolean interrupted = false; - final String sandbox = out + "/" + millNoServer + "-" + sig; + try { - return configureRunMillProcess(builder, sandbox).waitFor(); + return configureRunMillProcess(builder, processDir).waitFor(); } catch (InterruptedException e) { interrupted = true; @@ -40,7 +45,7 @@ static int launchMillNoServer(String[] args) throws Exception { } finally { if (!interrupted) { // cleanup if process terminated for sure - Files.walk(Paths.get(sandbox)) + Files.walk(processDir) // depth-first .sorted(Comparator.reverseOrder()) .forEach(p -> p.toFile().delete()); @@ -48,31 +53,51 @@ static int launchMillNoServer(String[] args) throws Exception { } } - static void launchMillServer(String serverDir, boolean setJnaNoSys) throws Exception { + static void launchMillServer(Path serverDir, boolean setJnaNoSys) throws Exception { List l = new ArrayList<>(); l.addAll(millLaunchJvmCommand(setJnaNoSys)); l.add("mill.runner.MillServerMain"); - l.add(new File(serverDir).getCanonicalPath()); - - File stdout = new java.io.File(serverDir + "/" + ServerFiles.stdout); - File stderr = new java.io.File(serverDir + "/" + ServerFiles.stderr); + l.add(serverDir.toFile().getCanonicalPath()); ProcessBuilder builder = new ProcessBuilder() .command(l) - .redirectOutput(stdout) - .redirectError(stderr); + .redirectOutput(serverDir.resolve(ServerFiles.stdout).toFile()) + .redirectError(serverDir.resolve(ServerFiles.stderr).toFile()); - configureRunMillProcess(builder, serverDir + "/" + ServerFiles.sandbox); + configureRunMillProcess(builder, serverDir); } static Process configureRunMillProcess( ProcessBuilder builder, - String serverDir + Path serverDir ) throws Exception { + Terminal term = TerminalBuilder.terminal(); + Path sandbox = serverDir.resolve(ServerFiles.sandbox); + Files.createDirectories(sandbox); + Files.write( + serverDir.resolve(ServerFiles.terminfo), + (term.getWidth() + " " + term.getHeight()).getBytes() + ); + Thread termInfoPropagatorThread = new Thread( + () -> { + try { + + while(true){ + Files.write( + serverDir.resolve(ServerFiles.terminfo), + (term.getWidth() + " " + term.getHeight()).getBytes() + ); + + Thread.sleep(100); + } + }catch (Exception e){} + }, + "TermInfoPropagatorThread" + ); + termInfoPropagatorThread.start(); builder.environment().put(EnvVars.MILL_WORKSPACE_ROOT, new File("").getCanonicalPath()); - File sandbox = new java.io.File(serverDir + "/" + ServerFiles.sandbox); - sandbox.mkdirs(); - builder.directory(sandbox); + + builder.directory(sandbox.toFile()); return builder.start(); } diff --git a/runner/src/mill/runner/CodeGen.scala b/runner/src/mill/runner/CodeGen.scala index 57ae17d81f6..8baf0736799 100644 --- a/runner/src/mill/runner/CodeGen.scala +++ b/runner/src/mill/runner/CodeGen.scala @@ -164,6 +164,7 @@ object CodeGen { |$newScriptCode |object $wrapperObjectName extends $wrapperObjectName { | $childAliases + | @_root_.scala.annotation.nowarn | override lazy val millDiscover: _root_.mill.define.Discover = _root_.mill.define.Discover[this.type] |}""".stripMargin case None => @@ -217,6 +218,7 @@ object CodeGen { // object initialization due to https://github.com/scala/scala3/issues/21444 s"""object $wrapperObjectName extends $wrapperObjectName{ | $childAliases + | @_root_.scala.annotation.nowarn | override lazy val millDiscover: _root_.mill.define.Discover = _root_.mill.define.Discover[this.type] |} |abstract class $wrapperObjectName $extendsClause {""".stripMargin diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 9129d789471..10ded7ac009 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -8,6 +8,7 @@ import mill.java9rtexport.Export import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal} import mill.bsp.{BspContext, BspServerResult} import mill.main.BuildInfo +import mill.main.client.ServerFiles import mill.util.{MultilinePromptLogger, PrintLogger} import java.lang.reflect.InvocationTargetException @@ -67,7 +68,7 @@ object MillMain { val (result, _) = try main0( - args = args, + args = args.tail, stateCache = RunnerState.empty, mainInteractive = mill.util.Util.isInteractive(), streams0 = runnerStreams, @@ -76,7 +77,8 @@ object MillMain { setIdle = b => (), userSpecifiedProperties0 = Map(), initialSystemProperties = sys.props.toMap, - systemExit = i => sys.exit(i) + systemExit = i => sys.exit(i), + serverDir = os.Path(args.head) ) catch handleMillException(runnerStreams.err, ()) finally { @@ -95,7 +97,8 @@ object MillMain { setIdle: Boolean => Unit, userSpecifiedProperties0: Map[String, String], initialSystemProperties: Map[String, String], - systemExit: Int => Nothing + systemExit: Int => Nothing, + serverDir: os.Path ): (Boolean, RunnerState) = { val printLoggerState = new PrintLogger.State() val streams = streams0 @@ -163,7 +166,8 @@ object MillMain { config.ticker .orElse(config.enableTicker) .orElse(Option.when(config.disableTicker.value)(false)), - printLoggerState + printLoggerState, + serverDir ) try { if (!config.silent.value) { @@ -309,7 +313,8 @@ object MillMain { config: MillCliConfig, mainInteractive: Boolean, enableTicker: Option[Boolean], - printLoggerState: PrintLogger.State + printLoggerState: PrintLogger.State, + serverDir: os.Path ): mill.util.ColorLogger = { val colored = config.color.getOrElse(mainInteractive) val colors = if (colored) mill.util.Colors.Default else mill.util.Colors.BlackWhite @@ -333,7 +338,8 @@ object MillMain { errorColor = colors.error, systemStreams0 = streams, debugEnabled = config.debugLog.value, - titleText = config.leftoverArgs.value.mkString(" ") + titleText = config.leftoverArgs.value.mkString(" "), + terminfoPath = serverDir / ServerFiles.terminfo ) } diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 996e4df80a2..4f11459b68c 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -57,7 +57,7 @@ class MillServerMain( setIdle: Boolean => Unit, userSpecifiedProperties: Map[String, String], initialSystemProperties: Map[String, String], - systemExit: Int => Nothing + systemExit: Int => Nothing, ): (Boolean, RunnerState) = { try MillMain.main0( args = args, @@ -69,7 +69,8 @@ class MillServerMain( setIdle = setIdle, userSpecifiedProperties0 = userSpecifiedProperties, initialSystemProperties = initialSystemProperties, - systemExit = systemExit + systemExit = systemExit, + serverDir = serverDir ) catch MillMain.handleMillException(streams.err, stateCache) } diff --git a/testkit/src/mill/testkit/IntegrationTesterBase.scala b/testkit/src/mill/testkit/IntegrationTesterBase.scala index e3c25c641b6..589728cc2f7 100644 --- a/testkit/src/mill/testkit/IntegrationTesterBase.scala +++ b/testkit/src/mill/testkit/IntegrationTesterBase.scala @@ -1,6 +1,6 @@ package mill.testkit import mill.api.Retry -import mill.main.client.OutFiles.{millWorker, out} +import mill.main.client.OutFiles.{millServer, out} import mill.main.client.ServerFiles.serverId trait IntegrationTesterBase { @@ -49,7 +49,7 @@ trait IntegrationTesterBase { if (os.exists(workspacePath / out)) { val serverIdFiles = for { outPath <- os.list.stream(workspacePath / out) - if outPath.last.startsWith(millWorker) + if outPath.last.startsWith(millServer) } yield outPath / serverId serverIdFiles.foreach(os.remove(_)) From 325ccc11b30ffd9f1ced2acf34e1dc73569e5ba6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 20:59:34 +0800 Subject: [PATCH 036/116] renderPrompt unit tests --- .../src/mill/util/MultilinePromptLogger.scala | 97 ++++++++------- .../util/MultilinePromptLoggerTests.scala | 110 ++++++++++++++++++ .../runner/client/MillProcessLauncher.java | 12 +- 3 files changed, 174 insertions(+), 45 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index cf95f8b4e17..1890a0c29ee 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -46,19 +46,21 @@ private[mill] class MultilinePromptLogger( finally paused = false } - val promptUpdaterThread = new Thread(() => { + val promptUpdaterThread = new Thread(() => while (!stopped) { Thread.sleep(promptUpdateIntervalMillis) if (!paused) { synchronized { - val s"$termWidth0 $termHeight0" = os.read(terminfoPath) - termWidth = termWidth0.toInt - termHeight = termHeight0.toInt + try { + val s"$termWidth0 $termHeight0" = os.read(terminfoPath) + termWidth = termWidth0.toInt + termHeight = termHeight0.toInt + } catch{case e: Exception => /*donothing*/} state.refreshPrompt() } } } - }) + ) promptUpdaterThread.start() @@ -128,7 +130,7 @@ private object MultilinePromptLogger { private val statusRemovalDelayMillis = 500 private val statusRemovalDelayMillis2 = 2500 - private case class Status(startTimeMillis: Long, + private[mill] case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) @@ -162,39 +164,23 @@ private object MultilinePromptLogger { // the statuses to only show the header alone if (ending) statuses.clear() - // -1 to leave a bit of buffer - val maxWidth = consoleWidth() - 1 - // -2 to account for 1 line header and 1 line `more threads` - val maxHeight = math.max(1, consoleHeight() / 3 - 2) - val headerSuffix = renderSeconds(now - startTimeMillis) - - val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) - val body0 = statuses - .collect { - case (threadId, status) => - if (now - status.removedTimeMillis > statusRemovalDelayMillis) "" - else splitShorten( - status.text + " " + renderSeconds(now - status.startTimeMillis), - maxWidth - ) - } - .toList - .sortBy(_.isEmpty) - - val body = - if (body0.length < maxHeight) body0 - else body0.take(maxHeight) ++ Seq(s"... and ${body0.length - maxHeight} more threads") - - val currentPrompt = header :: body - val currentHeight = body.length + 1 + val currentPromptLines = renderPrompt( + consoleWidth(), + consoleHeight(), + now, + startTimeMillis, + headerPrefix, + titleText, + statuses + ) // For the ending prompt, leave the cursor at the bottom rather than scrolling back up. // We do not want further output to overwrite the header as it will no longer re-render - val backUp = if (ending) "" else AnsiNav.up(currentHeight) + val backUp = if (ending) "" else AnsiNav.up(currentPromptLines.length) + currentPromptBytes = ( AnsiNav.clearScreen(0) + - currentPrompt.mkString("\n") + - "\n" + - backUp + currentPromptLines.mkString("\n") + "\n" + + backUp ).getBytes } @@ -225,10 +211,43 @@ private object MultilinePromptLogger { if (enableTicker) systemStreams0.err.write(currentPromptBytes) } - private def renderSeconds(millis: Long) = (millis / 1000).toInt match { - case 0 => "" - case n => s"${n}s" - } + } + private def renderSeconds(millis: Long) = (millis / 1000).toInt match { + case 0 => "" + case n => s"${n}s" + } + + def renderPrompt(consoleWidth: Int, + consoleHeight: Int, + now: Long, + startTimeMillis: Long, + headerPrefix: String, + titleText: String, + statuses: collection.SortedMap[Int, Status]) = { + // -1 to leave a bit of buffer + val maxWidth = consoleWidth - 1 + // -2 to account for 1 line header and 1 line `more threads` + val maxHeight = math.max(1, consoleHeight / 3 - 2) + val headerSuffix = renderSeconds(now - startTimeMillis) + + val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) + val body0 = statuses + .collect { + case (threadId, status) => + if (now - status.removedTimeMillis > statusRemovalDelayMillis) "" + else splitShorten( + status.text + " " + renderSeconds(now - status.startTimeMillis), + maxWidth + ) + } + .toList + .sortBy(_.isEmpty) + + val body = + if (body0.length <= maxHeight) body0 + else body0.take(maxHeight) ++ Seq(s"... and ${body0.length - maxHeight} more threads") + + header :: body } def renderHeader( diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index a066f0d8348..38127eef6d2 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -2,6 +2,8 @@ package mill.util import utest._ +import scala.collection.immutable.SortedMap + object MultilinePromptLoggerTests extends TestSuite { val tests = Tests { @@ -105,5 +107,113 @@ object MultilinePromptLoggerTests extends TestSuite { expected = " PREFIX_LONG = TITLE_A...TUVWXYZ =================== SUFFIX" ) } + + test("renderPrompt"){ + import MultilinePromptLogger.{renderPrompt, Status} + val now = System.currentTimeMillis() + test("simple") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile", + statuses = SortedMap( + 0 -> Status(now - 1000, "hello", Long.MaxValue), + 1 -> Status(now - 2000, "world", Long.MaxValue), + // 2 -> Status(now - 3000, "i am cow", Long.MaxValue), + // 3 -> Status(now - 4000, "hear me moo", Long.MaxValue) + ) + ) + val expected = List( + " 123/456 =============== __.compile ================ 1337s", + "hello 1s", + "world 2s" + ) + assert(rendered == expected) + } + + test("maxWithoutTruncation") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmn", + statuses = SortedMap( + 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz1234567890123456", Long.MaxValue), + 1 -> Status(now - 2000, "world", Long.MaxValue), + 2 -> Status(now - 3000, "i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), + ) + ) + + val expected = List( + " 123/456 ======== __.compile.abcdefghijklmn ======== 1337s", + "hello1234567890abcefghijklmnopqrstuvwxyz1234567890123456 1s", + "world 2s", + "i am cow 3s", + "hear me moo 4s" + ) + assert(rendered == expected) + } + test("minAfterTruncation") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmno", + statuses = SortedMap( + 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz12345678901234567", Long.MaxValue), + 1 -> Status(now - 2000, "world", Long.MaxValue), + 2 -> Status(now - 3000, "i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "i weight twice as much as you", Long.MaxValue), + ) + ) + + val expected = List( + " 123/456 ======= __.compile....efghijklmno ========= 1337s", + "hello1234567890abcefghijklmn...stuvwxyz12345678901234567 1s", + "world 2s", + "i am cow 3s", + "hear me moo 4s", + "... and 1 more threads" + ) + assert(rendered == expected) + } + + test("truncated") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", + statuses = SortedMap( + 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + 1 -> Status(now - 2000, "world", Long.MaxValue), + 2 -> Status(now - 3000, "i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "i weigh twice as much as you", Long.MaxValue), + 5 -> Status(now - 6000, "and i look good on the barbecue", Long.MaxValue), + ) + ) + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "hello1234567890abcefghijklmn...abcefghijklmnopqrstuvwxyz 1s", + "world 2s", + "i am cow 3s", + "hear me moo 4s", + "... and 2 more threads" + ) + assert(rendered == expected) + } + } } } diff --git a/runner/client/src/mill/runner/client/MillProcessLauncher.java b/runner/client/src/mill/runner/client/MillProcessLauncher.java index 55d10299b91..f0e2fafe27b 100644 --- a/runner/client/src/mill/runner/client/MillProcessLauncher.java +++ b/runner/client/src/mill/runner/client/MillProcessLauncher.java @@ -82,14 +82,14 @@ static Process configureRunMillProcess( () -> { try { - while(true){ - Files.write( - serverDir.resolve(ServerFiles.terminfo), - (term.getWidth() + " " + term.getHeight()).getBytes() - ); + while(true){ + Files.write( + serverDir.resolve(ServerFiles.terminfo), + (term.getWidth() + " " + term.getHeight()).getBytes() + ); Thread.sleep(100); - } + } }catch (Exception e){} }, "TermInfoPropagatorThread" From e4f3ea694baebb7f3eee71ecebb20867ae6e0f6a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 21:10:39 +0800 Subject: [PATCH 037/116] wip removalDelay test --- .../src/mill/util/MultilinePromptLogger.scala | 4 +-- .../util/MultilinePromptLoggerTests.scala | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 1890a0c29ee..f24359b0b38 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -127,8 +127,8 @@ private object MultilinePromptLogger { * is even more distracting than changing the contents of a line, so we want to minimize * those occurrences even further. */ - private val statusRemovalDelayMillis = 500 - private val statusRemovalDelayMillis2 = 2500 + val statusRemovalDelayMillis = 500 + val statusRemovalDelayMillis2 = 2500 private[mill] case class Status(startTimeMillis: Long, text: String, diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 38127eef6d2..8ef18c252dd 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -122,8 +122,6 @@ object MultilinePromptLoggerTests extends TestSuite { statuses = SortedMap( 0 -> Status(now - 1000, "hello", Long.MaxValue), 1 -> Status(now - 2000, "world", Long.MaxValue), - // 2 -> Status(now - 3000, "i am cow", Long.MaxValue), - // 3 -> Status(now - 4000, "hear me moo", Long.MaxValue) ) ) val expected = List( @@ -214,6 +212,39 @@ object MultilinePromptLoggerTests extends TestSuite { ) assert(rendered == expected) } + + test("removalDelay") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", + statuses = SortedMap( + // Not yet removed, should be shown + 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown + 1 -> Status(now - 2000, "world", now - MultilinePromptLogger.statusRemovalDelayMillis + 1), + 2 -> Status(now - 3000, "i am cow", now - MultilinePromptLogger.statusRemovalDelayMillis + 1), + // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not + // yet removed, so rendered as blank lines to prevent terminal jumping around too much + 3 -> Status(now - 4000, "hear me moo", now - MultilinePromptLogger.statusRemovalDelayMillis2 + 1), + 4 -> Status(now - 5000, "i weigh twice as much as you", now - MultilinePromptLogger.statusRemovalDelayMillis2 + 1), + 5 -> Status(now - 6000, "and i look good on the barbecue", now - MultilinePromptLogger.statusRemovalDelayMillis2 + 1), + ) + ) + pprint.log(rendered) + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "hello1234567890abcefghijklmn...abcefghijklmnopqrstuvwxyz 1s", + "world 2s", + "i am cow 3s", + "hear me moo 4s", + "... and 2 more threads" + ) + assert(rendered == expected) + } } } } From d44b0d5730011dcdccc2de418a12ba427cbea794 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 22:40:53 +0800 Subject: [PATCH 038/116] fix rendering --- main/util/src/mill/util/MultilinePromptLogger.scala | 4 ++-- .../src/mill/util/MultilinePromptLoggerTests.scala | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index f24359b0b38..27bd6fe97e9 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -244,8 +244,8 @@ private object MultilinePromptLogger { .sortBy(_.isEmpty) val body = - if (body0.length <= maxHeight) body0 - else body0.take(maxHeight) ++ Seq(s"... and ${body0.length - maxHeight} more threads") + if (body0.count(_.nonEmpty) <= maxHeight) body0.take(maxHeight) + else body0.take(maxHeight - 1) ++ Seq(s"... and ${body0.length - maxHeight + 1} more threads") header :: body } diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 8ef18c252dd..716b8b8dbd2 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -179,8 +179,7 @@ object MultilinePromptLoggerTests extends TestSuite { "hello1234567890abcefghijklmn...stuvwxyz12345678901234567 1s", "world 2s", "i am cow 3s", - "hear me moo 4s", - "... and 1 more threads" + "... and 2 more threads" ) assert(rendered == expected) } @@ -207,8 +206,7 @@ object MultilinePromptLoggerTests extends TestSuite { "hello1234567890abcefghijklmn...abcefghijklmnopqrstuvwxyz 1s", "world 2s", "i am cow 3s", - "hear me moo 4s", - "... and 2 more threads" + "... and 3 more threads" ) assert(rendered == expected) } @@ -216,7 +214,7 @@ object MultilinePromptLoggerTests extends TestSuite { test("removalDelay") { val rendered = renderPrompt( consoleWidth = 60, - consoleHeight = 20, + consoleHeight = 23, now = now, startTimeMillis = now - 1337000, headerPrefix = "123/456", @@ -234,14 +232,13 @@ object MultilinePromptLoggerTests extends TestSuite { 5 -> Status(now - 6000, "and i look good on the barbecue", now - MultilinePromptLogger.statusRemovalDelayMillis2 + 1), ) ) - pprint.log(rendered) val expected = List( " 123/456 __.compile....z1234567890 ================ 1337s", "hello1234567890abcefghijklmn...abcefghijklmnopqrstuvwxyz 1s", "world 2s", "i am cow 3s", - "hear me moo 4s", - "... and 2 more threads" + "", + "" ) assert(rendered == expected) } From d79906e3fc8f5f52ac127c448acb582216579df5 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 19 Sep 2024 22:44:44 +0800 Subject: [PATCH 039/116] fix-compile --- main/server/test/src/mill/main/server/ClientServerTests.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/server/test/src/mill/main/server/ClientServerTests.scala b/main/server/test/src/mill/main/server/ClientServerTests.scala index 31a316adb04..38980324b76 100644 --- a/main/server/test/src/mill/main/server/ClientServerTests.scala +++ b/main/server/test/src/mill/main/server/ClientServerTests.scala @@ -9,6 +9,8 @@ import mill.api.SystemStreams import scala.jdk.CollectionConverters._ import utest._ +import java.nio.file.Path + /** * Exercises the client-server logic in memory, using in-memory locks * and in-memory clients and servers @@ -94,7 +96,7 @@ object ClientServerTests extends TestSuite { memoryLocks, forceFailureForTestingMillisDelay ) { - def initServer(serverDir: String, b: Boolean, locks: Locks) = { + def initServer(serverDir: Path, b: Boolean, locks: Locks) = { val serverId = "server-" + nextServerId nextServerId += 1 new Thread(new EchoServer( From 12ec229801149b7a56eeafd8efc220af17ee45fd Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 07:19:29 +0800 Subject: [PATCH 040/116] . --- .../src/mill/util/MultilinePromptLogger.scala | 105 ++++++++++++------ .../util/MultilinePromptLoggerTests.scala | 38 ++++--- runner/src/mill/runner/MillServerMain.scala | 2 +- 3 files changed, 96 insertions(+), 49 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 27bd6fe97e9..c5bb43f1f8f 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -16,15 +16,23 @@ private[mill] class MultilinePromptLogger( ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ - var termWidth = 119 - var termHeight = 50 + val termWidth0 = 119 + val termHeight0 = 50 + var termWidth: Option[Int] = None + var termHeight: Option[Int] = None + readTerminalDims() + private val state = new State( titleText, enableTicker, systemStreams0, System.currentTimeMillis(), - () => termWidth, - () => termHeight + () => + Tuple3( + termWidth.getOrElse(termWidth0), + termHeight.getOrElse(termHeight0), + termWidth.isDefined + ) ) val systemStreams = new SystemStreams( new PrintStream(new StateStream(systemStreams0.out)), @@ -46,16 +54,30 @@ private[mill] class MultilinePromptLogger( finally paused = false } + def readTerminalDims(): Unit = { + try { + val s"$termWidth0 $termHeight0" = os.read(terminfoPath) + termWidth = termWidth0.toInt match { + case -1 => None + case n => Some(n) + } + termHeight = termHeight0.toInt match { + case -1 => None + case n => Some(n) + } + } catch { case e: Exception => /*donothing*/ } + } + val promptUpdaterThread = new Thread(() => while (!stopped) { - Thread.sleep(promptUpdateIntervalMillis) + Thread.sleep( + if (termWidth.isDefined) promptUpdateIntervalMillis + else nonInteractivePromptUpdateIntervalMillis + ) + if (!paused) { synchronized { - try { - val s"$termWidth0 $termHeight0" = os.read(terminfoPath) - termWidth = termWidth0.toInt - termHeight = termHeight0.toInt - } catch{case e: Exception => /*donothing*/} + readTerminalDims() state.refreshPrompt() } } @@ -121,6 +143,15 @@ private object MultilinePromptLogger { */ private val promptUpdateIntervalMillis = 100 + /** + * How often to update the multiline status prompt in noninteractive scenarios, + * e.g. background job logs or piped to a log file. Much less frequent than the + * interactive scenario because we cannot rely on ANSI codes to over-write the + * previous prompt, so we have to be a lot more conservative to avoid spamming + * the logs + */ + private val nonInteractivePromptUpdateIntervalMillis = 10000 + /** * Add some extra latency delay to the process of removing an entry from the status * prompt entirely, because removing an entry changes the height of the prompt, which @@ -130,17 +161,14 @@ private object MultilinePromptLogger { val statusRemovalDelayMillis = 500 val statusRemovalDelayMillis2 = 2500 - private[mill] case class Status(startTimeMillis: Long, - text: String, - var removedTimeMillis: Long) + private[mill] case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) private class State( titleText: String, enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long, - consoleWidth: () => Int, - consoleHeight: () => Int, + consoleDims: () => (Int, Int, Boolean) ) { private val statuses = collection.mutable.SortedMap.empty[Int, Status] @@ -155,7 +183,7 @@ private object MultilinePromptLogger { val now = System.currentTimeMillis() for (k <- statuses.keySet) { val removedTime = statuses(k).removedTimeMillis - if (now - removedTime > statusRemovalDelayMillis2){ + if (now - removedTime > statusRemovalDelayMillis2) { statuses.remove(k) } } @@ -164,9 +192,11 @@ private object MultilinePromptLogger { // the statuses to only show the header alone if (ending) statuses.clear() + val (consoleWidth, consoleHeight, consoleInteractive) = consoleDims() + // don't show prompt for non-interactive terminal val currentPromptLines = renderPrompt( - consoleWidth(), - consoleHeight(), + consoleWidth, + consoleHeight, now, startTimeMillis, headerPrefix, @@ -177,11 +207,15 @@ private object MultilinePromptLogger { // We do not want further output to overwrite the header as it will no longer re-render val backUp = if (ending) "" else AnsiNav.up(currentPromptLines.length) - currentPromptBytes = ( - AnsiNav.clearScreen(0) + - currentPromptLines.mkString("\n") + "\n" + - backUp - ).getBytes + val currentPromptStr = + if (!consoleInteractive) currentPromptLines.mkString("\n") + else { + AnsiNav.clearScreen(0) + + currentPromptLines.mkString("\n") + "\n" + + backUp + } + + currentPromptBytes = currentPromptStr.getBytes } def updateGlobal(s: String): Unit = synchronized { @@ -217,13 +251,15 @@ private object MultilinePromptLogger { case n => s"${n}s" } - def renderPrompt(consoleWidth: Int, - consoleHeight: Int, - now: Long, - startTimeMillis: Long, - headerPrefix: String, - titleText: String, - statuses: collection.SortedMap[Int, Status]) = { + def renderPrompt( + consoleWidth: Int, + consoleHeight: Int, + now: Long, + startTimeMillis: Long, + headerPrefix: String, + titleText: String, + statuses: collection.SortedMap[Int, Status] + ): List[String] = { // -1 to leave a bit of buffer val maxWidth = consoleWidth - 1 // -2 to account for 1 line header and 1 line `more threads` @@ -260,7 +296,8 @@ private object MultilinePromptLogger { val headerSuffixStr = s" $headerSuffix0" val titleText = s" $titleText0 " // -12 just to ensure we always have some ==== divider on each side of the title - val maxTitleLength = maxWidth - math.max(headerPrefixStr.length, headerSuffixStr.length) * 2 - 12 + val maxTitleLength = + maxWidth - math.max(headerPrefixStr.length, headerSuffixStr.length) * 2 - 12 val shortenedTitle = splitShorten(titleText, maxTitleLength) // +2 to offset the title a bit to the right so it looks centered, as the `headerPrefixStr` @@ -268,8 +305,10 @@ private object MultilinePromptLogger { // offsetting by `headerPrefixStr.length` to prevent the title from shifting left and right // as the `headerPrefixStr` changes, even at the expense of it not being perfectly centered. val leftDivider = "=" * ((maxWidth / 2) - (titleText.length / 2) - headerPrefixStr.length + 2) - val rightDivider = "=" * (maxWidth - headerPrefixStr.length - leftDivider.length - shortenedTitle.length - headerSuffixStr.length) - val headerString = headerPrefixStr + leftDivider + shortenedTitle + rightDivider + headerSuffixStr + val rightDivider = + "=" * (maxWidth - headerPrefixStr.length - leftDivider.length - shortenedTitle.length - headerSuffixStr.length) + val headerString = + headerPrefixStr + leftDivider + shortenedTitle + rightDivider + headerSuffixStr assert( headerString.length == maxWidth, s"${pprint.apply(headerString)} is length ${headerString.length}, requires $maxWidth" diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 716b8b8dbd2..82fcf5e59bc 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -57,7 +57,7 @@ object MultilinePromptLoggerTests extends TestSuite { } } } - test("renderHeader"){ + test("renderHeader") { import MultilinePromptLogger.renderHeader def check(prefix: String, title: String, suffix: String, maxWidth: Int, expected: String) = { @@ -108,8 +108,8 @@ object MultilinePromptLoggerTests extends TestSuite { ) } - test("renderPrompt"){ - import MultilinePromptLogger.{renderPrompt, Status} + test("renderPrompt") { + import MultilinePromptLogger._ val now = System.currentTimeMillis() test("simple") { val rendered = renderPrompt( @@ -121,7 +121,7 @@ object MultilinePromptLoggerTests extends TestSuite { titleText = "__.compile", statuses = SortedMap( 0 -> Status(now - 1000, "hello", Long.MaxValue), - 1 -> Status(now - 2000, "world", Long.MaxValue), + 1 -> Status(now - 2000, "world", Long.MaxValue) ) ) val expected = List( @@ -141,10 +141,14 @@ object MultilinePromptLoggerTests extends TestSuite { headerPrefix = "123/456", titleText = "__.compile.abcdefghijklmn", statuses = SortedMap( - 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz1234567890123456", Long.MaxValue), + 0 -> Status( + now - 1000, + "hello1234567890abcefghijklmnopqrstuvwxyz1234567890123456", + Long.MaxValue + ), 1 -> Status(now - 2000, "world", Long.MaxValue), 2 -> Status(now - 3000, "i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), + 3 -> Status(now - 4000, "hear me moo", Long.MaxValue) ) ) @@ -166,15 +170,19 @@ object MultilinePromptLoggerTests extends TestSuite { headerPrefix = "123/456", titleText = "__.compile.abcdefghijklmno", statuses = SortedMap( - 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz12345678901234567", Long.MaxValue), + 0 -> Status( + now - 1000, + "hello1234567890abcefghijklmnopqrstuvwxyz12345678901234567", + Long.MaxValue + ), 1 -> Status(now - 2000, "world", Long.MaxValue), 2 -> Status(now - 3000, "i am cow", Long.MaxValue), 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "i weight twice as much as you", Long.MaxValue), + 4 -> Status(now - 5000, "i weight twice as much as you", Long.MaxValue) ) ) - val expected = List( + val expected = List( " 123/456 ======= __.compile....efghijklmno ========= 1337s", "hello1234567890abcefghijklmn...stuvwxyz12345678901234567 1s", "world 2s", @@ -198,7 +206,7 @@ object MultilinePromptLoggerTests extends TestSuite { 2 -> Status(now - 3000, "i am cow", Long.MaxValue), 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), 4 -> Status(now - 5000, "i weigh twice as much as you", Long.MaxValue), - 5 -> Status(now - 6000, "and i look good on the barbecue", Long.MaxValue), + 5 -> Status(now - 6000, "and i look good on the barbecue", Long.MaxValue) ) ) val expected = List( @@ -223,13 +231,13 @@ object MultilinePromptLoggerTests extends TestSuite { // Not yet removed, should be shown 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(now - 2000, "world", now - MultilinePromptLogger.statusRemovalDelayMillis + 1), - 2 -> Status(now - 3000, "i am cow", now - MultilinePromptLogger.statusRemovalDelayMillis + 1), + 1 -> Status(now - 2000, "world", now - statusRemovalDelayMillis + 1), + 2 -> Status(now - 3000, "i am cow", now - statusRemovalDelayMillis + 1), // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(now - 4000, "hear me moo", now - MultilinePromptLogger.statusRemovalDelayMillis2 + 1), - 4 -> Status(now - 5000, "i weigh twice as much as you", now - MultilinePromptLogger.statusRemovalDelayMillis2 + 1), - 5 -> Status(now - 6000, "and i look good on the barbecue", now - MultilinePromptLogger.statusRemovalDelayMillis2 + 1), + 3 -> Status(now - 4000, "hear me moo", now - statusRemovalDelayMillis2 + 1), + 4 -> Status(now - 5000, "i weigh twice", now - statusRemovalDelayMillis2 + 1), + 5 -> Status(now - 6000, "as much as you", now - statusRemovalDelayMillis2 + 1) ) ) val expected = List( diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index 4f11459b68c..a6bfa48dad2 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -57,7 +57,7 @@ class MillServerMain( setIdle: Boolean => Unit, userSpecifiedProperties: Map[String, String], initialSystemProperties: Map[String, String], - systemExit: Int => Nothing, + systemExit: Int => Nothing ): (Boolean, RunnerState) = { try MillMain.main0( args = args, From 085ede3b4cc308cbe3e4c5f11ae010af7712bb11 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 07:22:04 +0800 Subject: [PATCH 041/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index c5bb43f1f8f..d122b43d76f 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -84,7 +84,7 @@ private[mill] class MultilinePromptLogger( } ) - promptUpdaterThread.start() + if (enableTicker) promptUpdaterThread.start() def info(s: String): Unit = synchronized { systemStreams.err.println(s) } From 611c7d49aad2d8f94a187cde25fda2f13c262412 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 07:28:26 +0800 Subject: [PATCH 042/116] use ProxyStream to prepare for batch prompt updates --- .../src/mill/util/MultilinePromptLogger.scala | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index d122b43d76f..e9e61d89419 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -1,6 +1,7 @@ package mill.util import mill.api.SystemStreams +import mill.main.client.ProxyStream import java.io._ @@ -34,9 +35,21 @@ private[mill] class MultilinePromptLogger( termWidth.isDefined ) ) + + val pipeIn = new PipedInputStream() + val pipeOut = new PipedOutputStream() + + pipeIn.connect(pipeOut) + + val proxyOut = new ProxyStream.Output(pipeOut, ProxyStream.OUT) + val proxyErr = new ProxyStream.Output(pipeOut, ProxyStream.ERR) + val pumper = new ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err) + + new Thread(pumper).start() + val systemStreams = new SystemStreams( - new PrintStream(new StateStream(systemStreams0.out)), - new PrintStream(new StateStream(systemStreams0.err)), + new PrintStream(new StateStream(proxyOut)/*new StateStream(systemStreams0.out)*/), + new PrintStream(new StateStream(proxyErr)/*new StateStream(systemStreams0.err)*/), systemStreams0.in ) @@ -106,7 +119,7 @@ private[mill] class MultilinePromptLogger( override def rawOutputStream: PrintStream = systemStreams0.out - private class StateStream(wrapped: PrintStream) extends OutputStream { + private class StateStream(wrapped: OutputStream) extends OutputStream { override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { lastIndexOfNewline(b, off, len) match { case -1 => wrapped.write(b, off, len) @@ -233,7 +246,7 @@ private object MultilinePromptLogger { updatePromptBytes() } - def writeWithPrompt[T](wrapped: PrintStream)(t: => T): T = synchronized { + def writeWithPrompt[T](wrapped: OutputStream)(t: => T): T = synchronized { if (enableTicker) wrapped.write(writePreludeBytes) val res = t if (enableTicker) wrapped.write(currentPromptBytes) From 34c720838068bf5e8aa0e505b384c6eaf7384dda Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 07:36:58 +0800 Subject: [PATCH 043/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index e9e61d89419..899354ca47c 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -70,12 +70,13 @@ private[mill] class MultilinePromptLogger( def readTerminalDims(): Unit = { try { val s"$termWidth0 $termHeight0" = os.read(terminfoPath) + termWidth = termWidth0.toInt match { - case -1 => None + case -1 | 0 => None case n => Some(n) } termHeight = termHeight0.toInt match { - case -1 => None + case -1 | 0 => None case n => Some(n) } } catch { case e: Exception => /*donothing*/ } From 68429862142ba8587f6def4e54b8adf66faa6a01 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 12:05:21 +0800 Subject: [PATCH 044/116] move StateStream logic into Pumper hooks for debouncing --- .../src/mill/main/client/ProxyStream.java | 15 +++++- .../src/mill/util/MultilinePromptLogger.scala | 51 +++++++------------ 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/main/client/src/mill/main/client/ProxyStream.java b/main/client/src/mill/main/client/ProxyStream.java index d28e3b3b426..d19f79c590e 100644 --- a/main/client/src/mill/main/client/ProxyStream.java +++ b/main/client/src/mill/main/client/ProxyStream.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.function.BiConsumer; +import java.util.function.Consumer; /** * Logic to capture a pair of streams (typically stdout and stderr), combining @@ -100,17 +102,25 @@ public static class Pumper implements Runnable{ private InputStream src; private OutputStream destOut; private OutputStream destErr; - public Pumper(InputStream src, OutputStream destOut, OutputStream destErr){ + public Pumper(InputStream src, + OutputStream destOut, + OutputStream destErr){ this.src = src; this.destOut = destOut; this.destErr = destErr; } + public void preRead(InputStream src){} + + + public void preWrite(){} + public void run() { byte[] buffer = new byte[1024]; while (true) { try { + this.preRead(src); int header = src.read(); // -1 means socket was closed, 0 means a ProxyStream.END was sent. Note // that only header values > 0 represent actual data to read: @@ -124,6 +134,7 @@ public void run() { int offset = 0; int delta = -1; while (offset < quantity) { + this.preRead(src); delta = src.read(buffer, offset, quantity - offset); if (delta == -1) { break; @@ -133,11 +144,11 @@ public void run() { } if (delta != -1) { + this.preWrite(); switch(stream){ case ProxyStream.OUT: destOut.write(buffer, 0, offset); break; case ProxyStream.ERR: destErr.write(buffer, 0, offset); break; } - flush(); } } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 899354ca47c..257612c70dd 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -38,18 +38,28 @@ private[mill] class MultilinePromptLogger( val pipeIn = new PipedInputStream() val pipeOut = new PipedOutputStream() - + pipeIn.available() pipeIn.connect(pipeOut) val proxyOut = new ProxyStream.Output(pipeOut, ProxyStream.OUT) val proxyErr = new ProxyStream.Output(pipeOut, ProxyStream.ERR) - val pumper = new ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err) + val pumper = new ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err){ + override def preRead(src: InputStream): Unit = { + if (src.available() == 0){ + if (enableTicker) systemStreams0.err.write(state.currentPromptBytes) + } + } + + override def preWrite(): Unit = { + systemStreams0.err.write(state.writePreludeBytes) + } + } new Thread(pumper).start() val systemStreams = new SystemStreams( - new PrintStream(new StateStream(proxyOut)/*new StateStream(systemStreams0.out)*/), - new PrintStream(new StateStream(proxyErr)/*new StateStream(systemStreams0.err)*/), + new PrintStream(proxyOut), + new PrintStream(proxyErr), systemStreams0.in ) @@ -119,33 +129,6 @@ private[mill] class MultilinePromptLogger( } override def rawOutputStream: PrintStream = systemStreams0.out - - private class StateStream(wrapped: OutputStream) extends OutputStream { - override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { - lastIndexOfNewline(b, off, len) match { - case -1 => wrapped.write(b, off, len) - case lastNewlineIndex => - val indexOfCharAfterNewline = lastNewlineIndex + 1 - // We look for the last newline in the output and use that as an anchor, since - // we know that after a newline the cursor is at column zero, and column zero - // is the only place we can reliably position the cursor since the saveCursor and - // restoreCursor ANSI codes do not work well in the presence of scrolling - state.writeWithPrompt(wrapped) { - wrapped.write(b, off, indexOfCharAfterNewline - off) - } - wrapped.write(b, indexOfCharAfterNewline, off + len - indexOfCharAfterNewline) - } - } - - override def write(b: Int): Unit = synchronized { - if (b == '\n') state.writeWithPrompt(wrapped)(wrapped.write(b)) - else wrapped.write(b) - } - - override def flush(): Unit = synchronized { - wrapped.flush() - } - } } private object MultilinePromptLogger { @@ -189,9 +172,9 @@ private object MultilinePromptLogger { private var headerPrefix = "" // Pre-compute the prelude and current prompt as byte arrays so that // writing them out is fast, since they get written out very frequently - private val writePreludeBytes: Array[Byte] = - (AnsiNav.clearScreen(0) + AnsiNav.left(9999)).getBytes - private var currentPromptBytes: Array[Byte] = Array[Byte]() + val writePreludeBytes: Array[Byte] = + AnsiNav.clearScreen(0).getBytes + @volatile var currentPromptBytes: Array[Byte] = Array[Byte]() private def updatePromptBytes(ending: Boolean = false) = { val now = System.currentTimeMillis() From 816b80096a6fd51afa5046b05481f754a09e9ecb Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 12:11:49 +0800 Subject: [PATCH 045/116] avoid duplicate preWrite clearScreens --- .../src/mill/util/MultilinePromptLogger.scala | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 257612c70dd..a90c874bcef 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -17,8 +17,6 @@ private[mill] class MultilinePromptLogger( ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ - val termWidth0 = 119 - val termHeight0 = 50 var termWidth: Option[Int] = None var termHeight: Option[Int] = None readTerminalDims() @@ -30,8 +28,8 @@ private[mill] class MultilinePromptLogger( System.currentTimeMillis(), () => Tuple3( - termWidth.getOrElse(termWidth0), - termHeight.getOrElse(termHeight0), + termWidth.getOrElse(defaultTermWidth), + termHeight.getOrElse(defaultTermHeight), termWidth.isDefined ) ) @@ -44,14 +42,23 @@ private[mill] class MultilinePromptLogger( val proxyOut = new ProxyStream.Output(pipeOut, ProxyStream.OUT) val proxyErr = new ProxyStream.Output(pipeOut, ProxyStream.ERR) val pumper = new ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err){ + object PumperState extends Enumeration{ + val init, prompt, cleared = Value + } + var pumperState = PumperState.init override def preRead(src: InputStream): Unit = { - if (src.available() == 0){ - if (enableTicker) systemStreams0.err.write(state.currentPromptBytes) + if (enableTicker && src.available() == 0){ + systemStreams0.err.write(state.currentPromptBytes) + pumperState = PumperState.prompt } } override def preWrite(): Unit = { - systemStreams0.err.write(state.writePreludeBytes) + // Before any write, make sure we clear the terminal of any prompt that was + // written earlier and not yet cleared, so the following output can be written + // to a clean section of the terminal + if (pumperState != PumperState.cleared) systemStreams0.err.write(clearScreenToEndBytes) + pumperState = PumperState.cleared } } @@ -133,6 +140,8 @@ private[mill] class MultilinePromptLogger( private object MultilinePromptLogger { + val defaultTermWidth = 119 + val defaultTermHeight = 50 /** * How often to update the multiline status prompt on the terminal. * Too frequent is bad because it causes a lot of visual noise, @@ -160,6 +169,8 @@ private object MultilinePromptLogger { private[mill] case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) + private val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes + private class State( titleText: String, enableTicker: Boolean, @@ -172,8 +183,7 @@ private object MultilinePromptLogger { private var headerPrefix = "" // Pre-compute the prelude and current prompt as byte arrays so that // writing them out is fast, since they get written out very frequently - val writePreludeBytes: Array[Byte] = - AnsiNav.clearScreen(0).getBytes + @volatile var currentPromptBytes: Array[Byte] = Array[Byte]() private def updatePromptBytes(ending: Boolean = false) = { @@ -230,19 +240,12 @@ private object MultilinePromptLogger { updatePromptBytes() } - def writeWithPrompt[T](wrapped: OutputStream)(t: => T): T = synchronized { - if (enableTicker) wrapped.write(writePreludeBytes) - val res = t - if (enableTicker) wrapped.write(currentPromptBytes) - res - } - def refreshPrompt(ending: Boolean = false): Unit = synchronized { updatePromptBytes(ending) if (enableTicker) systemStreams0.err.write(currentPromptBytes) } - } + private def renderSeconds(millis: Long) = (millis / 1000).toInt match { case 0 => "" case n => s"${n}s" From 432b1fcbd3184ae27b2cf3bc13512594e4226720 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 12:44:50 +0800 Subject: [PATCH 046/116] buffer up prompts --- .../src/mill/main/client/ProxyStream.java | 1 - .../src/mill/util/MultilinePromptLogger.scala | 213 ++++++++++-------- .../util/MultilinePromptLoggerTests.scala | 10 +- 3 files changed, 124 insertions(+), 100 deletions(-) diff --git a/main/client/src/mill/main/client/ProxyStream.java b/main/client/src/mill/main/client/ProxyStream.java index d19f79c590e..75677fd2a72 100644 --- a/main/client/src/mill/main/client/ProxyStream.java +++ b/main/client/src/mill/main/client/ProxyStream.java @@ -149,7 +149,6 @@ public void run() { case ProxyStream.OUT: destOut.write(buffer, 0, offset); break; case ProxyStream.ERR: destErr.write(buffer, 0, offset); break; } - flush(); } } } catch (org.newsclub.net.unix.ConnectionResetSocketException e) { diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index a90c874bcef..99ffa441e7b 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -17,88 +17,27 @@ private[mill] class MultilinePromptLogger( ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ - var termWidth: Option[Int] = None - var termHeight: Option[Int] = None - readTerminalDims() + private var termWidth: Option[Int] = None + private var termHeight: Option[Int] = None + + for((w, h) <- readTerminalDims(terminfoPath)) { + termWidth = w + termHeight = h + } private val state = new State( titleText, enableTicker, systemStreams0, System.currentTimeMillis(), - () => - Tuple3( - termWidth.getOrElse(defaultTermWidth), - termHeight.getOrElse(defaultTermHeight), - termWidth.isDefined - ) + () => (termWidth, termHeight) ) - val pipeIn = new PipedInputStream() - val pipeOut = new PipedOutputStream() - pipeIn.available() - pipeIn.connect(pipeOut) - - val proxyOut = new ProxyStream.Output(pipeOut, ProxyStream.OUT) - val proxyErr = new ProxyStream.Output(pipeOut, ProxyStream.ERR) - val pumper = new ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err){ - object PumperState extends Enumeration{ - val init, prompt, cleared = Value - } - var pumperState = PumperState.init - override def preRead(src: InputStream): Unit = { - if (enableTicker && src.available() == 0){ - systemStreams0.err.write(state.currentPromptBytes) - pumperState = PumperState.prompt - } - } - - override def preWrite(): Unit = { - // Before any write, make sure we clear the terminal of any prompt that was - // written earlier and not yet cleared, so the following output can be written - // to a clean section of the terminal - if (pumperState != PumperState.cleared) systemStreams0.err.write(clearScreenToEndBytes) - pumperState = PumperState.cleared - } - } - - new Thread(pumper).start() - - val systemStreams = new SystemStreams( - new PrintStream(proxyOut), - new PrintStream(proxyErr), - systemStreams0.in - ) - - override def close(): Unit = { - state.refreshPrompt(ending = true) - stopped = true - } + private val streams = new Streams(enableTicker, systemStreams0, () => state.currentPromptBytes) @volatile var stopped = false @volatile var paused = false - override def withPaused[T](t: => T): T = { - paused = true - try t - finally paused = false - } - - def readTerminalDims(): Unit = { - try { - val s"$termWidth0 $termHeight0" = os.read(terminfoPath) - - termWidth = termWidth0.toInt match { - case -1 | 0 => None - case n => Some(n) - } - termHeight = termHeight0.toInt match { - case -1 | 0 => None - case n => Some(n) - } - } catch { case e: Exception => /*donothing*/ } - } - val promptUpdaterThread = new Thread(() => while (!stopped) { Thread.sleep( @@ -108,7 +47,10 @@ private[mill] class MultilinePromptLogger( if (!paused) { synchronized { - readTerminalDims() + for((w, h) <- readTerminalDims(terminfoPath)) { + termWidth = w + termHeight = h + } state.refreshPrompt() } } @@ -117,31 +59,39 @@ private[mill] class MultilinePromptLogger( if (enableTicker) promptUpdaterThread.start() + override def withPaused[T](t: => T): T = { + paused = true + try t + finally paused = false + } + def info(s: String): Unit = synchronized { systemStreams.err.println(s) } def error(s: String): Unit = synchronized { systemStreams.err.println(s) } - override def globalTicker(s: String): Unit = { - state.updateGlobal(s) - } - override def endTicker(): Unit = synchronized { - state.updateCurrent(None) - } - def ticker(s: String): Unit = synchronized { - state.updateCurrent(Some(s)) - } + override def globalTicker(s: String): Unit = synchronized { state.updateGlobal(s) } - def debug(s: String): Unit = synchronized { - if (debugEnabled) systemStreams.err.println(s) - } + override def endTicker(): Unit = synchronized { state.updateCurrent(None) } + + def ticker(s: String): Unit = synchronized { state.updateCurrent(Some(s)) } + + def debug(s: String): Unit = synchronized { if (debugEnabled) systemStreams.err.println(s) } override def rawOutputStream: PrintStream = systemStreams0.out + + override def close(): Unit = { + streams.close() + state.refreshPrompt(ending = true) + stopped = true + } + + def systemStreams = streams.systemStreams } private object MultilinePromptLogger { - val defaultTermWidth = 119 - val defaultTermHeight = 50 + private val defaultTermWidth = 119 + private val defaultTermHeight = 50 /** * How often to update the multiline status prompt on the terminal. * Too frequent is bad because it causes a lot of visual noise, @@ -164,19 +114,76 @@ private object MultilinePromptLogger { * is even more distracting than changing the contents of a line, so we want to minimize * those occurrences even further. */ - val statusRemovalDelayMillis = 500 - val statusRemovalDelayMillis2 = 2500 + val statusRemovalHideDelayMillis = 250 + + /** + * How long to wait before actually removing the blank line left by a removed status + * and reducing the height of the prompt. + */ + val statusRemovalRemoveDelayMillis = 2000 private[mill] case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) private val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes + private class Streams(enableTicker: Boolean, + systemStreams0: SystemStreams, + currentPromptBytes: () => Array[Byte]){ + + // We force both stdout and stderr streams into a single `Piped*Stream` pair via + // `ProxyStream`, as we need to preserve the ordering of writes to each individual + // stream, and also need to know when *both* streams are quiescent so that we can + // print the prompt at the bottom + val pipeIn = new PipedInputStream() + val pipeOut = new PipedOutputStream() + pipeIn.available() + pipeIn.connect(pipeOut) + val proxyOut = new ProxyStream.Output(pipeOut, ProxyStream.OUT) + val proxyErr = new ProxyStream.Output(pipeOut, ProxyStream.ERR) + val systemStreams = new SystemStreams( + new PrintStream(proxyOut), + new PrintStream(proxyErr), + systemStreams0.in + ) + + val pumper = new ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err){ + object PumperState extends Enumeration{ + val init, prompt, cleared = Value + } + var pumperState = PumperState.init + override def preRead(src: InputStream): Unit = { + // Only bother printing the propmt after the streams have become quiescent + // and there is no more stuff to print. This helps us printing the prompt on + // every small write when most such prompts will get immediately over-written + // by subsequent writes + if (enableTicker && src.available() == 0){ + systemStreams0.err.write(currentPromptBytes()) + pumperState = PumperState.prompt + } + } + + override def preWrite(): Unit = { + // Before any write, make sure we clear the terminal of any prompt that was + // written earlier and not yet cleared, so the following output can be written + // to a clean section of the terminal + if (pumperState != PumperState.cleared) systemStreams0.err.write(clearScreenToEndBytes) + pumperState = PumperState.cleared + } + } + val pumperThread = new Thread(pumper) + pumperThread.start() + + def close() = { + pipeIn.close() + pipeOut.close() + } + } private class State( titleText: String, enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long, - consoleDims: () => (Int, Int, Boolean) + consoleDims: () => (Option[Int], Option[Int]) ) { private val statuses = collection.mutable.SortedMap.empty[Int, Status] @@ -190,7 +197,7 @@ private object MultilinePromptLogger { val now = System.currentTimeMillis() for (k <- statuses.keySet) { val removedTime = statuses(k).removedTimeMillis - if (now - removedTime > statusRemovalDelayMillis2) { + if (now - removedTime > statusRemovalRemoveDelayMillis) { statuses.remove(k) } } @@ -199,11 +206,11 @@ private object MultilinePromptLogger { // the statuses to only show the header alone if (ending) statuses.clear() - val (consoleWidth, consoleHeight, consoleInteractive) = consoleDims() + val (termWidth0, termHeight0) = consoleDims() // don't show prompt for non-interactive terminal val currentPromptLines = renderPrompt( - consoleWidth, - consoleHeight, + termWidth0.getOrElse(defaultTermWidth), + termHeight0.getOrElse(defaultTermHeight), now, startTimeMillis, headerPrefix, @@ -215,7 +222,7 @@ private object MultilinePromptLogger { val backUp = if (ending) "" else AnsiNav.up(currentPromptLines.length) val currentPromptStr = - if (!consoleInteractive) currentPromptLines.mkString("\n") + if (termWidth0.isEmpty) currentPromptLines.mkString("\n") else { AnsiNav.clearScreen(0) + currentPromptLines.mkString("\n") + "\n" + @@ -251,6 +258,24 @@ private object MultilinePromptLogger { case n => s"${n}s" } + def readTerminalDims(terminfoPath: os.Path) = { + try { + val s"$termWidth0 $termHeight0" = os.read(terminfoPath) + Some( + Tuple2( + termWidth0.toInt match { + case -1 | 0 => None + case n => Some(n) + }, + termHeight0.toInt match { + case -1 | 0 => None + case n => Some(n) + } + ) + ) + }catch{case e => None} + } + def renderPrompt( consoleWidth: Int, consoleHeight: Int, @@ -270,7 +295,7 @@ private object MultilinePromptLogger { val body0 = statuses .collect { case (threadId, status) => - if (now - status.removedTimeMillis > statusRemovalDelayMillis) "" + if (now - status.removedTimeMillis > statusRemovalHideDelayMillis) "" else splitShorten( status.text + " " + renderSeconds(now - status.startTimeMillis), maxWidth diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 82fcf5e59bc..8d377eda983 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -231,13 +231,13 @@ object MultilinePromptLoggerTests extends TestSuite { // Not yet removed, should be shown 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(now - 2000, "world", now - statusRemovalDelayMillis + 1), - 2 -> Status(now - 3000, "i am cow", now - statusRemovalDelayMillis + 1), + 1 -> Status(now - 2000, "world", now - statusRemovalHideDelayMillis + 1), + 2 -> Status(now - 3000, "i am cow", now - statusRemovalHideDelayMillis + 1), // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(now - 4000, "hear me moo", now - statusRemovalDelayMillis2 + 1), - 4 -> Status(now - 5000, "i weigh twice", now - statusRemovalDelayMillis2 + 1), - 5 -> Status(now - 6000, "as much as you", now - statusRemovalDelayMillis2 + 1) + 3 -> Status(now - 4000, "hear me moo", now - statusRemovalRemoveDelayMillis + 1), + 4 -> Status(now - 5000, "i weigh twice", now - statusRemovalRemoveDelayMillis + 1), + 5 -> Status(now - 6000, "as much as you", now - statusRemovalRemoveDelayMillis + 1) ) ) val expected = List( From 7db0c81d1ef19dea335bb64fedff2a7db6c3429d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 12:58:06 +0800 Subject: [PATCH 047/116] write final prompt before closing streams --- .../src/mill/util/MultilinePromptLogger.scala | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 99ffa441e7b..1873f657e55 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -80,8 +80,8 @@ private[mill] class MultilinePromptLogger( override def rawOutputStream: PrintStream = systemStreams0.out override def close(): Unit = { - streams.close() state.refreshPrompt(ending = true) + streams.close() stopped = true } @@ -185,6 +185,7 @@ private object MultilinePromptLogger { startTimeMillis: Long, consoleDims: () => (Option[Int], Option[Int]) ) { + var promptDirty = false private val statuses = collection.mutable.SortedMap.empty[Int, Status] private var headerPrefix = "" @@ -234,7 +235,7 @@ private object MultilinePromptLogger { def updateGlobal(s: String): Unit = synchronized { headerPrefix = s - updatePromptBytes() + promptDirty = true } def updateCurrent(sOpt: Option[String]): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt @@ -244,12 +245,16 @@ private object MultilinePromptLogger { case None => statuses.get(threadId).foreach(_.removedTimeMillis = now) case Some(s) => statuses(threadId) = Status(now, s, Long.MaxValue) } - updatePromptBytes() + promptDirty = true } def refreshPrompt(ending: Boolean = false): Unit = synchronized { - updatePromptBytes(ending) - if (enableTicker) systemStreams0.err.write(currentPromptBytes) + if (promptDirty || ending) { + promptDirty = false + updatePromptBytes(ending) + + systemStreams0.err.write(currentPromptBytes) + } } } @@ -302,7 +307,9 @@ private object MultilinePromptLogger { ) } .toList - .sortBy(_.isEmpty) + // Sort alphabetically because the `#nn` prefix is part of the string, and then + // put all empty strings last since those are less important and can be ignored + .sortBy(x => (x.isEmpty, x)) val body = if (body0.count(_.nonEmpty) <= maxHeight) body0.take(maxHeight) From 36ac68716e4b8e3a4cbb8b12ade35f67bc392716 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 15:21:36 +0800 Subject: [PATCH 048/116] simplify termDimensions --- .../src/mill/util/MultilinePromptLogger.scala | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 1873f657e55..42d30806258 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -17,20 +17,16 @@ private[mill] class MultilinePromptLogger( ) extends ColorLogger with AutoCloseable { import MultilinePromptLogger._ - private var termWidth: Option[Int] = None - private var termHeight: Option[Int] = None + private var termDimensions: (Option[Int], Option[Int]) = (None, None) - for((w, h) <- readTerminalDims(terminfoPath)) { - termWidth = w - termHeight = h - } + readTerminalDims(terminfoPath).foreach(termDimensions = _) private val state = new State( titleText, enableTicker, systemStreams0, System.currentTimeMillis(), - () => (termWidth, termHeight) + () => termDimensions ) private val streams = new Streams(enableTicker, systemStreams0, () => state.currentPromptBytes) @@ -41,16 +37,13 @@ private[mill] class MultilinePromptLogger( val promptUpdaterThread = new Thread(() => while (!stopped) { Thread.sleep( - if (termWidth.isDefined) promptUpdateIntervalMillis + if (termDimensions._1.isDefined) promptUpdateIntervalMillis else nonInteractivePromptUpdateIntervalMillis ) if (!paused) { synchronized { - for((w, h) <- readTerminalDims(terminfoPath)) { - termWidth = w - termHeight = h - } + readTerminalDims(terminfoPath).foreach(termDimensions = _) state.refreshPrompt() } } From 306ec44e4032770db56a676dea107f3da2ac8e3c Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 15:22:48 +0800 Subject: [PATCH 049/116] simplify termDimensions --- main/util/src/mill/util/MultilinePromptLogger.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 42d30806258..85adddb7daf 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -36,10 +36,11 @@ private[mill] class MultilinePromptLogger( val promptUpdaterThread = new Thread(() => while (!stopped) { - Thread.sleep( + val promptUpdateInterval = if (termDimensions._1.isDefined) promptUpdateIntervalMillis else nonInteractivePromptUpdateIntervalMillis - ) + + Thread.sleep(promptUpdateInterval) if (!paused) { synchronized { From 1df4fe479db116e9e0c269c08eaae0a452b9f162 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 15:33:21 +0800 Subject: [PATCH 050/116] fix more threads reporting and remove unnecessary blank line at bottom --- main/util/src/mill/util/MultilinePromptLogger.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 85adddb7daf..5ae16fd3df8 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -212,15 +212,17 @@ private object MultilinePromptLogger { titleText, statuses ) - // For the ending prompt, leave the cursor at the bottom rather than scrolling back up. + // For the ending prompt, leave the cursor at the bottom rather than scrolling back left/up. // We do not want further output to overwrite the header as it will no longer re-render - val backUp = if (ending) "" else AnsiNav.up(currentPromptLines.length) + val backUp = + if (ending) "" + else AnsiNav.left(9999) + AnsiNav.up(currentPromptLines.length - 1) val currentPromptStr = if (termWidth0.isEmpty) currentPromptLines.mkString("\n") else { AnsiNav.clearScreen(0) + - currentPromptLines.mkString("\n") + "\n" + + currentPromptLines.mkString("\n") + backUp } @@ -305,9 +307,10 @@ private object MultilinePromptLogger { // put all empty strings last since those are less important and can be ignored .sortBy(x => (x.isEmpty, x)) + val nonEmptyBodyCount = body0.count(_.nonEmpty) val body = - if (body0.count(_.nonEmpty) <= maxHeight) body0.take(maxHeight) - else body0.take(maxHeight - 1) ++ Seq(s"... and ${body0.length - maxHeight + 1} more threads") + if (nonEmptyBodyCount <= maxHeight) body0.take(maxHeight) + else body0.take(maxHeight - 1) ++ Seq(s"... and ${nonEmptyBodyCount - maxHeight + 1} more threads") header :: body } From 8f8ed1ca03d1ef7919c045540653b77a41c9e1bd Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 15:51:16 +0800 Subject: [PATCH 051/116] more fixes --- .../src/mill/util/MultilinePromptLogger.scala | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 5ae16fd3df8..1ebbb4fc344 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -23,7 +23,6 @@ private[mill] class MultilinePromptLogger( private val state = new State( titleText, - enableTicker, systemStreams0, System.currentTimeMillis(), () => termDimensions @@ -108,11 +107,13 @@ private object MultilinePromptLogger { * is even more distracting than changing the contents of a line, so we want to minimize * those occurrences even further. */ - val statusRemovalHideDelayMillis = 250 + val statusRemovalHideDelayMillis = 500 /** * How long to wait before actually removing the blank line left by a removed status - * and reducing the height of the prompt. + * and reducing the height of the prompt. Having the prompt change height is even more + * distracting than having entries in the prompt disappear, so give it a longer timeout + * so it happens less. */ val statusRemovalRemoveDelayMillis = 2000 @@ -174,12 +175,10 @@ private object MultilinePromptLogger { } private class State( titleText: String, - enableTicker: Boolean, systemStreams0: SystemStreams, startTimeMillis: Long, consoleDims: () => (Option[Int], Option[Int]) ) { - var promptDirty = false private val statuses = collection.mutable.SortedMap.empty[Int, Status] private var headerPrefix = "" @@ -212,26 +211,28 @@ private object MultilinePromptLogger { titleText, statuses ) - // For the ending prompt, leave the cursor at the bottom rather than scrolling back left/up. - // We do not want further output to overwrite the header as it will no longer re-render - val backUp = - if (ending) "" - else AnsiNav.left(9999) + AnsiNav.up(currentPromptLines.length - 1) val currentPromptStr = - if (termWidth0.isEmpty) currentPromptLines.mkString("\n") + if (termWidth0.isEmpty) currentPromptLines.mkString("\n") + "\n" else { + // For the ending prompt, leave the cursor at the bottom on a new line rather than + // scrolling back left/up. We do not want further output to overwrite the header as + // it will no longer re-render + val backUp = + if (ending) "\n" + else AnsiNav.left(9999) + AnsiNav.up(currentPromptLines.length - 1) + AnsiNav.clearScreen(0) + currentPromptLines.mkString("\n") + backUp } currentPromptBytes = currentPromptStr.getBytes + } def updateGlobal(s: String): Unit = synchronized { headerPrefix = s - promptDirty = true } def updateCurrent(sOpt: Option[String]): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt @@ -241,16 +242,12 @@ private object MultilinePromptLogger { case None => statuses.get(threadId).foreach(_.removedTimeMillis = now) case Some(s) => statuses(threadId) = Status(now, s, Long.MaxValue) } - promptDirty = true } def refreshPrompt(ending: Boolean = false): Unit = synchronized { - if (promptDirty || ending) { - promptDirty = false - updatePromptBytes(ending) + updatePromptBytes(ending) - systemStreams0.err.write(currentPromptBytes) - } + systemStreams0.err.write(currentPromptBytes) } } From 289c8ddff311950b563e9855b0821dea5e2ddee6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 15:53:50 +0800 Subject: [PATCH 052/116] fix+format --- .../src/mill/util/MultilinePromptLogger.scala | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 1ebbb4fc344..1b1c7862658 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -85,6 +85,7 @@ private object MultilinePromptLogger { private val defaultTermWidth = 119 private val defaultTermHeight = 50 + /** * How often to update the multiline status prompt on the terminal. * Too frequent is bad because it causes a lot of visual noise, @@ -121,9 +122,11 @@ private object MultilinePromptLogger { private val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes - private class Streams(enableTicker: Boolean, - systemStreams0: SystemStreams, - currentPromptBytes: () => Array[Byte]){ + private class Streams( + enableTicker: Boolean, + systemStreams0: SystemStreams, + currentPromptBytes: () => Array[Byte] + ) { // We force both stdout and stderr streams into a single `Piped*Stream` pair via // `ProxyStream`, as we need to preserve the ordering of writes to each individual @@ -141,8 +144,9 @@ private object MultilinePromptLogger { systemStreams0.in ) - val pumper = new ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err){ - object PumperState extends Enumeration{ + val pumper: pumper = new pumper + class pumper extends ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err) { + object PumperState extends Enumeration { val init, prompt, cleared = Value } var pumperState = PumperState.init @@ -151,7 +155,7 @@ private object MultilinePromptLogger { // and there is no more stuff to print. This helps us printing the prompt on // every small write when most such prompts will get immediately over-written // by subsequent writes - if (enableTicker && src.available() == 0){ + if (enableTicker && src.available() == 0) { systemStreams0.err.write(currentPromptBytes()) pumperState = PumperState.prompt } @@ -168,7 +172,7 @@ private object MultilinePromptLogger { val pumperThread = new Thread(pumper) pumperThread.start() - def close() = { + def close(): Unit = { pipeIn.close() pipeOut.close() } @@ -256,7 +260,7 @@ private object MultilinePromptLogger { case n => s"${n}s" } - def readTerminalDims(terminfoPath: os.Path) = { + def readTerminalDims(terminfoPath: os.Path): Option[(Option[Int], Option[Int])] = { try { val s"$termWidth0 $termHeight0" = os.read(terminfoPath) Some( @@ -271,7 +275,7 @@ private object MultilinePromptLogger { } ) ) - }catch{case e => None} + } catch { case e => None } } def renderPrompt( @@ -307,7 +311,9 @@ private object MultilinePromptLogger { val nonEmptyBodyCount = body0.count(_.nonEmpty) val body = if (nonEmptyBodyCount <= maxHeight) body0.take(maxHeight) - else body0.take(maxHeight - 1) ++ Seq(s"... and ${nonEmptyBodyCount - maxHeight + 1} more threads") + else body0.take(maxHeight - 1) ++ Seq( + s"... and ${nonEmptyBodyCount - maxHeight + 1} more threads" + ) header :: body } From b40bba83c6bbe1ada5e3ec6be9810e16050a11ee Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 16:08:33 +0800 Subject: [PATCH 053/116] improve non-interactive prompting --- .../src/mill/util/MultilinePromptLogger.scala | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 1b1c7862658..90d4a5dab69 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -28,7 +28,12 @@ private[mill] class MultilinePromptLogger( () => termDimensions ) - private val streams = new Streams(enableTicker, systemStreams0, () => state.currentPromptBytes) + private val streams = new Streams( + enableTicker, + systemStreams0, + () => state.currentPromptBytes, + interactive = () => termDimensions._1.nonEmpty + ) @volatile var stopped = false @volatile var paused = false @@ -98,9 +103,10 @@ private object MultilinePromptLogger { * e.g. background job logs or piped to a log file. Much less frequent than the * interactive scenario because we cannot rely on ANSI codes to over-write the * previous prompt, so we have to be a lot more conservative to avoid spamming - * the logs + * the logs, but we still want to print it occasionally so people can debug stuck + * background or CI jobs and see what tasks it is running when stuck */ - private val nonInteractivePromptUpdateIntervalMillis = 10000 + private val nonInteractivePromptUpdateIntervalMillis = 60000 /** * Add some extra latency delay to the process of removing an entry from the status @@ -125,7 +131,8 @@ private object MultilinePromptLogger { private class Streams( enableTicker: Boolean, systemStreams0: SystemStreams, - currentPromptBytes: () => Array[Byte] + currentPromptBytes: () => Array[Byte], + interactive: () => Boolean ) { // We force both stdout and stderr streams into a single `Piped*Stream` pair via @@ -156,7 +163,7 @@ private object MultilinePromptLogger { // every small write when most such prompts will get immediately over-written // by subsequent writes if (enableTicker && src.available() == 0) { - systemStreams0.err.write(currentPromptBytes()) + if (interactive()) systemStreams0.err.write(currentPromptBytes()) pumperState = PumperState.prompt } } @@ -165,7 +172,7 @@ private object MultilinePromptLogger { // Before any write, make sure we clear the terminal of any prompt that was // written earlier and not yet cleared, so the following output can be written // to a clean section of the terminal - if (pumperState != PumperState.cleared) systemStreams0.err.write(clearScreenToEndBytes) + if (interactive() && pumperState != PumperState.cleared) systemStreams0.err.write(clearScreenToEndBytes) pumperState = PumperState.cleared } } @@ -213,7 +220,8 @@ private object MultilinePromptLogger { startTimeMillis, headerPrefix, titleText, - statuses + statuses, + interactive = consoleDims()._1.nonEmpty ) val currentPromptStr = @@ -285,7 +293,8 @@ private object MultilinePromptLogger { startTimeMillis: Long, headerPrefix: String, titleText: String, - statuses: collection.SortedMap[Int, Status] + statuses: collection.SortedMap[Int, Status], + interactive: Boolean ): List[String] = { // -1 to leave a bit of buffer val maxWidth = consoleWidth - 1 @@ -295,14 +304,18 @@ private object MultilinePromptLogger { val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) val body0 = statuses - .collect { - case (threadId, status) => + .map { + case (threadId, status) => if (now - status.removedTimeMillis > statusRemovalHideDelayMillis) "" else splitShorten( status.text + " " + renderSeconds(now - status.startTimeMillis), maxWidth ) } + // For background or CI jobs,we do not need to preserve the height of the prompt + // between renderings, since consecutive prompts do not appear at the same place + // in the log file. Thus we can aggressively remove all blank spacer lines + .filter(_.nonEmpty || interactive) .toList // Sort alphabetically because the `#nn` prefix is part of the string, and then // put all empty strings last since those are less important and can be ignored @@ -315,7 +328,12 @@ private object MultilinePromptLogger { s"... and ${nonEmptyBodyCount - maxHeight + 1} more threads" ) - header :: body + // For background or CI jobs, the prompt won't be at the bottom of the terminal but + // will instead be in the middle of a big log file with logs above and below, so we + // need some kind of footer to tell the reader when the prompt ends and logs begin + val footer = Option.when(!interactive)("=" * maxWidth).toList + + header :: body ::: footer } def renderHeader( From 1e0110fe611aeb67cd5850a85f3e296019f992c0 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 16:17:11 +0800 Subject: [PATCH 054/116] . --- .../src/mill/util/MultilinePromptLogger.scala | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 90d4a5dab69..472cc0705a5 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -172,7 +172,8 @@ private object MultilinePromptLogger { // Before any write, make sure we clear the terminal of any prompt that was // written earlier and not yet cleared, so the following output can be written // to a clean section of the terminal - if (interactive() && pumperState != PumperState.cleared) systemStreams0.err.write(clearScreenToEndBytes) + if (interactive() && pumperState != PumperState.cleared) + systemStreams0.err.write(clearScreenToEndBytes) pumperState = PumperState.cleared } } @@ -190,6 +191,7 @@ private object MultilinePromptLogger { startTimeMillis: Long, consoleDims: () => (Option[Int], Option[Int]) ) { + var lastRenderedPromptHash = 0 private val statuses = collection.mutable.SortedMap.empty[Int, Status] private var headerPrefix = "" @@ -257,9 +259,16 @@ private object MultilinePromptLogger { } def refreshPrompt(ending: Boolean = false): Unit = synchronized { - updatePromptBytes(ending) - systemStreams0.err.write(currentPromptBytes) + // For non-interactive jobs, we only want to print the new prompt if the contents + // differs from the previous prompt, since the prompts do not overwrite each other + // in log files and printing large numbers of identical prompts is spammy and useless + lazy val statusesHashCode = statuses.hashCode + if (consoleDims()._1.nonEmpty || statusesHashCode != lastRenderedPromptHash) { + lastRenderedPromptHash = statusesHashCode + updatePromptBytes(ending) + systemStreams0.err.write(currentPromptBytes) + } } } @@ -305,14 +314,14 @@ private object MultilinePromptLogger { val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) val body0 = statuses .map { - case (threadId, status) => + case (threadId, status) => if (now - status.removedTimeMillis > statusRemovalHideDelayMillis) "" else splitShorten( status.text + " " + renderSeconds(now - status.startTimeMillis), maxWidth ) } - // For background or CI jobs,we do not need to preserve the height of the prompt + // For non-interactive jobs, we do not need to preserve the height of the prompt // between renderings, since consecutive prompts do not appear at the same place // in the log file. Thus we can aggressively remove all blank spacer lines .filter(_.nonEmpty || interactive) @@ -328,7 +337,7 @@ private object MultilinePromptLogger { s"... and ${nonEmptyBodyCount - maxHeight + 1} more threads" ) - // For background or CI jobs, the prompt won't be at the bottom of the terminal but + // For non-interactive jobs, the prompt won't be at the bottom of the terminal but // will instead be in the middle of a big log file with logs above and below, so we // need some kind of footer to tell the reader when the prompt ends and logs begin val footer = Option.when(!interactive)("=" * maxWidth).toList From 1573a9641a72c2c327ca663c4a842e51e3fc3673 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 20 Sep 2024 16:23:07 +0800 Subject: [PATCH 055/116] tweaks --- main/util/src/mill/util/MultilinePromptLogger.scala | 2 +- runner/client/src/mill/runner/client/MillProcessLauncher.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 472cc0705a5..f682cd29243 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -78,7 +78,7 @@ private[mill] class MultilinePromptLogger( override def rawOutputStream: PrintStream = systemStreams0.out override def close(): Unit = { - state.refreshPrompt(ending = true) + if (enableTicker) state.refreshPrompt(ending = true) streams.close() stopped = true } diff --git a/runner/client/src/mill/runner/client/MillProcessLauncher.java b/runner/client/src/mill/runner/client/MillProcessLauncher.java index f0e2fafe27b..d4459898385 100644 --- a/runner/client/src/mill/runner/client/MillProcessLauncher.java +++ b/runner/client/src/mill/runner/client/MillProcessLauncher.java @@ -71,7 +71,7 @@ static Process configureRunMillProcess( ProcessBuilder builder, Path serverDir ) throws Exception { - Terminal term = TerminalBuilder.terminal(); + Terminal term = TerminalBuilder.builder().dumb(true).build(); Path sandbox = serverDir.resolve(ServerFiles.sandbox); Files.createDirectories(sandbox); Files.write( From 3e51dabef975f67862224bf6948dbec8607eac56 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 05:14:14 +0800 Subject: [PATCH 056/116] try and fix exit code --- main/client/src/mill/main/client/ProxyStream.java | 14 +++++++++++--- .../src/mill/main/client/ServerLauncher.java | 2 +- .../test/src/mill/main/client/ClientTests.java | 3 ++- .../src/mill/main/client/ProxyStreamTests.java | 3 ++- .../util/src/mill/util/MultilinePromptLogger.scala | 8 +++++--- readme.adoc | 2 +- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/main/client/src/mill/main/client/ProxyStream.java b/main/client/src/mill/main/client/ProxyStream.java index 75677fd2a72..643577e31f5 100644 --- a/main/client/src/mill/main/client/ProxyStream.java +++ b/main/client/src/mill/main/client/ProxyStream.java @@ -102,12 +102,15 @@ public static class Pumper implements Runnable{ private InputStream src; private OutputStream destOut; private OutputStream destErr; + private boolean exitOnClose; public Pumper(InputStream src, OutputStream destOut, - OutputStream destErr){ + OutputStream destErr, + boolean exitOnClose){ this.src = src; this.destOut = destOut; this.destErr = destErr; + this.exitOnClose = exitOnClose; } public void preRead(InputStream src){} @@ -155,8 +158,13 @@ public void run() { // This happens when you run mill shutdown and the server exits gracefully break; } catch (IOException e) { - e.printStackTrace(); - System.exit(1); + // This happens when the upstream pipe was closed + if (exitOnClose){ + e.printStackTrace(); + System.exit(1); + }else{ + break; + } } } diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index ab162de1aee..3e49e8ba54b 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -160,7 +160,7 @@ int run(Path serverDir, boolean setJnaNoSys, Locks locks) throws Exception { InputStream outErr = ioSocket.getInputStream(); OutputStream in = ioSocket.getOutputStream(); - ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr); + ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr, true); InputPumper inPump = new InputPumper(() -> stdin, () -> in, true); Thread outPumperThread = new Thread(outPumper, "outPump"); outPumperThread.setDaemon(true); diff --git a/main/client/test/src/mill/main/client/ClientTests.java b/main/client/test/src/mill/main/client/ClientTests.java index 0466db7e2ec..0d65c3aaa6b 100644 --- a/main/client/test/src/mill/main/client/ClientTests.java +++ b/main/client/test/src/mill/main/client/ClientTests.java @@ -145,7 +145,8 @@ public void proxyInputOutputStreams(byte[] samples1, ByteArrayOutputStream dest2 = new ByteArrayOutputStream(); ProxyStream.Pumper pumper = new ProxyStream.Pumper( new ByteArrayInputStream(bytes), - dest1, dest2 + dest1, dest2, + true ); pumper.run(); assertTrue(Arrays.equals(samples1, dest1.toByteArray())); diff --git a/main/client/test/src/mill/main/client/ProxyStreamTests.java b/main/client/test/src/mill/main/client/ProxyStreamTests.java index 3af807c7a3e..aebf6e637ba 100644 --- a/main/client/test/src/mill/main/client/ProxyStreamTests.java +++ b/main/client/test/src/mill/main/client/ProxyStreamTests.java @@ -72,7 +72,8 @@ public void test0(byte[] outData, byte[] errData, int repeats, boolean gracefulE ProxyStream.Pumper pumper = new ProxyStream.Pumper( pipedInputStream, new TeeOutputStream(destOut, destCombined), - new TeeOutputStream(destErr, destCombined) + new TeeOutputStream(destErr, destCombined), + true ); diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index f682cd29243..561b920f355 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -77,7 +77,7 @@ private[mill] class MultilinePromptLogger( override def rawOutputStream: PrintStream = systemStreams0.out - override def close(): Unit = { + override def close(): Unit = synchronized { if (enableTicker) state.refreshPrompt(ending = true) streams.close() stopped = true @@ -152,7 +152,7 @@ private object MultilinePromptLogger { ) val pumper: pumper = new pumper - class pumper extends ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err) { + class pumper extends ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err, false) { object PumperState extends Enumeration { val init, prompt, cleared = Value } @@ -181,8 +181,10 @@ private object MultilinePromptLogger { pumperThread.start() def close(): Unit = { + pipeOut.flush() pipeIn.close() - pipeOut.close() + // Not sure why this causes problems + // pipeOut.close() } } private class State( diff --git a/readme.adoc b/readme.adoc index 82ee50ed328..33a80b33ec3 100644 --- a/readme.adoc +++ b/readme.adoc @@ -145,7 +145,7 @@ You can reproduce any of the tests manually using `dist.run`, e.g. [source,bash] ---- -./mill dist.launcher && (cd example/basic/1-simple && ../../../out/dist/launcher.dest/run run --text hello) +./mill dist.launcher && (cd example/javalib/basic/1-simple && ../../../../out/dist/launcher.dest/run run --text hello) ---- === Sub-Process Tests *with* Packaging/Publishing From 9ed81b3ad600c4eaa9644278844a24a23c88ae9d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 06:58:18 +0800 Subject: [PATCH 057/116] avoid closing downstream streams in ProxyStreams.Pumper --- .../client/src/mill/main/client/ProxyStream.java | 16 ++++------------ .../src/mill/main/client/ServerLauncher.java | 2 +- .../test/src/mill/main/client/ClientTests.java | 3 +-- .../src/mill/main/client/ProxyStreamTests.java | 3 +-- .../src/mill/util/MultilinePromptLogger.scala | 8 +++----- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/main/client/src/mill/main/client/ProxyStream.java b/main/client/src/mill/main/client/ProxyStream.java index 643577e31f5..9a1cbc472f7 100644 --- a/main/client/src/mill/main/client/ProxyStream.java +++ b/main/client/src/mill/main/client/ProxyStream.java @@ -102,15 +102,12 @@ public static class Pumper implements Runnable{ private InputStream src; private OutputStream destOut; private OutputStream destErr; - private boolean exitOnClose; public Pumper(InputStream src, OutputStream destOut, - OutputStream destErr, - boolean exitOnClose){ + OutputStream destErr){ this.src = src; this.destOut = destOut; this.destErr = destErr; - this.exitOnClose = exitOnClose; } public void preRead(InputStream src){} @@ -159,18 +156,13 @@ public void run() { break; } catch (IOException e) { // This happens when the upstream pipe was closed - if (exitOnClose){ - e.printStackTrace(); - System.exit(1); - }else{ - break; - } + break; } } try { - destOut.close(); - destErr.close(); + destOut.flush(); + destErr.flush(); } catch(IOException e) {} } diff --git a/main/client/src/mill/main/client/ServerLauncher.java b/main/client/src/mill/main/client/ServerLauncher.java index 3e49e8ba54b..ab162de1aee 100644 --- a/main/client/src/mill/main/client/ServerLauncher.java +++ b/main/client/src/mill/main/client/ServerLauncher.java @@ -160,7 +160,7 @@ int run(Path serverDir, boolean setJnaNoSys, Locks locks) throws Exception { InputStream outErr = ioSocket.getInputStream(); OutputStream in = ioSocket.getOutputStream(); - ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr, true); + ProxyStream.Pumper outPumper = new ProxyStream.Pumper(outErr, stdout, stderr); InputPumper inPump = new InputPumper(() -> stdin, () -> in, true); Thread outPumperThread = new Thread(outPumper, "outPump"); outPumperThread.setDaemon(true); diff --git a/main/client/test/src/mill/main/client/ClientTests.java b/main/client/test/src/mill/main/client/ClientTests.java index 0d65c3aaa6b..0466db7e2ec 100644 --- a/main/client/test/src/mill/main/client/ClientTests.java +++ b/main/client/test/src/mill/main/client/ClientTests.java @@ -145,8 +145,7 @@ public void proxyInputOutputStreams(byte[] samples1, ByteArrayOutputStream dest2 = new ByteArrayOutputStream(); ProxyStream.Pumper pumper = new ProxyStream.Pumper( new ByteArrayInputStream(bytes), - dest1, dest2, - true + dest1, dest2 ); pumper.run(); assertTrue(Arrays.equals(samples1, dest1.toByteArray())); diff --git a/main/client/test/src/mill/main/client/ProxyStreamTests.java b/main/client/test/src/mill/main/client/ProxyStreamTests.java index aebf6e637ba..3af807c7a3e 100644 --- a/main/client/test/src/mill/main/client/ProxyStreamTests.java +++ b/main/client/test/src/mill/main/client/ProxyStreamTests.java @@ -72,8 +72,7 @@ public void test0(byte[] outData, byte[] errData, int repeats, boolean gracefulE ProxyStream.Pumper pumper = new ProxyStream.Pumper( pipedInputStream, new TeeOutputStream(destOut, destCombined), - new TeeOutputStream(destErr, destCombined), - true + new TeeOutputStream(destErr, destCombined) ); diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 561b920f355..cc67741b887 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -151,8 +151,7 @@ private object MultilinePromptLogger { systemStreams0.in ) - val pumper: pumper = new pumper - class pumper extends ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err, false) { + object pumper extends ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err) { object PumperState extends Enumeration { val init, prompt, cleared = Value } @@ -181,10 +180,9 @@ private object MultilinePromptLogger { pumperThread.start() def close(): Unit = { - pipeOut.flush() pipeIn.close() - // Not sure why this causes problems - // pipeOut.close() + pipeOut.close() + pumperThread.join() } } private class State( From a3c0fe6447d460f0fe89ebc97600905a69a6e83b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 07:24:09 +0800 Subject: [PATCH 058/116] . --- build.mill | 1 - .../src/mill/main/client/ProxyStream.java | 4 +- .../src/mill/main/client/ServerFiles.java | 2 +- main/eval/src/mill/eval/GroupEvaluator.scala | 1 - .../src/mill/util/MultilinePromptLogger.scala | 4 +- .../util/MultilinePromptLoggerTests.scala | 127 ++++++++++++------ 6 files changed, 92 insertions(+), 47 deletions(-) diff --git a/build.mill b/build.mill index 2024a17ab46..a4039a6288d 100644 --- a/build.mill +++ b/build.mill @@ -20,7 +20,6 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload -//import $packages._ object Settings { val pomOrg = "com.lihaoyi" diff --git a/main/client/src/mill/main/client/ProxyStream.java b/main/client/src/mill/main/client/ProxyStream.java index 9a1cbc472f7..b726140f5e9 100644 --- a/main/client/src/mill/main/client/ProxyStream.java +++ b/main/client/src/mill/main/client/ProxyStream.java @@ -102,9 +102,7 @@ public static class Pumper implements Runnable{ private InputStream src; private OutputStream destOut; private OutputStream destErr; - public Pumper(InputStream src, - OutputStream destOut, - OutputStream destErr){ + public Pumper(InputStream src, OutputStream destOut, OutputStream destErr){ this.src = src; this.destOut = destOut; this.destErr = destErr; diff --git a/main/client/src/mill/main/client/ServerFiles.java b/main/client/src/mill/main/client/ServerFiles.java index d303d22b349..3351fb93902 100644 --- a/main/client/src/mill/main/client/ServerFiles.java +++ b/main/client/src/mill/main/client/ServerFiles.java @@ -69,7 +69,7 @@ public static String pipe(String base) { final public static String stderr = "stderr"; /** - * Terminal information + * Terminal information that we need to propagate from client to server */ final public static String terminfo = "terminfo"; } diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 79591605f27..d8f0f47a8dd 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -321,7 +321,6 @@ private[mill] trait GroupEvaluator { else v.## ) } - } multiLogger.close() diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index cc67741b887..112bdf5c344 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -307,8 +307,8 @@ private object MultilinePromptLogger { ): List[String] = { // -1 to leave a bit of buffer val maxWidth = consoleWidth - 1 - // -2 to account for 1 line header and 1 line `more threads` - val maxHeight = math.max(1, consoleHeight / 3 - 2) + // -1 to account for header + val maxHeight = math.max(1, consoleHeight / 3 - 1) val headerSuffix = renderSeconds(now - startTimeMillis) val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 8d377eda983..7eb6d54ec64 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -122,7 +122,8 @@ object MultilinePromptLoggerTests extends TestSuite { statuses = SortedMap( 0 -> Status(now - 1000, "hello", Long.MaxValue), 1 -> Status(now - 2000, "world", Long.MaxValue) - ) + ), + interactive = true ) val expected = List( " 123/456 =============== __.compile ================ 1337s", @@ -143,21 +144,24 @@ object MultilinePromptLoggerTests extends TestSuite { statuses = SortedMap( 0 -> Status( now - 1000, - "hello1234567890abcefghijklmnopqrstuvwxyz1234567890123456", + "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123", Long.MaxValue ), - 1 -> Status(now - 2000, "world", Long.MaxValue), - 2 -> Status(now - 3000, "i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "hear me moo", Long.MaxValue) - ) + 1 -> Status(now - 2000, "#2 world", Long.MaxValue), + 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue) + ), + interactive = true ) val expected = List( " 123/456 ======== __.compile.abcdefghijklmn ======== 1337s", - "hello1234567890abcefghijklmnopqrstuvwxyz1234567890123456 1s", - "world 2s", - "i am cow 3s", - "hear me moo 4s" + "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", + "#5 i weigh twice as much as you 5s" ) assert(rendered == expected) } @@ -172,21 +176,24 @@ object MultilinePromptLoggerTests extends TestSuite { statuses = SortedMap( 0 -> Status( now - 1000, - "hello1234567890abcefghijklmnopqrstuvwxyz12345678901234567", + "#1 hello1234567890abcefghijklmnopqrstuvwxyz12345678901234", Long.MaxValue ), - 1 -> Status(now - 2000, "world", Long.MaxValue), - 2 -> Status(now - 3000, "i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "i weight twice as much as you", Long.MaxValue) - ) + 1 -> Status(now - 2000, "#2 world", Long.MaxValue), + 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), + 5 -> Status(now - 6000, "#6 and I look good on the barbecue", Long.MaxValue) + ), + interactive = true ) val expected = List( " 123/456 ======= __.compile....efghijklmno ========= 1337s", - "hello1234567890abcefghijklmn...stuvwxyz12345678901234567 1s", - "world 2s", - "i am cow 3s", + "#1 hello1234567890abcefghijk...pqrstuvwxyz12345678901234 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", "... and 2 more threads" ) assert(rendered == expected) @@ -201,19 +208,22 @@ object MultilinePromptLoggerTests extends TestSuite { headerPrefix = "123/456", titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", statuses = SortedMap( - 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), - 1 -> Status(now - 2000, "world", Long.MaxValue), - 2 -> Status(now - 3000, "i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "i weigh twice as much as you", Long.MaxValue), - 5 -> Status(now - 6000, "and i look good on the barbecue", Long.MaxValue) - ) + 0 -> Status(now - 1000, "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + 1 -> Status(now - 2000, "#2 world", Long.MaxValue), + 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), + 5 -> Status(now - 6000, "#6 and i look good on the barbecue", Long.MaxValue), + 6 -> Status(now - 7000, "#7 yoghurt curds cream cheese and butter", Long.MaxValue) + ), + interactive = true ) val expected = List( " 123/456 __.compile....z1234567890 ================ 1337s", - "hello1234567890abcefghijklmn...abcefghijklmnopqrstuvwxyz 1s", - "world 2s", - "i am cow 3s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", "... and 3 more threads" ) assert(rendered == expected) @@ -229,25 +239,64 @@ object MultilinePromptLoggerTests extends TestSuite { titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", statuses = SortedMap( // Not yet removed, should be shown - 0 -> Status(now - 1000, "hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + 0 -> Status(now - 1000, "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(now - 2000, "world", now - statusRemovalHideDelayMillis + 1), - 2 -> Status(now - 3000, "i am cow", now - statusRemovalHideDelayMillis + 1), + 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), + 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(now - 4000, "hear me moo", now - statusRemovalRemoveDelayMillis + 1), - 4 -> Status(now - 5000, "i weigh twice", now - statusRemovalRemoveDelayMillis + 1), - 5 -> Status(now - 6000, "as much as you", now - statusRemovalRemoveDelayMillis + 1) - ) + 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), + 4 -> Status(now - 5000, "#5i weigh twice", now - statusRemovalRemoveDelayMillis + 1), + 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1), + 6 -> Status(now - 7000, "#7 and I look good on the barbecue", now - statusRemovalRemoveDelayMillis + 1) + ), + interactive = true ) + val expected = List( " 123/456 __.compile....z1234567890 ================ 1337s", - "hello1234567890abcefghijklmn...abcefghijklmnopqrstuvwxyz 1s", - "world 2s", - "i am cow 3s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "", "", "" ) + + assert(rendered == expected) + } + + test("nonInteractive") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 23, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", + statuses = SortedMap( + // Not yet removed, should be shown + 0 -> Status(now - 1000, "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown + 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), + 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), + // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not + // yet removed, so rendered as blank lines to prevent terminal jumping around too much + 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), + 4 -> Status(now - 5000, "#5 i weigh twice", now - statusRemovalRemoveDelayMillis + 1), + 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1) + ), + interactive = false + ) + + // Make sure + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "===========================================================" + ) assert(rendered == expected) } } From 35907a3b6104aa53002f8d37d75e197ae4639e14 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 07:28:19 +0800 Subject: [PATCH 059/116] fixes --- main/util/test/src/mill/util/MultilinePromptLoggerTests.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 7eb6d54ec64..c187cc7fea8 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -289,7 +289,8 @@ object MultilinePromptLoggerTests extends TestSuite { interactive = false ) - // Make sure + // Make sure the non-interactive prompt does not show the blank lines, + // and it contains a footer line to mark the end of the prompt in logs val expected = List( " 123/456 __.compile....z1234567890 ================ 1337s", "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", From b5a70837cc0cfa0060df54571dc31799a18084ed Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 07:30:16 +0800 Subject: [PATCH 060/116] fixes --- main/eval/src/mill/eval/GroupEvaluator.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index d8f0f47a8dd..a59d02c40bd 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -243,7 +243,6 @@ private[mill] trait GroupEvaluator { } val tickerPrefix = maybeTargetLabel.collect { -// case targetLabel if logRun && enableTicker => s"$counterMsg $targetLabel " case targetLabel if logRun && enableTicker => targetLabel } def withTicker[T](s: Option[String])(t: => T): T = s match { From 315658640d4fef10556b17e9dd97c68885799864 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 11:25:15 +0800 Subject: [PATCH 061/116] wip --- main/package.mill | 10 ++-------- main/util/src/mill/util/MultilinePromptLogger.scala | 1 + 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/main/package.mill b/main/package.mill index 1453e882b25..7844a161a9a 100644 --- a/main/package.mill +++ b/main/package.mill @@ -171,16 +171,10 @@ object `package` extends RootModule with build.MillStableScalaModule with BuildI object client extends build.MillPublishJavaModule with BuildInfo { def buildInfoPackageName = "mill.main.client" def buildInfoMembers = Seq(BuildInfo.Value("millVersion", build.millVersion(), "Mill version.")) - def ivyDeps = Agg( - build.Deps.junixsocket, - build.Deps.jline - ) + def ivyDeps = Agg(build.Deps.junixsocket, build.Deps.jline) object test extends JavaModuleTests with TestModule.Junit4 { - def ivyDeps = Agg( - build.Deps.junitInterface, - build.Deps.commonsIO - ) + def ivyDeps = Agg(build.Deps.junitInterface, build.Deps.commonsIO) } } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 112bdf5c344..f22ca45a808 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -246,6 +246,7 @@ private object MultilinePromptLogger { } def updateGlobal(s: String): Unit = synchronized { + statuses.clear() headerPrefix = s } def updateCurrent(sOpt: Option[String]): Unit = synchronized { From 2eab6a2761b428c25596c084b488072fdf030b13 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 13:45:45 +0800 Subject: [PATCH 062/116] add global override for systemstreams to prevent threading problems --- main/api/src/mill/api/SystemStreams.scala | 101 +++++++++++++++----- runner/src/mill/runner/MillMain.scala | 2 +- runner/src/mill/runner/MillServerMain.scala | 2 +- 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/main/api/src/mill/api/SystemStreams.scala b/main/api/src/mill/api/SystemStreams.scala index 5b687d18191..5a183b95ec4 100644 --- a/main/api/src/mill/api/SystemStreams.scala +++ b/main/api/src/mill/api/SystemStreams.scala @@ -3,6 +3,9 @@ package mill.api import java.io.{InputStream, OutputStream, PrintStream} import mill.main.client.InputPumper +import java.util.concurrent.atomic.AtomicInteger +import scala.util.DynamicVariable + /** * Represents a set of streams that look similar to those provided by the * operating system. These may internally be proxied/redirected/processed, but @@ -62,30 +65,24 @@ object SystemStreams { Some(new InputPumper(() => processOut.wrapped, () => dest, false, () => true)) } def withStreams[T](systemStreams: SystemStreams)(t: => T): T = { - val in = System.in - val out = System.out - val err = System.err - try { - // If we are setting a stream back to its original value, make sure we reset - // `os.Inherit` to `os.InheritRaw` for that stream. This direct inheritance - // ensures that interactive applications involving console IO work, as the - // presence of a `PumpedProcess` would cause most interactive CLIs (e.g. - // scala console, REPL, etc.) to misbehave - val inheritIn = - if (systemStreams.in eq original.in) os.InheritRaw - else new PumpedProcessInput - - val inheritOut = - if (systemStreams.out eq original.out) os.InheritRaw - else new PumpedProcessOutput(systemStreams.out) - - val inheritErr = - if (systemStreams.err eq original.err) os.InheritRaw - else new PumpedProcessOutput(systemStreams.err) - - System.setIn(systemStreams.in) - System.setOut(systemStreams.out) - System.setErr(systemStreams.err) + // If we are setting a stream back to its original value, make sure we reset + // `os.Inherit` to `os.InheritRaw` for that stream. This direct inheritance + // ensures that interactive applications involving console IO work, as the + // presence of a `PumpedProcess` would cause most interactive CLIs (e.g. + // scala console, REPL, etc.) to misbehave + val inheritIn = + if (systemStreams.in eq original.in) os.InheritRaw + else new PumpedProcessInput + + val inheritOut = + if (systemStreams.out eq original.out) os.InheritRaw + else new PumpedProcessOutput(systemStreams.out) + + val inheritErr = + if (systemStreams.err eq original.err) os.InheritRaw + else new PumpedProcessOutput(systemStreams.err) + + ThreadLocalStreams.current.withValue(systemStreams) { Console.withIn(systemStreams.in) { Console.withOut(systemStreams.out) { Console.withErr(systemStreams.err) { @@ -99,10 +96,66 @@ object SystemStreams { } } } + } + } + + + /** + * Manages the global override of `System.{in,out,err}`. Overrides of those streams are + * global, so we cannot just override them per-use-site in a multithreaded environment + * because different threads may interleave and stomp over each other's over-writes. + * Instead, we over-write them globally with a set of streams that does nothing but + * forward to the per-thread [[ThreadLocalStreams.current]] streams, allowing callers + * to each reach their own thread-local streams without clashing across multiple threads + */ + def withTopLevelSystemStreamProxy[T](t: => T): T = { + val in = System.in + val out = System.out + val err = System.err + + try { + System.setIn(ThreadLocalStreams.In) + System.setOut(ThreadLocalStreams.Out) + System.setErr(ThreadLocalStreams.Err) + t } finally { System.setErr(err) System.setOut(out) System.setIn(in) } } + + private object ThreadLocalStreams{ + val current = new DynamicVariable(original) + + object Out extends PrintStream(new ProxyOutputStream{ def delegate() = current.value.out}) + object Err extends PrintStream(new ProxyOutputStream{ def delegate() = current.value.err }) + object In extends ProxyInputStream{ def delegate() = current.value.in} + + abstract class ProxyOutputStream extends OutputStream{ + def delegate(): OutputStream + override def write(b: Array[Byte], off: Int, len: Int): Unit = delegate().write(b, off, len) + override def write(b: Array[Byte]): Unit = delegate().write(b) + def write(b: Int): Unit = delegate().write(b) + override def flush(): Unit = delegate().flush() + override def close(): Unit = delegate().close() + } + abstract class ProxyInputStream extends InputStream{ + def delegate(): InputStream + override def read(): Int = delegate().read() + override def read(b: Array[Byte], off: Int, len: Int): Int = delegate().read(b, off, len) + override def read(b: Array[Byte]): Int = delegate().read(b) + override def readNBytes(b: Array[Byte], off: Int, len: Int): Int = delegate().readNBytes(b, off, len) + override def readNBytes(len: Int): Array[Byte] = delegate().readNBytes(len) + override def readAllBytes(): Array[Byte] = delegate().readAllBytes() + override def mark(readlimit: Int): Unit = delegate().mark(readlimit) + override def markSupported(): Boolean = delegate().markSupported() + override def available(): Int = delegate().available() + override def reset(): Unit = delegate().reset() + override def skip(n: Long): Long = delegate().skip(n) + override def skipNBytes(n: Long): Unit = delegate().skipNBytes(n) + override def close(): Unit = delegate().close() + override def transferTo(out: OutputStream): Long = delegate().transferTo(out) + } + } } diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 10ded7ac009..20653f37bd7 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -34,7 +34,7 @@ object MillMain { (false, onError) } - def main(args: Array[String]): Unit = { + def main(args: Array[String]): Unit = SystemStreams.withTopLevelSystemStreamProxy { val initialSystemStreams = new SystemStreams(System.out, System.err, System.in) // setup streams val (runnerStreams, cleanupStreams, bspLog) = diff --git a/runner/src/mill/runner/MillServerMain.scala b/runner/src/mill/runner/MillServerMain.scala index a6bfa48dad2..917a2f3b619 100644 --- a/runner/src/mill/runner/MillServerMain.scala +++ b/runner/src/mill/runner/MillServerMain.scala @@ -8,7 +8,7 @@ import mill.main.client.lock.Locks import scala.util.Try object MillServerMain { - def main(args0: Array[String]): Unit = { + def main(args0: Array[String]): Unit = SystemStreams.withTopLevelSystemStreamProxy { // Disable SIGINT interrupt signal in the Mill server. // // This gets passed through from the client to server whenever the user From 7199e057791cd62295202c7dbd01bb97f8e81d02 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 14:00:31 +0800 Subject: [PATCH 063/116] fix stream original comparison --- main/api/src/mill/api/SystemStreams.scala | 13 +++++++------ runner/src/mill/runner/MillMain.scala | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/main/api/src/mill/api/SystemStreams.scala b/main/api/src/mill/api/SystemStreams.scala index 5a183b95ec4..eb75cadbbcb 100644 --- a/main/api/src/mill/api/SystemStreams.scala +++ b/main/api/src/mill/api/SystemStreams.scala @@ -3,7 +3,6 @@ package mill.api import java.io.{InputStream, OutputStream, PrintStream} import mill.main.client.InputPumper -import java.util.concurrent.atomic.AtomicInteger import scala.util.DynamicVariable /** @@ -31,11 +30,13 @@ object SystemStreams { * stdout/stderr/stdin. */ def isOriginal(): Boolean = { - (System.out eq original.out) && - (System.err eq original.err) && - (System.in eq original.in) && - (Console.out eq original.out) && - (Console.err eq original.err) + (Console.out eq original.out) && (Console.err eq original.err) + // We do not check System.* for equality because they are always overridden by + // `ThreadLocalStreams` + // (System.out eq original.out) && + // (System.err eq original.err) && + // (System.in eq original.in) && + // We do not check `Console.in` for equality, because `Console.withIn` always wraps // `Console.in` in a `new BufferedReader` each time, and so it is impossible to check diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 20653f37bd7..a4c86bc3c23 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -250,7 +250,6 @@ object MillMain { ).evaluate() } ) - bspContext.foreach { ctx => repeatForBsp = BspContext.bspServerHandle.lastResult == Some( @@ -273,6 +272,7 @@ object MillMain { loopRes } } + if (config.ringBell.value) { if (success) println("\u0007") else { From 1a05e1c928dab9acf28c5e974b460206e5a81f36 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 15:03:36 +0800 Subject: [PATCH 064/116] fix missing trailing output --- main/client/src/mill/main/client/ProxyStream.java | 1 - main/util/src/mill/util/MultilinePromptLogger.scala | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/main/client/src/mill/main/client/ProxyStream.java b/main/client/src/mill/main/client/ProxyStream.java index b726140f5e9..3f3655a0196 100644 --- a/main/client/src/mill/main/client/ProxyStream.java +++ b/main/client/src/mill/main/client/ProxyStream.java @@ -110,7 +110,6 @@ public Pumper(InputStream src, OutputStream destOut, OutputStream destErr){ public void preRead(InputStream src){} - public void preWrite(){} public void run() { diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index f22ca45a808..a7caa9d8471 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -176,11 +176,14 @@ private object MultilinePromptLogger { pumperState = PumperState.cleared } } + val pumperThread = new Thread(pumper) pumperThread.start() def close(): Unit = { - pipeIn.close() + // Close the write side of the pipe first but do not close the read side, so + // the `pumperThread` can continue reading remaining text in the pipe buffer + // before terminating on its own pipeOut.close() pumperThread.join() } From 7ccdea390ae8fd56b804c158c92c946a50db9ea1 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 15:27:43 +0800 Subject: [PATCH 065/116] fix --- main/api/src/mill/api/SystemStreams.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main/api/src/mill/api/SystemStreams.scala b/main/api/src/mill/api/SystemStreams.scala index eb75cadbbcb..9b2753f1f9d 100644 --- a/main/api/src/mill/api/SystemStreams.scala +++ b/main/api/src/mill/api/SystemStreams.scala @@ -154,7 +154,8 @@ object SystemStreams { override def available(): Int = delegate().available() override def reset(): Unit = delegate().reset() override def skip(n: Long): Long = delegate().skip(n) - override def skipNBytes(n: Long): Unit = delegate().skipNBytes(n) + // Not present in some versions of Java + // override def skipNBytes(n: Long): Unit = delegate().skipNBytes(n) override def close(): Unit = delegate().close() override def transferTo(out: OutputStream): Long = delegate().transferTo(out) } From e8420a0e4e44e227bb93d869bdcc8a2cc01339ab Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 21 Sep 2024 19:00:38 +0800 Subject: [PATCH 066/116] avoid infinite recursion with system.out --- main/api/src/mill/api/SystemStreams.scala | 17 +++++++++++++---- main/test/src/mill/UTestFramework.scala | 3 +++ runner/src/mill/runner/MillMain.scala | 2 +- testkit/src/mill/testkit/UnitTester.scala | 4 ++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/main/api/src/mill/api/SystemStreams.scala b/main/api/src/mill/api/SystemStreams.scala index 9b2753f1f9d..222d6e94211 100644 --- a/main/api/src/mill/api/SystemStreams.scala +++ b/main/api/src/mill/api/SystemStreams.scala @@ -115,9 +115,7 @@ object SystemStreams { val err = System.err try { - System.setIn(ThreadLocalStreams.In) - System.setOut(ThreadLocalStreams.Out) - System.setErr(ThreadLocalStreams.Err) + setTopLevelSystemStreamProxy() t } finally { System.setErr(err) @@ -125,8 +123,19 @@ object SystemStreams { System.setIn(in) } } + def setTopLevelSystemStreamProxy() = { + // Make sure to initialize `Console` to cache references to the original + // `System.{in,out,err}` streams before we redirect them + Console.out + Console.err + Console.in + System.setIn(ThreadLocalStreams.In) + System.setOut(ThreadLocalStreams.Out) + System.setErr(ThreadLocalStreams.Err) + } + - private object ThreadLocalStreams{ + private[mill] object ThreadLocalStreams{ val current = new DynamicVariable(original) object Out extends PrintStream(new ProxyOutputStream{ def delegate() = current.value.out}) diff --git a/main/test/src/mill/UTestFramework.scala b/main/test/src/mill/UTestFramework.scala index 7a5f7ad7fc7..491625df64b 100644 --- a/main/test/src/mill/UTestFramework.scala +++ b/main/test/src/mill/UTestFramework.scala @@ -1,10 +1,13 @@ package mill +import mill.api.SystemStreams + class UTestFramework extends utest.runner.Framework { override def exceptionStackFrameHighlighter(s: StackTraceElement): Boolean = { s.getClassName.startsWith("mill.") } override def setup(): Unit = { + SystemStreams.setTopLevelSystemStreamProxy() // force initialization os.remove.all(os.pwd / "target/workspace") } diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index a4c86bc3c23..f688af40baf 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -35,7 +35,7 @@ object MillMain { } def main(args: Array[String]): Unit = SystemStreams.withTopLevelSystemStreamProxy { - val initialSystemStreams = new SystemStreams(System.out, System.err, System.in) + val initialSystemStreams = new SystemStreams(Console.out, Console.err, System.in) // setup streams val (runnerStreams, cleanupStreams, bspLog) = if (args.headOption == Option("--bsp")) { diff --git a/testkit/src/mill/testkit/UnitTester.scala b/testkit/src/mill/testkit/UnitTester.scala index 1d921c67477..92da0995beb 100644 --- a/testkit/src/mill/testkit/UnitTester.scala +++ b/testkit/src/mill/testkit/UnitTester.scala @@ -18,8 +18,8 @@ object UnitTester { sourceRoot: os.Path, failFast: Boolean = false, threads: Option[Int] = Some(1), - outStream: PrintStream = System.out, - errStream: PrintStream = System.err, + outStream: PrintStream = Console.out, + errStream: PrintStream = Console.err, inStream: InputStream = DummyInputStream, debugEnabled: Boolean = false, env: Map[String, String] = Evaluator.defaultEnv, From e3de3288a37d1ebf10529c1c6a830fbdda701433 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 15:27:54 +0800 Subject: [PATCH 067/116] swap out Piped*Stream with our on PipeStreams --- .../client/src/mill/main/client/DebugLog.java | 22 +++ .../mill/util/LinePrefixOutputStream.scala | 1 + .../src/mill/util/MultilinePromptLogger.scala | 17 +- main/util/src/mill/util/PipeStreams.scala | 175 ++++++++++++++++++ main/util/src/mill/util/PrefixLogger.scala | 12 +- .../test/src/mill/util/PipeStreamsTests.scala | 131 +++++++++++++ 6 files changed, 343 insertions(+), 15 deletions(-) create mode 100644 main/client/src/mill/main/client/DebugLog.java create mode 100644 main/util/src/mill/util/PipeStreams.scala create mode 100644 main/util/test/src/mill/util/PipeStreamsTests.scala diff --git a/main/client/src/mill/main/client/DebugLog.java b/main/client/src/mill/main/client/DebugLog.java new file mode 100644 index 00000000000..cc7886854d1 --- /dev/null +++ b/main/client/src/mill/main/client/DebugLog.java @@ -0,0 +1,22 @@ +package mill.main.client; +import java.io.IOException; +import java.nio.file.*; + +/** + * Used to add `println`s in scenarios where you can't figure out where on earth + * your stdout/stderr/logs are going and so we just dump them in a file in your + * home folder so you can find them + */ +public class DebugLog{ + synchronized public static void println(String s){ + try { + Files.writeString( + Paths.get(System.getProperty("user.home"), "mill-debug-log.txt"), + s + "\n", + StandardOpenOption.APPEND + ); + }catch (IOException e){ + throw new RuntimeException(e); + } + } +} diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 87a5a613190..34b2d06015e 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -45,4 +45,5 @@ class LinePrefixOutputStream( buffer.reset() super.flush() } + override def close() = out.close() } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index a7caa9d8471..748242c26fb 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -1,7 +1,7 @@ package mill.util import mill.api.SystemStreams -import mill.main.client.ProxyStream +import mill.main.client.{DebugLog, ProxyStream} import java.io._ @@ -139,19 +139,16 @@ private object MultilinePromptLogger { // `ProxyStream`, as we need to preserve the ordering of writes to each individual // stream, and also need to know when *both* streams are quiescent so that we can // print the prompt at the bottom - val pipeIn = new PipedInputStream() - val pipeOut = new PipedOutputStream() - pipeIn.available() - pipeIn.connect(pipeOut) - val proxyOut = new ProxyStream.Output(pipeOut, ProxyStream.OUT) - val proxyErr = new ProxyStream.Output(pipeOut, ProxyStream.ERR) + val pipe = new PipeStreams() + val proxyOut = new ProxyStream.Output(pipe.output, ProxyStream.OUT) + val proxyErr = new ProxyStream.Output(pipe.output, ProxyStream.ERR) val systemStreams = new SystemStreams( new PrintStream(proxyOut), new PrintStream(proxyErr), systemStreams0.in ) - object pumper extends ProxyStream.Pumper(pipeIn, systemStreams0.out, systemStreams0.err) { + object pumper extends ProxyStream.Pumper(pipe.input, systemStreams0.out, systemStreams0.err) { object PumperState extends Enumeration { val init, prompt, cleared = Value } @@ -180,11 +177,13 @@ private object MultilinePromptLogger { val pumperThread = new Thread(pumper) pumperThread.start() + Thread.sleep(100) def close(): Unit = { // Close the write side of the pipe first but do not close the read side, so // the `pumperThread` can continue reading remaining text in the pipe buffer // before terminating on its own - pipeOut.close() + ProxyStream.sendEnd(pipe.output) + pipe.output.close() pumperThread.join() } } diff --git a/main/util/src/mill/util/PipeStreams.scala b/main/util/src/mill/util/PipeStreams.scala new file mode 100644 index 00000000000..4168cb09981 --- /dev/null +++ b/main/util/src/mill/util/PipeStreams.scala @@ -0,0 +1,175 @@ +package mill.util + +import java.io.{IOException, InputStream, OutputStream} + +/** + * Fork of `java.io.Piped{Input,Output}Stream` that allows reads and writes. + * to come from separate threads. Really the same logic just with the assertions + * on thread liveness removed, added some synchronization to ensure atomic + * writes, and somewhat cleaned up as a single object rather than two loose + * objects you have to connect together. + */ +class PipeStreams(val bufferSize: Int = 1024) { pipe => + + private var closedByWriter = false + @volatile private var closedByReader = false + private val buffer: Array[Byte] = new Array[Byte](bufferSize) + private var in: Int = -1 + private var out = 0 + + val input: InputStream = Input + private object Input extends InputStream { + + private[PipeStreams] def receive(b: Int): Unit = synchronized{ + checkStateForReceive() + if (in == out) awaitSpace() + if (in < 0) { + in = 0 + out = 0 + } + buffer(in) = (b & 0xFF).toByte + in += 1 + if (in >= buffer.length) in = 0 + } + + + private[PipeStreams] def receive(b: Array[Byte], off0: Int, len: Int): Unit = synchronized{ + var off = off0 + checkStateForReceive() + var bytesToTransfer = len + while (bytesToTransfer > 0) { + if (in == out) awaitSpace() + var nextTransferAmount = 0 + if (out < in) nextTransferAmount = buffer.length - in + else if (in < out) { + if (in == -1) { + in = 0 + out = 0 + nextTransferAmount = buffer.length - in + } + else { + nextTransferAmount = out - in + } + } + if (nextTransferAmount > bytesToTransfer) nextTransferAmount = bytesToTransfer + assert(nextTransferAmount > 0) + System.arraycopy(b, off, buffer, in, nextTransferAmount) + bytesToTransfer -= nextTransferAmount + off += nextTransferAmount + in += nextTransferAmount + if (in >= buffer.length) in = 0 + } + } + + + private def checkStateForReceive(): Unit = { + if (closedByWriter || closedByReader) throw new IOException("Pipe closed") + } + + + private def awaitSpace(): Unit = { + while (in == out) { + checkStateForReceive() + /* full: kick any waiting readers */ + notifyWaitCatch() + } + } + + private[util] def receivedLast(): Unit = synchronized { + closedByWriter = true + notifyAll() + } + + private def notifyWaitCatch() = { + notifyAll() + try wait(1000) + catch { case ex: InterruptedException => + throw new java.io.InterruptedIOException + } + } + + override def read(): Int = synchronized { + if (closedByReader) throw new IOException("Pipe closed") + while (in < 0) { + if (closedByWriter) return -1 /* closed by writer, return EOF */ + + /* might be a writer waiting */ + notifyWaitCatch() + } + + val ret = buffer(out) & 0xFF + out += 1 + if (out >= buffer.length) out = 0 + if (in == out) in = -1 /* now empty */ + + ret + } + + override def read(b: Array[Byte], off: Int, len0: Int): Int = synchronized{ + var len = len0 + if (b == null) throw new NullPointerException + else if (off < 0 || len < 0 || len > b.length - off) throw new IndexOutOfBoundsException + else if (len == 0) return 0 + /* possibly wait on the first character */ + val c = read + if (c < 0) return -1 + b(off) = c.toByte + var rlen = 1 + while ((in >= 0) && (len > 1)) { + var available = 0 + if (in > out) available = Math.min(buffer.length - out, in - out) + else available = buffer.length - out + // A byte is read beforehand outside the loop + + if (available > (len - 1)) available = len - 1 + System.arraycopy(buffer, out, b, off + rlen, available) + + out += available + rlen += available + len -= available + if (out >= buffer.length) out = 0 + if (in == out) in = -1 /* now empty */ + + } + rlen + } + + override def available(): Int = synchronized{ + if (in < 0) 0 + else if (in == out) buffer.length + else if (in > out) in - out + else in + buffer.length - out + } + + override def close(): Unit = { + closedByReader = true + synchronized { + in = -1 + } + } + + override def readNBytes(b: Array[Byte], off: Int, len: Int): Int = synchronized{ super.readNBytes(b, off, len) } + + override def readNBytes(len: Int): Array[Byte] = synchronized{ super.readNBytes(len) } + + override def readAllBytes(): Array[Byte] = synchronized{ super.readAllBytes() } + + override def transferTo(out: OutputStream): Long = synchronized{ super.transferTo(out) } + } + + val output: OutputStream = Output + private object Output extends OutputStream { + override def write(b: Int): Unit = synchronized { Input.receive(b) } + override def write(b: Array[Byte]): Unit = synchronized{ super.write(b) } + override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { + if (b == null) throw new NullPointerException + else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException + } + else if (len != 0) Input.receive(b, off, len) + } + + override def flush(): Unit = Input.synchronized { Input.notifyAll() } + override def close(): Unit = Input.receivedLast() + } +} diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 96bc7a16108..27d6c68646e 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -22,16 +22,16 @@ class PrefixLogger( val systemStreams = new SystemStreams( out = outStream0.getOrElse( - new PrintStream(new LinePrefixOutputStream( - infoColor(context).render, +// new PrintStream(new LinePrefixOutputStream( +// infoColor(context).render, logger0.systemStreams.out - )) +// )) ), err = errStream0.getOrElse( - new PrintStream(new LinePrefixOutputStream( - infoColor(context).render, +// new PrintStream(new LinePrefixOutputStream( +// infoColor(context).render, logger0.systemStreams.err - )) +// )) ), logger0.systemStreams.in ) diff --git a/main/util/test/src/mill/util/PipeStreamsTests.scala b/main/util/test/src/mill/util/PipeStreamsTests.scala new file mode 100644 index 00000000000..511e537a5f9 --- /dev/null +++ b/main/util/test/src/mill/util/PipeStreamsTests.scala @@ -0,0 +1,131 @@ +package mill.util + +import utest._ + +import java.util.concurrent.Executors +import scala.concurrent._ +import scala.concurrent.ExecutionContext.Implicits._ +import scala.concurrent.duration.Duration._ + + +object PipeStreamsTests extends TestSuite { + val tests = Tests { + test("hello"){ // Single write and read works + val pipe = new PipeStreams() + val data = Array[Byte](1, 2, 3, 4, 5, 0, 3) + assert(data.length < pipe.bufferSize) + + pipe.output.write(data) + + val out = new Array[Byte](7) + pipe.input.read(out) + out ==> data + } + + test("multiple"){ // Single sequential write and read works + val pipe = new PipeStreams() + val chunkSize = 10 + val chunkCount = 100 + for(i <- Range(0, chunkCount)){ + pipe.output.write(Array.fill(chunkSize)(i.toByte)) + } + + + for(i <- Range(0, chunkCount)){ + pipe.input.readNBytes(chunkSize) ==> Array.fill(chunkSize)(i.toByte) + } + } + test("concurrentWriteRead"){ // Single sequential write and read works + val pipe = new PipeStreams(bufferSize = 13) + val chunkSize = 20 + val chunkCount = 100 + assert(pipe.bufferSize < chunkSize * chunkCount) // ensure it gets filled + val writer = Future { + for (i <- Range(0, chunkCount)) { + pipe.output.write(Array.fill(chunkSize)(i.toByte)) + } + } + + Thread.sleep(100) // Give it time for pipe to fill up + + val reader = Future { + for (i <- Range(0, chunkCount)) yield { + pipe.input.readNBytes(chunkSize).toSeq + } + } + + val result = Await.result(reader, Inf) + val expected = Seq.tabulate(chunkCount)(i => Array.fill(chunkSize)(i.toByte).toSeq) + result ==> expected + } + test("multiThreadWrite"){ // multiple writes across different threads followed by read + val chunkSize = 10 + val chunkCount = 100 + val pipe = new PipeStreams() + assert(chunkSize * chunkCount < pipe.bufferSize) + val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(100)) + val writeFutures = for(i <- Range(0, chunkCount)) yield Future{ + pipe.output.write(Array.fill(chunkSize)(i.toByte)) + }(writerPool) + + Await.ready(Future.sequence(writeFutures), Inf) + + val out = pipe.input.readNBytes(chunkSize * chunkCount) + + val expectedLists = + for(i <- Range(0, chunkCount)) + yield Array.fill(chunkSize)(i.toByte).toSeq + + val sortedGroups = out.toSeq.grouped(chunkSize).toSeq.sortBy(_.head).toVector + + sortedGroups ==> expectedLists + } + test("multiThreadWriteConcurrentRead"){ // multiple writes across different threads interleaved by reads + val chunkSize = 20 + val chunkCount = 100 + val pipe = new PipeStreams(bufferSize = 113) + assert(chunkSize * chunkCount > pipe.bufferSize) + val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(100)) + for(i <- Range(0, chunkCount)) yield Future{ + pipe.output.write(Array.fill(chunkSize)(i.toByte)) + }(writerPool) + + val out = pipe.input.readNBytes(chunkSize * chunkCount) + + val expectedLists = + for(i <- Range(0, chunkCount)) + yield Array.fill(chunkSize)(i.toByte).toSeq + + val sortedGroups = out.toSeq.grouped(chunkSize).toSeq.sortBy(_.head).toVector + + sortedGroups ==> expectedLists + } + // Not sure why this use case doesn't work yet + // test("multiThreadWriteMultiThreadRead"){ // multiple writes across different threads interleaved by reads + // val chunkSize = 20 + // val chunkCount = 100 + // val pipe = new PipeStreams(bufferSize = 137) + // val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(100)) + // val readerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(100)) + // for(i <- Range(0, chunkCount)) yield Future{ + // pipe.output.write(Array.fill(chunkSize)(i.toByte)) + // }(writerPool) + // + // val reader = for (i <- Range(0, chunkCount)) yield Future { + // pipe.input.readNBytes(chunkSize).toSeq + // }(readerPool) + // + // + // val out = Await.result(Future.sequence(reader), Inf) + // val expectedLists = + // for(i <- Range(0, chunkCount)) + // yield Array.fill(chunkSize)(i.toByte).toSeq + // + // val sortedGroups = out.sortBy(_.head).toVector + // + // pprint.log(sortedGroups) + // pprint.log(expectedLists) + // sortedGroups ==> expectedLists + // } + } +} From e23fc079732f35799e1c92800217ef3cf59bf1c0 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 15:29:34 +0800 Subject: [PATCH 068/116] . --- main/util/test/src/mill/util/PipeStreamsTests.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/util/test/src/mill/util/PipeStreamsTests.scala b/main/util/test/src/mill/util/PipeStreamsTests.scala index 511e537a5f9..24927c9fa23 100644 --- a/main/util/test/src/mill/util/PipeStreamsTests.scala +++ b/main/util/test/src/mill/util/PipeStreamsTests.scala @@ -63,7 +63,7 @@ object PipeStreamsTests extends TestSuite { val chunkCount = 100 val pipe = new PipeStreams() assert(chunkSize * chunkCount < pipe.bufferSize) - val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(100)) + val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(40)) val writeFutures = for(i <- Range(0, chunkCount)) yield Future{ pipe.output.write(Array.fill(chunkSize)(i.toByte)) }(writerPool) @@ -85,7 +85,7 @@ object PipeStreamsTests extends TestSuite { val chunkCount = 100 val pipe = new PipeStreams(bufferSize = 113) assert(chunkSize * chunkCount > pipe.bufferSize) - val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(100)) + val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(40)) for(i <- Range(0, chunkCount)) yield Future{ pipe.output.write(Array.fill(chunkSize)(i.toByte)) }(writerPool) From aabc13d48113d09330192fe253bd4b2dcde21a88 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 15:39:47 +0800 Subject: [PATCH 069/116] put back line prefixes --- main/util/src/mill/util/PrefixLogger.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 27d6c68646e..96bc7a16108 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -22,16 +22,16 @@ class PrefixLogger( val systemStreams = new SystemStreams( out = outStream0.getOrElse( -// new PrintStream(new LinePrefixOutputStream( -// infoColor(context).render, + new PrintStream(new LinePrefixOutputStream( + infoColor(context).render, logger0.systemStreams.out -// )) + )) ), err = errStream0.getOrElse( -// new PrintStream(new LinePrefixOutputStream( -// infoColor(context).render, + new PrintStream(new LinePrefixOutputStream( + infoColor(context).render, logger0.systemStreams.err -// )) + )) ), logger0.systemStreams.in ) From 6da90c0f31af6cfade2b6555ebd4604820251692 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 15:58:18 +0800 Subject: [PATCH 070/116] fixes --- .github/workflows/run-mill-action.yml | 2 +- docs/modules/ROOT/pages/Out_Dir.adoc | 6 +++--- example/depth/sandbox/1-task/build.mill | 4 ++-- .../missing-build-file/src/MissingBuildFileTests.scala | 2 +- main/client/src/mill/main/client/ServerFiles.java | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/run-mill-action.yml b/.github/workflows/run-mill-action.yml index 9d667968e14..b7548ee6694 100644 --- a/.github/workflows/run-mill-action.yml +++ b/.github/workflows/run-mill-action.yml @@ -77,7 +77,7 @@ jobs: if: inputs.millargs != '' && startsWith(inputs.os, 'windows') - name: Run Mill (on Windows) Worker Cleanup - run: 'taskkill -f -im java* && rm -rf out/mill-worker-*' + run: 'taskkill -f -im java* && rm -rf out/mill-server-*' if: inputs.millargs != '' && startsWith(inputs.os, 'windows') shell: bash continue-on-error: true diff --git a/docs/modules/ROOT/pages/Out_Dir.adoc b/docs/modules/ROOT/pages/Out_Dir.adoc index ec0903dc99e..6be83fa94d5 100644 --- a/docs/modules/ROOT/pages/Out_Dir.adoc +++ b/docs/modules/ROOT/pages/Out_Dir.adoc @@ -5,7 +5,7 @@ Mill puts all its output in the top-level `out/` folder. == Structure of the `out/` Directory The `out/` folder contains all the generated files & metadata for your build. -It holds some files needed to manage Mill's longer running server instances (`out/mill-worker-*`) as well as a directory and file structure resembling the project's module structure. +It holds some files needed to manage Mill's longer running server instances (`out/mill-server-*`) as well as a directory and file structure resembling the project's module structure. .Example of the `out/` directory after running `mill main.compile` [source,text] @@ -47,7 +47,7 @@ out/ │ ├── unmanagedClasspath.json │ └── upstreamCompileOutput.json ├── mill-profile.json -└── mill-worker-VpZubuAK6LQHHN+3ojh1LsTZqWY=-1/ +└── mill-server/VpZubuAK6LQHHN+3ojh1LsTZqWY=-1/ ---- <1> The `main` directory contains all files associated with target and submodules of the `main` module. @@ -109,5 +109,5 @@ This is very useful if Mill is being unexpectedly slow, and you want to find out `mill-chrome-profile.json`:: This file is only written if you run Mill in parallel mode, e.g. `mill --jobs 4`. This file can be opened in Google Chrome with the built-in `tracing:` protocol even while Mill is still running, so you get a nice chart of what's going on in parallel. -`mill-worker-*/`:: +`mill-server/*`:: Each Mill server instance needs to keep some temporary files in one of these directories. Deleting it will also terminate the associated server instance, if it is still running. diff --git a/example/depth/sandbox/1-task/build.mill b/example/depth/sandbox/1-task/build.mill index 92d08906d72..b8b1b1489e1 100644 --- a/example/depth/sandbox/1-task/build.mill +++ b/example/depth/sandbox/1-task/build.mill @@ -52,14 +52,14 @@ def osProcTask = Task { // // Lastly, there is the possibily of calling `os.pwd` outside of a task. When outside of // a task there is no `.dest/` folder associated, so instead Mill will redirect `os.pwd` -// towards an empty `sandbox/` folder in `out/mill-worker.../`: +// towards an empty `sandbox/` folder in `out/mill-server/...`: val externalPwd = os.pwd def externalPwdTask = Task { println(externalPwd.toString) } /** Usage > ./mill externalPwdTask -.../out/mill-worker-.../sandbox/sandbox +.../out/mill-server-.../sandbox/sandbox */ diff --git a/integration/failure/missing-build-file/src/MissingBuildFileTests.scala b/integration/failure/missing-build-file/src/MissingBuildFileTests.scala index 2b6f625d991..892f5439a42 100644 --- a/integration/failure/missing-build-file/src/MissingBuildFileTests.scala +++ b/integration/failure/missing-build-file/src/MissingBuildFileTests.scala @@ -9,7 +9,7 @@ object MissingBuildFileTests extends UtestIntegrationTestSuite { test - integrationTest { tester => val res = tester.eval(("resolve", "_")) assert(!res.isSuccess) - val s"build.mill file not found in $msg. Are you in a Mill project folder?" = res.err + val s"${prefix}build.mill file not found in $msg. Are you in a Mill project folder?" = res.err } } } diff --git a/main/client/src/mill/main/client/ServerFiles.java b/main/client/src/mill/main/client/ServerFiles.java index 3351fb93902..19f6efd19ad 100644 --- a/main/client/src/mill/main/client/ServerFiles.java +++ b/main/client/src/mill/main/client/ServerFiles.java @@ -1,7 +1,7 @@ package mill.main.client; /** - * Central place containing all the files that live inside the `out/mill-worker-*` folder + * Central place containing all the files that live inside the `out/mill-serer-*` folder * and documentation about what they do */ public class ServerFiles { @@ -9,14 +9,14 @@ public class ServerFiles { final public static String sandbox = "sandbox"; /** - * Ensures only a single client is manipulating each mill-worker folder at + * Ensures only a single client is manipulating each mill-server folder at * a time, either spawning the server or submitting a command. Also used by * the server to detect when a client disconnects, so it can terminate execution */ final public static String clientLock = "clientLock"; /** - * Lock file ensuring a single server is running in a particular mill-worker + * Lock file ensuring a single server is running in a particular mill-server * folder. If multiple servers are spawned in the same folder, only one takes * the lock and the others fail to do so and terminate immediately. */ From 5d85b47eda59d7a6d721fc4e1aeb0a3e81338046 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 16:16:13 +0800 Subject: [PATCH 071/116] fixes --- .github/workflows/run-mill-action.yml | 2 +- docs/modules/ROOT/pages/Out_Dir.adoc | 2 +- example/depth/sandbox/1-task/build.mill | 2 +- .../failure/missing-build-file/src/MissingBuildFileTests.scala | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-mill-action.yml b/.github/workflows/run-mill-action.yml index b7548ee6694..0cfc4435929 100644 --- a/.github/workflows/run-mill-action.yml +++ b/.github/workflows/run-mill-action.yml @@ -77,7 +77,7 @@ jobs: if: inputs.millargs != '' && startsWith(inputs.os, 'windows') - name: Run Mill (on Windows) Worker Cleanup - run: 'taskkill -f -im java* && rm -rf out/mill-server-*' + run: 'taskkill -f -im java* && rm -rf out/mill-server/*' if: inputs.millargs != '' && startsWith(inputs.os, 'windows') shell: bash continue-on-error: true diff --git a/docs/modules/ROOT/pages/Out_Dir.adoc b/docs/modules/ROOT/pages/Out_Dir.adoc index 6be83fa94d5..0deb6330950 100644 --- a/docs/modules/ROOT/pages/Out_Dir.adoc +++ b/docs/modules/ROOT/pages/Out_Dir.adoc @@ -5,7 +5,7 @@ Mill puts all its output in the top-level `out/` folder. == Structure of the `out/` Directory The `out/` folder contains all the generated files & metadata for your build. -It holds some files needed to manage Mill's longer running server instances (`out/mill-server-*`) as well as a directory and file structure resembling the project's module structure. +It holds some files needed to manage Mill's longer running server instances (`out/mill-server/*`) as well as a directory and file structure resembling the project's module structure. .Example of the `out/` directory after running `mill main.compile` [source,text] diff --git a/example/depth/sandbox/1-task/build.mill b/example/depth/sandbox/1-task/build.mill index b8b1b1489e1..27203e392f3 100644 --- a/example/depth/sandbox/1-task/build.mill +++ b/example/depth/sandbox/1-task/build.mill @@ -59,7 +59,7 @@ def externalPwdTask = Task { println(externalPwd.toString) } /** Usage > ./mill externalPwdTask -.../out/mill-server-.../sandbox/sandbox +.../out/mill-server/.../sandbox/sandbox */ diff --git a/integration/failure/missing-build-file/src/MissingBuildFileTests.scala b/integration/failure/missing-build-file/src/MissingBuildFileTests.scala index 892f5439a42..1b314a5c396 100644 --- a/integration/failure/missing-build-file/src/MissingBuildFileTests.scala +++ b/integration/failure/missing-build-file/src/MissingBuildFileTests.scala @@ -11,5 +11,6 @@ object MissingBuildFileTests extends UtestIntegrationTestSuite { assert(!res.isSuccess) val s"${prefix}build.mill file not found in $msg. Are you in a Mill project folder?" = res.err } + getClass.getResource() } } From e4cd4f61a101111f07152894511200f44e16ff44 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 16:23:15 +0800 Subject: [PATCH 072/116] fixes --- .../failure/missing-build-file/src/MissingBuildFileTests.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/integration/failure/missing-build-file/src/MissingBuildFileTests.scala b/integration/failure/missing-build-file/src/MissingBuildFileTests.scala index 1b314a5c396..892f5439a42 100644 --- a/integration/failure/missing-build-file/src/MissingBuildFileTests.scala +++ b/integration/failure/missing-build-file/src/MissingBuildFileTests.scala @@ -11,6 +11,5 @@ object MissingBuildFileTests extends UtestIntegrationTestSuite { assert(!res.isSuccess) val s"${prefix}build.mill file not found in $msg. Are you in a Mill project folder?" = res.err } - getClass.getResource() } } From 2ae34787d00e6496ff8d83cb2a005e4c85541c21 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 17:55:46 +0800 Subject: [PATCH 073/116] fix --- example/depth/sandbox/1-task/build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/depth/sandbox/1-task/build.mill b/example/depth/sandbox/1-task/build.mill index 27203e392f3..f5f133f36da 100644 --- a/example/depth/sandbox/1-task/build.mill +++ b/example/depth/sandbox/1-task/build.mill @@ -59,7 +59,7 @@ def externalPwdTask = Task { println(externalPwd.toString) } /** Usage > ./mill externalPwdTask -.../out/mill-server/.../sandbox/sandbox +.../out/mill-server/.../sandbox */ From 985c6918b69ad2dd7965d557694adcb62b7173c9 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 18:14:01 +0800 Subject: [PATCH 074/116] . --- main/api/src/mill/api/SystemStreams.scala | 20 ++++----- .../mill/util/LinePrefixOutputStream.scala | 2 +- .../src/mill/util/MultilinePromptLogger.scala | 2 +- main/util/src/mill/util/PipeStreams.scala | 41 +++++++++---------- .../util/MultilinePromptLoggerTests.scala | 24 +++++++++-- .../test/src/mill/util/PipeStreamsTests.scala | 29 +++++++------ 6 files changed, 65 insertions(+), 53 deletions(-) diff --git a/main/api/src/mill/api/SystemStreams.scala b/main/api/src/mill/api/SystemStreams.scala index 222d6e94211..29052c4c4af 100644 --- a/main/api/src/mill/api/SystemStreams.scala +++ b/main/api/src/mill/api/SystemStreams.scala @@ -37,7 +37,6 @@ object SystemStreams { // (System.err eq original.err) && // (System.in eq original.in) && - // We do not check `Console.in` for equality, because `Console.withIn` always wraps // `Console.in` in a `new BufferedReader` each time, and so it is impossible to check // whether it is original or not. We just have to assume that it is kept in sync with @@ -100,7 +99,6 @@ object SystemStreams { } } - /** * Manages the global override of `System.{in,out,err}`. Overrides of those streams are * global, so we cannot just override them per-use-site in a multithreaded environment @@ -123,7 +121,7 @@ object SystemStreams { System.setIn(in) } } - def setTopLevelSystemStreamProxy() = { + def setTopLevelSystemStreamProxy(): Unit = { // Make sure to initialize `Console` to cache references to the original // `System.{in,out,err}` streams before we redirect them Console.out @@ -134,15 +132,14 @@ object SystemStreams { System.setErr(ThreadLocalStreams.Err) } - - private[mill] object ThreadLocalStreams{ + private[mill] object ThreadLocalStreams { val current = new DynamicVariable(original) - object Out extends PrintStream(new ProxyOutputStream{ def delegate() = current.value.out}) - object Err extends PrintStream(new ProxyOutputStream{ def delegate() = current.value.err }) - object In extends ProxyInputStream{ def delegate() = current.value.in} + object Out extends PrintStream(new ProxyOutputStream { def delegate() = current.value.out }) + object Err extends PrintStream(new ProxyOutputStream { def delegate() = current.value.err }) + object In extends ProxyInputStream { def delegate() = current.value.in } - abstract class ProxyOutputStream extends OutputStream{ + abstract class ProxyOutputStream extends OutputStream { def delegate(): OutputStream override def write(b: Array[Byte], off: Int, len: Int): Unit = delegate().write(b, off, len) override def write(b: Array[Byte]): Unit = delegate().write(b) @@ -150,12 +147,13 @@ object SystemStreams { override def flush(): Unit = delegate().flush() override def close(): Unit = delegate().close() } - abstract class ProxyInputStream extends InputStream{ + abstract class ProxyInputStream extends InputStream { def delegate(): InputStream override def read(): Int = delegate().read() override def read(b: Array[Byte], off: Int, len: Int): Int = delegate().read(b, off, len) override def read(b: Array[Byte]): Int = delegate().read(b) - override def readNBytes(b: Array[Byte], off: Int, len: Int): Int = delegate().readNBytes(b, off, len) + override def readNBytes(b: Array[Byte], off: Int, len: Int): Int = + delegate().readNBytes(b, off, len) override def readNBytes(len: Int): Array[Byte] = delegate().readNBytes(len) override def readAllBytes(): Array[Byte] = delegate().readAllBytes() override def mark(readlimit: Int): Unit = delegate().mark(readlimit) diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 34b2d06015e..2fb3ce922c3 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -45,5 +45,5 @@ class LinePrefixOutputStream( buffer.reset() super.flush() } - override def close() = out.close() + override def close(): Unit = out.close() } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 748242c26fb..f9e63c8c3dc 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -1,7 +1,7 @@ package mill.util import mill.api.SystemStreams -import mill.main.client.{DebugLog, ProxyStream} +import mill.main.client.ProxyStream import java.io._ diff --git a/main/util/src/mill/util/PipeStreams.scala b/main/util/src/mill/util/PipeStreams.scala index 4168cb09981..3e79ba98185 100644 --- a/main/util/src/mill/util/PipeStreams.scala +++ b/main/util/src/mill/util/PipeStreams.scala @@ -20,20 +20,19 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => val input: InputStream = Input private object Input extends InputStream { - private[PipeStreams] def receive(b: Int): Unit = synchronized{ + private[PipeStreams] def receive(b: Int): Unit = synchronized { checkStateForReceive() if (in == out) awaitSpace() if (in < 0) { in = 0 out = 0 } - buffer(in) = (b & 0xFF).toByte + buffer(in) = (b & 0xff).toByte in += 1 if (in >= buffer.length) in = 0 } - - private[PipeStreams] def receive(b: Array[Byte], off0: Int, len: Int): Unit = synchronized{ + private[PipeStreams] def receive(b: Array[Byte], off0: Int, len: Int): Unit = synchronized { var off = off0 checkStateForReceive() var bytesToTransfer = len @@ -46,8 +45,7 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => in = 0 out = 0 nextTransferAmount = buffer.length - in - } - else { + } else { nextTransferAmount = out - in } } @@ -61,12 +59,10 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => } } - private def checkStateForReceive(): Unit = { if (closedByWriter || closedByReader) throw new IOException("Pipe closed") } - private def awaitSpace(): Unit = { while (in == out) { checkStateForReceive() @@ -83,8 +79,9 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => private def notifyWaitCatch() = { notifyAll() try wait(1000) - catch { case ex: InterruptedException => - throw new java.io.InterruptedIOException + catch { + case ex: InterruptedException => + throw new java.io.InterruptedIOException } } @@ -97,7 +94,7 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => notifyWaitCatch() } - val ret = buffer(out) & 0xFF + val ret = buffer(out) & 0xff out += 1 if (out >= buffer.length) out = 0 if (in == out) in = -1 /* now empty */ @@ -105,7 +102,7 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => ret } - override def read(b: Array[Byte], off: Int, len0: Int): Int = synchronized{ + override def read(b: Array[Byte], off: Int, len0: Int): Int = synchronized { var len = len0 if (b == null) throw new NullPointerException else if (off < 0 || len < 0 || len > b.length - off) throw new IndexOutOfBoundsException @@ -134,7 +131,7 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => rlen } - override def available(): Int = synchronized{ + override def available(): Int = synchronized { if (in < 0) 0 else if (in == out) buffer.length else if (in > out) in - out @@ -148,25 +145,27 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => } } - override def readNBytes(b: Array[Byte], off: Int, len: Int): Int = synchronized{ super.readNBytes(b, off, len) } + override def readNBytes(b: Array[Byte], off: Int, len: Int): Int = + synchronized { super.readNBytes(b, off, len) } - override def readNBytes(len: Int): Array[Byte] = synchronized{ super.readNBytes(len) } + override def readNBytes(len: Int): Array[Byte] = synchronized { super.readNBytes(len) } - override def readAllBytes(): Array[Byte] = synchronized{ super.readAllBytes() } + override def readAllBytes(): Array[Byte] = synchronized { super.readAllBytes() } - override def transferTo(out: OutputStream): Long = synchronized{ super.transferTo(out) } + override def transferTo(out: OutputStream): Long = synchronized { super.transferTo(out) } } val output: OutputStream = Output private object Output extends OutputStream { override def write(b: Int): Unit = synchronized { Input.receive(b) } - override def write(b: Array[Byte]): Unit = synchronized{ super.write(b) } + override def write(b: Array[Byte]): Unit = synchronized { super.write(b) } override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { if (b == null) throw new NullPointerException - else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { + else if ( + (off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) + ) { throw new IndexOutOfBoundsException - } - else if (len != 0) Input.receive(b, off, len) + } else if (len != 0) Input.receive(b, off, len) } override def flush(): Unit = Input.synchronized { Input.notifyAll() } diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index c187cc7fea8..6ef776a8c31 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -208,7 +208,11 @@ object MultilinePromptLoggerTests extends TestSuite { headerPrefix = "123/456", titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", statuses = SortedMap( - 0 -> Status(now - 1000, "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, + Long.MaxValue + ), 1 -> Status(now - 2000, "#2 world", Long.MaxValue), 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), @@ -239,7 +243,11 @@ object MultilinePromptLoggerTests extends TestSuite { titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", statuses = SortedMap( // Not yet removed, should be shown - 0 -> Status(now - 1000, "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, + Long.MaxValue + ), // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), @@ -248,7 +256,11 @@ object MultilinePromptLoggerTests extends TestSuite { 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), 4 -> Status(now - 5000, "#5i weigh twice", now - statusRemovalRemoveDelayMillis + 1), 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1), - 6 -> Status(now - 7000, "#7 and I look good on the barbecue", now - statusRemovalRemoveDelayMillis + 1) + 6 -> Status( + now - 7000, + "#7 and I look good on the barbecue", + now - statusRemovalRemoveDelayMillis + 1 + ) ), interactive = true ) @@ -276,7 +288,11 @@ object MultilinePromptLoggerTests extends TestSuite { titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", statuses = SortedMap( // Not yet removed, should be shown - 0 -> Status(now - 1000, "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, Long.MaxValue), + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, + Long.MaxValue + ), // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), diff --git a/main/util/test/src/mill/util/PipeStreamsTests.scala b/main/util/test/src/mill/util/PipeStreamsTests.scala index 24927c9fa23..ab009adcc1e 100644 --- a/main/util/test/src/mill/util/PipeStreamsTests.scala +++ b/main/util/test/src/mill/util/PipeStreamsTests.scala @@ -7,10 +7,9 @@ import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits._ import scala.concurrent.duration.Duration._ - object PipeStreamsTests extends TestSuite { val tests = Tests { - test("hello"){ // Single write and read works + test("hello") { // Single write and read works val pipe = new PipeStreams() val data = Array[Byte](1, 2, 3, 4, 5, 0, 3) assert(data.length < pipe.bufferSize) @@ -22,20 +21,19 @@ object PipeStreamsTests extends TestSuite { out ==> data } - test("multiple"){ // Single sequential write and read works + test("multiple") { // Single sequential write and read works val pipe = new PipeStreams() val chunkSize = 10 val chunkCount = 100 - for(i <- Range(0, chunkCount)){ + for (i <- Range(0, chunkCount)) { pipe.output.write(Array.fill(chunkSize)(i.toByte)) } - - for(i <- Range(0, chunkCount)){ + for (i <- Range(0, chunkCount)) { pipe.input.readNBytes(chunkSize) ==> Array.fill(chunkSize)(i.toByte) } } - test("concurrentWriteRead"){ // Single sequential write and read works + test("concurrentWriteRead") { // Single sequential write and read works val pipe = new PipeStreams(bufferSize = 13) val chunkSize = 20 val chunkCount = 100 @@ -58,42 +56,43 @@ object PipeStreamsTests extends TestSuite { val expected = Seq.tabulate(chunkCount)(i => Array.fill(chunkSize)(i.toByte).toSeq) result ==> expected } - test("multiThreadWrite"){ // multiple writes across different threads followed by read + test("multiThreadWrite") { // multiple writes across different threads followed by read val chunkSize = 10 val chunkCount = 100 val pipe = new PipeStreams() assert(chunkSize * chunkCount < pipe.bufferSize) val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(40)) - val writeFutures = for(i <- Range(0, chunkCount)) yield Future{ - pipe.output.write(Array.fill(chunkSize)(i.toByte)) - }(writerPool) + val writeFutures = + for (i <- Range(0, chunkCount)) yield Future { + pipe.output.write(Array.fill(chunkSize)(i.toByte)) + }(writerPool) Await.ready(Future.sequence(writeFutures), Inf) val out = pipe.input.readNBytes(chunkSize * chunkCount) val expectedLists = - for(i <- Range(0, chunkCount)) + for (i <- Range(0, chunkCount)) yield Array.fill(chunkSize)(i.toByte).toSeq val sortedGroups = out.toSeq.grouped(chunkSize).toSeq.sortBy(_.head).toVector sortedGroups ==> expectedLists } - test("multiThreadWriteConcurrentRead"){ // multiple writes across different threads interleaved by reads + test("multiThreadWriteConcurrentRead") { // multiple writes across different threads interleaved by reads val chunkSize = 20 val chunkCount = 100 val pipe = new PipeStreams(bufferSize = 113) assert(chunkSize * chunkCount > pipe.bufferSize) val writerPool = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(40)) - for(i <- Range(0, chunkCount)) yield Future{ + for (i <- Range(0, chunkCount)) yield Future { pipe.output.write(Array.fill(chunkSize)(i.toByte)) }(writerPool) val out = pipe.input.readNBytes(chunkSize * chunkCount) val expectedLists = - for(i <- Range(0, chunkCount)) + for (i <- Range(0, chunkCount)) yield Array.fill(chunkSize)(i.toByte).toSeq val sortedGroups = out.toSeq.grouped(chunkSize).toSeq.sortBy(_.head).toVector From 414e727be9047a8223027dc42383174c65f89d59 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 22 Sep 2024 20:26:25 +0800 Subject: [PATCH 075/116] try to fix circular system stream --- runner/src/mill/runner/MillMain.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index f688af40baf..693f06c5edc 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -35,7 +35,7 @@ object MillMain { } def main(args: Array[String]): Unit = SystemStreams.withTopLevelSystemStreamProxy { - val initialSystemStreams = new SystemStreams(Console.out, Console.err, System.in) + val initialSystemStreams = SystemStreams.original // setup streams val (runnerStreams, cleanupStreams, bspLog) = if (args.headOption == Option("--bsp")) { From 68b0d1aa790ee220cd92be5e41b6e52cfe03c30b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 10:49:47 +0800 Subject: [PATCH 076/116] . --- build.mill | 2 +- main/api/src/mill/api/Logger.scala | 1 + main/eval/src/mill/eval/EvaluatorCore.scala | 1 + .../src/mill/util/MultilinePromptLogger.scala | 18 ++++++++++-------- main/util/src/mill/util/PipeStreams.scala | 10 +++++----- runner/src/mill/runner/MillMain.scala | 2 +- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/build.mill b/build.mill index a4039a6288d..0095d56e48d 100644 --- a/build.mill +++ b/build.mill @@ -20,7 +20,7 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload - +//import $packages._ object Settings { val pomOrg = "com.lihaoyi" val githubOrg = "com-lihaoyi" diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index eb5cf64764b..93e1ab795b6 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -45,6 +45,7 @@ trait Logger { def error(s: String): Unit def ticker(s: String): Unit def globalTicker(s: String): Unit = () + def clearAllTickers(): Unit = () def endTicker(): Unit = () def debug(s: String): Unit diff --git a/main/eval/src/mill/eval/EvaluatorCore.scala b/main/eval/src/mill/eval/EvaluatorCore.scala index 24e592d6929..8e75460e870 100644 --- a/main/eval/src/mill/eval/EvaluatorCore.scala +++ b/main/eval/src/mill/eval/EvaluatorCore.scala @@ -188,6 +188,7 @@ private[mill] trait EvaluatorCore extends GroupEvaluator { )(ec) evaluateTerminals(leafCommands, _ => "")(ExecutionContexts.RunNow) + logger.clearAllTickers() val finishedOptsMap = terminals0 .map(t => (t, Await.result(futures(t), duration.Duration.Inf))) .toMap diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index f9e63c8c3dc..53f54e7aa8a 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -1,7 +1,7 @@ package mill.util import mill.api.SystemStreams -import mill.main.client.ProxyStream +import mill.main.client.{DebugLog, ProxyStream} import java.io._ @@ -68,7 +68,7 @@ private[mill] class MultilinePromptLogger( def error(s: String): Unit = synchronized { systemStreams.err.println(s) } override def globalTicker(s: String): Unit = synchronized { state.updateGlobal(s) } - + override def clearAllTickers(): Unit = synchronized{ state.clearStatuses() } override def endTicker(): Unit = synchronized { state.updateCurrent(None) } def ticker(s: String): Unit = synchronized { state.updateCurrent(Some(s)) } @@ -159,12 +159,16 @@ private object MultilinePromptLogger { // every small write when most such prompts will get immediately over-written // by subsequent writes if (enableTicker && src.available() == 0) { - if (interactive()) systemStreams0.err.write(currentPromptBytes()) + if (interactive()) { + DebugLog.println("prompt") + systemStreams0.err.write(currentPromptBytes()) + } pumperState = PumperState.prompt } } override def preWrite(): Unit = { + DebugLog.println("write") // Before any write, make sure we clear the terminal of any prompt that was // written earlier and not yet cleared, so the following output can be written // to a clean section of the terminal @@ -193,7 +197,7 @@ private object MultilinePromptLogger { startTimeMillis: Long, consoleDims: () => (Option[Int], Option[Int]) ) { - var lastRenderedPromptHash = 0 + private var lastRenderedPromptHash = 0 private val statuses = collection.mutable.SortedMap.empty[Int, Status] private var headerPrefix = "" @@ -247,10 +251,8 @@ private object MultilinePromptLogger { } - def updateGlobal(s: String): Unit = synchronized { - statuses.clear() - headerPrefix = s - } + def clearStatuses(): Unit = synchronized { statuses.clear() } + def updateGlobal(s: String): Unit = synchronized { headerPrefix = s } def updateCurrent(sOpt: Option[String]): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt diff --git a/main/util/src/mill/util/PipeStreams.scala b/main/util/src/mill/util/PipeStreams.scala index 3e79ba98185..aded18619ca 100644 --- a/main/util/src/mill/util/PipeStreams.scala +++ b/main/util/src/mill/util/PipeStreams.scala @@ -3,11 +3,11 @@ package mill.util import java.io.{IOException, InputStream, OutputStream} /** - * Fork of `java.io.Piped{Input,Output}Stream` that allows reads and writes. - * to come from separate threads. Really the same logic just with the assertions - * on thread liveness removed, added some synchronization to ensure atomic - * writes, and somewhat cleaned up as a single object rather than two loose - * objects you have to connect together. + * Fork of `java.io.Piped{Input,Output}Stream` that allows writes to come from + * separate threads. Really the same logic just with the assertions on thread + * liveness removed, added some synchronization to ensure atomic writes, and + * somewhat cleaned up as a single object rather than two loose objects you have + * to connect together. */ class PipeStreams(val bufferSize: Int = 1024) { pipe => diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 693f06c5edc..96e43dbfc90 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -333,7 +333,7 @@ object MillMain { } else { new MultilinePromptLogger( colored = colored, - enableTicker = enableTicker.getOrElse(mainInteractive), + enableTicker = enableTicker.getOrElse(true), infoColor = colors.info, errorColor = colors.error, systemStreams0 = streams, From 7f94afd927166d8c5f04563090ababb801fb8192 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 10:51:26 +0800 Subject: [PATCH 077/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 53f54e7aa8a..729b7bed8be 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -68,7 +68,7 @@ private[mill] class MultilinePromptLogger( def error(s: String): Unit = synchronized { systemStreams.err.println(s) } override def globalTicker(s: String): Unit = synchronized { state.updateGlobal(s) } - override def clearAllTickers(): Unit = synchronized{ state.clearStatuses() } + override def clearAllTickers(): Unit = synchronized { state.clearStatuses() } override def endTicker(): Unit = synchronized { state.updateCurrent(None) } def ticker(s: String): Unit = synchronized { state.updateCurrent(Some(s)) } From 7fcd03da6905f28b5993af50e10d397d63fdb08e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 10:53:01 +0800 Subject: [PATCH 078/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 729b7bed8be..681c742c01b 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -160,7 +160,6 @@ private object MultilinePromptLogger { // by subsequent writes if (enableTicker && src.available() == 0) { if (interactive()) { - DebugLog.println("prompt") systemStreams0.err.write(currentPromptBytes()) } pumperState = PumperState.prompt @@ -168,7 +167,6 @@ private object MultilinePromptLogger { } override def preWrite(): Unit = { - DebugLog.println("write") // Before any write, make sure we clear the terminal of any prompt that was // written earlier and not yet cleared, so the following output can be written // to a clean section of the terminal From d2d2b1e1f95192d15eae20aa7f9726ca698e6a41 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 12:51:43 +0800 Subject: [PATCH 079/116] . --- main/util/src/mill/util/MultilinePromptLogger.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 681c742c01b..012327d7e2b 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -1,7 +1,7 @@ package mill.util import mill.api.SystemStreams -import mill.main.client.{DebugLog, ProxyStream} +import mill.main.client.ProxyStream import java.io._ From 516182d1534821d5f228a1c0848ce5e0352841ca Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 12:54:23 +0800 Subject: [PATCH 080/116] . --- main/eval/src/mill/eval/GroupEvaluator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index a59d02c40bd..5b36d6cd2d7 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -255,7 +255,7 @@ private[mill] trait GroupEvaluator { withTicker(tickerPrefix) { val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { override def ticker(s: String): Unit = { - if (enableTicker) super.ticker(tickerPrefix.getOrElse("") + s) + if (enableTicker) super.ticker(tickerPrefix.map(_ + " ").getOrElse("") + s) else () // do nothing } From 7726da39c1e86f7296575af04421c3dda2613140 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 12:58:29 +0800 Subject: [PATCH 081/116] . --- readme.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/readme.adoc b/readme.adoc index 33a80b33ec3..0c759295161 100644 --- a/readme.adoc +++ b/readme.adoc @@ -87,6 +87,14 @@ The following table contains the main ways you can test the code in | Bootstrapping: Building Mill with your current checkout of Mill | | `installLocal` | `test-mill-bootstrap.sh` |=== +In general, `println` or `pprint.log` should work in most places and be sufficient for +instrumenting and debugging the Mill codebase. In the occasional spot where `println` +doesn't work you can use `mill.main.client.DebugLog.println` which writes to a file +`~/mill-debug-log.txt` in your home folder. `DebugLog` is useful for scenarios like +debugging Mill's terminal UI (where `println` would mess things up) or subprocesses +(where stdout/stderr may get captured or used and cannot be used to display your own +debug statements). + === In-Process Tests In-process tests live in the `.test` sub-modules of the various Mill modules. From 38a2b1accd95fdfde4e7db73e6b926b7a84beca5 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 18:57:15 +0800 Subject: [PATCH 082/116] always notify in PipeStreams --- .../client/src/mill/main/client/DebugLog.java | 8 +- main/util/src/mill/util/FileLogger.scala | 1 + .../mill/util/LinePrefixOutputStream.scala | 78 +++++++++++++------ main/util/src/mill/util/MultiLogger.scala | 2 +- .../src/mill/util/MultilinePromptLogger.scala | 4 +- main/util/src/mill/util/PipeStreams.scala | 4 +- main/util/src/mill/util/PrefixLogger.scala | 3 + main/util/src/mill/util/PrintLogger.scala | 2 +- main/util/src/mill/util/ProxyLogger.scala | 1 + .../util/LinePrefixOutputStreamTests.scala | 69 ++++++++++++++++ 10 files changed, 139 insertions(+), 33 deletions(-) create mode 100644 main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala diff --git a/main/client/src/mill/main/client/DebugLog.java b/main/client/src/mill/main/client/DebugLog.java index cc7886854d1..2ee8473f570 100644 --- a/main/client/src/mill/main/client/DebugLog.java +++ b/main/client/src/mill/main/client/DebugLog.java @@ -9,12 +9,10 @@ */ public class DebugLog{ synchronized public static void println(String s){ + Path path = Paths.get(System.getProperty("user.home"), "mill-debug-log.txt"); try { - Files.writeString( - Paths.get(System.getProperty("user.home"), "mill-debug-log.txt"), - s + "\n", - StandardOpenOption.APPEND - ); + if (!Files.exists(path)) Files.createFile(path); + Files.writeString(path, s + "\n", StandardOpenOption.APPEND); }catch (IOException e){ throw new RuntimeException(e); } diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index b8ec7118205..f887bc49760 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -11,6 +11,7 @@ class FileLogger( override val debugEnabled: Boolean, append: Boolean = false ) extends Logger { + override def toString = s"FileLogger($file)" private[this] var outputStreamUsed: Boolean = false lazy val fileStream: PrintStream = { diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 2fb3ce922c3..772a359e69a 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -1,49 +1,79 @@ package mill.util -import java.io.{ByteArrayOutputStream, FilterOutputStream, OutputStream} +import mill.main.client.DebugLog + +import java.io.{ByteArrayOutputStream, OutputStream} /** - * Prefixes the first and each new line with a dynamically provided prefix. + * Prefixes the first and each new line with a dynamically provided prefix, + * and buffers up each line in memory before writing to the [[out]] stream + * to prevent individual lines from being mixed together + * * @param linePrefix The function to provide the prefix. * @param out The underlying output stream. */ class LinePrefixOutputStream( linePrefix: String, out: OutputStream -) extends FilterOutputStream(out) { - - private[this] var isFirst = true +) extends OutputStream { + private[this] val linePrefixBytes = linePrefix.getBytes("UTF-8") + private[this] val linePrefixNonEmpty = linePrefixBytes.length != 0 + private[this] var isNewLine = true val buffer = new ByteArrayOutputStream() override def write(b: Array[Byte]): Unit = write(b, 0, b.length) - override def write(b: Array[Byte], off: Int, len: Int): Unit = { - var i = off - while (i < len) { - write(b(i)) - i += 1 + private[this] def writeLinePrefixIfNecessary(): Unit = { + if (isNewLine && linePrefixNonEmpty) { + isNewLine = false + buffer.write(linePrefixBytes) } } - override def write(b: Int): Unit = { - if (isFirst) { - isFirst = false - if (linePrefix != "") { - buffer.write(linePrefix.getBytes("UTF-8")) + def writeOutBuffer() = { + out.synchronized { out.write(buffer.toByteArray) } + buffer.reset() + } + + override def write(b: Array[Byte], off: Int, len: Int): Unit = synchronized { + var start = off + var i = off + val max = off + len + while (i < max) { + writeLinePrefixIfNecessary() + if (b(i) == '\n') { + i += 1 // +1 to include the newline + buffer.write(b, start, i - start) + isNewLine = true + start = i + writeOutBuffer() } + i += 1 } + + if (math.min(i, max) - start > 0){ + writeLinePrefixIfNecessary() + buffer.write(b, start, math.min(i, max) - start) + if (b(max - 1) == '\n') writeOutBuffer() + } + + } + + override def write(b: Int): Unit = synchronized { + writeLinePrefixIfNecessary() buffer.write(b) if (b == '\n') { - flush() - isFirst = true + writeOutBuffer() + isNewLine = true } } - override def flush(): Unit = { - out.synchronized { - out.write(buffer.toByteArray) - } - buffer.reset() - super.flush() + override def flush(): Unit = synchronized { + writeOutBuffer() + out.flush() + } + + override def close(): Unit = { + flush() + out.close() } - override def close(): Unit = out.close() } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 5dc6a855771..644d246031d 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -11,7 +11,7 @@ class MultiLogger( val inStream0: InputStream, override val debugEnabled: Boolean ) extends Logger { - + override def toString = s"MultiLogger($logger1, $logger2)" lazy val systemStreams = new SystemStreams( new MultiStream(logger1.systemStreams.out, logger2.systemStreams.out), new MultiStream(logger1.systemStreams.err, logger2.systemStreams.err), diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 012327d7e2b..4d26c3c30f9 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -2,6 +2,7 @@ package mill.util import mill.api.SystemStreams import mill.main.client.ProxyStream +import pprint.Util.literalize import java.io._ @@ -15,6 +16,7 @@ private[mill] class MultilinePromptLogger( titleText: String, terminfoPath: os.Path ) extends ColorLogger with AutoCloseable { + override def toString = s"MultilinePromptLogger(${literalize(titleText)}" import MultilinePromptLogger._ private var termDimensions: (Option[Int], Option[Int]) = (None, None) @@ -141,7 +143,7 @@ private object MultilinePromptLogger { // print the prompt at the bottom val pipe = new PipeStreams() val proxyOut = new ProxyStream.Output(pipe.output, ProxyStream.OUT) - val proxyErr = new ProxyStream.Output(pipe.output, ProxyStream.ERR) + val proxyErr: ProxyStream.Output = new ProxyStream.Output(pipe.output, ProxyStream.ERR) val systemStreams = new SystemStreams( new PrintStream(proxyOut), new PrintStream(proxyErr), diff --git a/main/util/src/mill/util/PipeStreams.scala b/main/util/src/mill/util/PipeStreams.scala index aded18619ca..c1f6b75d8e2 100644 --- a/main/util/src/mill/util/PipeStreams.scala +++ b/main/util/src/mill/util/PipeStreams.scala @@ -30,6 +30,7 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => buffer(in) = (b & 0xff).toByte in += 1 if (in >= buffer.length) in = 0 + notifyAll() } private[PipeStreams] def receive(b: Array[Byte], off0: Int, len: Int): Unit = synchronized { @@ -57,6 +58,7 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => in += nextTransferAmount if (in >= buffer.length) in = 0 } + notifyAll() } private def checkStateForReceive(): Unit = { @@ -168,7 +170,7 @@ class PipeStreams(val bufferSize: Int = 1024) { pipe => } else if (len != 0) Input.receive(b, off, len) } - override def flush(): Unit = Input.synchronized { Input.notifyAll() } + override def flush(): Unit = () override def close(): Unit = Input.receivedLast() } } diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 96bc7a16108..054d2c6253b 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -1,6 +1,8 @@ package mill.util import mill.api.SystemStreams +import mill.main.client.DebugLog +import pprint.Util.literalize import java.io.PrintStream @@ -12,6 +14,7 @@ class PrefixLogger( errStream0: Option[PrintStream] = None ) extends ColorLogger { + override def toString = s"PrefixLogger($logger0, ${literalize(context)}, ${literalize(tickerContext)})" def this(logger0: ColorLogger, context: String, tickerContext: String) = this(logger0, context, tickerContext, None, None) diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index f59f65d66cd..c3f5569a318 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -13,7 +13,7 @@ class PrintLogger( val context: String, printLoggerState: PrintLogger.State ) extends ColorLogger { - + override def toString = s"PrintLogger($colored, $enableTicker)" def info(s: String): Unit = synchronized { printLoggerState.value = PrintLogger.State.Newline systemStreams.err.println(infoColor(context + s)) diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 4358f8a392a..db3f556673d 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -9,6 +9,7 @@ import java.io.PrintStream * used as a base class for wrappers that modify logging behavior. */ class ProxyLogger(logger: Logger) extends Logger { + override def toString = s"ProxyLogger($logger)" def colored = logger.colored lazy val systemStreams = logger.systemStreams diff --git a/main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala b/main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala new file mode 100644 index 00000000000..6ecda54f4c6 --- /dev/null +++ b/main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala @@ -0,0 +1,69 @@ +package mill.util + +import utest._ + +import java.io.ByteArrayOutputStream + + +object LinePrefixOutputStreamTests extends TestSuite { + val tests = Tests { + test("charByChar") { + val baos = new ByteArrayOutputStream() + val lpos = new LinePrefixOutputStream("PREFIX", baos) + for(b <- "hello\nworld\n!".getBytes()) lpos.write(b) + lpos.flush() + assert(baos.toString == "PREFIXhello\nPREFIXworld\nPREFIX!") + } + + test("charByCharTrailingNewline") { + val baos = new ByteArrayOutputStream() + val lpos = new LinePrefixOutputStream("PREFIX", baos) + for(b <- "hello\nworld\n".getBytes()) lpos.write(b) + lpos.flush() + assert(baos.toString == "PREFIXhello\nPREFIXworld\n") + } + + test("allAtOnce") { + val baos = new ByteArrayOutputStream() + val lpos = new LinePrefixOutputStream("PREFIX", baos) + val arr = "hello\nworld\n!".getBytes() + lpos.write(arr) + lpos.flush() + + assert(baos.toString == "PREFIXhello\nPREFIXworld\nPREFIX!") + } + + test("allAtOnceTrailingNewline") { + val baos = new ByteArrayOutputStream() + val lpos = new LinePrefixOutputStream("PREFIX", baos) + val arr = "hello\nworld\n".getBytes() + lpos.write(arr) + lpos.flush() + + assert(baos.toString == "PREFIXhello\nPREFIXworld\n") + } + + test("ranges") { + for(str <- Seq("hello\nworld\n")){ + val arr = str.getBytes() + for(i1 <- Range(0, arr.length)) { + for (i2 <- Range(i1, arr.length)) { + for (i3 <- Range(i2, arr.length)) { + val baos = new ByteArrayOutputStream() + val lpos = new LinePrefixOutputStream("PREFIX", baos) + lpos.write(arr, 0, i1) + lpos.write(arr, i1, i2 - i1) + lpos.write(arr, i2, i3 - i2) + lpos.write(arr, i3, arr.length - i3) + lpos.flush() + assert(baos.toString == "PREFIXhello\nPREFIXworld\n") + } + } + } + } + + + + } + } +} From fd3acf9147c5fe80a2879b22667a6b35df69725d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Sep 2024 19:05:40 +0800 Subject: [PATCH 083/116] . --- main/util/src/mill/util/LinePrefixOutputStream.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 772a359e69a..531d4963fbd 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -1,6 +1,5 @@ package mill.util -import mill.main.client.DebugLog import java.io.{ByteArrayOutputStream, OutputStream} From 47040136f1d108cee342a584c50b7091a89f6667 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 07:58:36 +0800 Subject: [PATCH 084/116] task log headers --- build.mill | 2 +- main/api/src/mill/api/Logger.scala | 4 +++ main/eval/src/mill/eval/EvaluatorCore.scala | 16 ++++++--- main/eval/src/mill/eval/GroupEvaluator.scala | 13 +++++--- main/util/src/mill/util/DummyLogger.scala | 1 + main/util/src/mill/util/FileLogger.scala | 1 - .../mill/util/LinePrefixOutputStream.scala | 4 ++- main/util/src/mill/util/MultiLogger.scala | 9 +++++ .../src/mill/util/MultilinePromptLogger.scala | 29 +++++++++++++--- main/util/src/mill/util/PrefixLogger.scala | 33 ++++++++++++++----- main/util/src/mill/util/ProxyLogger.scala | 2 ++ 11 files changed, 88 insertions(+), 26 deletions(-) diff --git a/build.mill b/build.mill index 0095d56e48d..2be5f92276c 100644 --- a/build.mill +++ b/build.mill @@ -20,7 +20,7 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload -//import $packages._ +import $packages._ object Settings { val pomOrg = "com.lihaoyi" val githubOrg = "com-lihaoyi" diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 93e1ab795b6..520baf24c2d 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,6 +44,10 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit + def reportPrefix(s: String): Unit = () + def ticker(identifier: String, + identSuffix: String, + message: String): Unit = ticker(s"$identifier $message") def globalTicker(s: String): Unit = () def clearAllTickers(): Unit = () def endTicker(): Unit = () diff --git a/main/eval/src/mill/eval/EvaluatorCore.scala b/main/eval/src/mill/eval/EvaluatorCore.scala index 8e75460e870..9aeb68e470f 100644 --- a/main/eval/src/mill/eval/EvaluatorCore.scala +++ b/main/eval/src/mill/eval/EvaluatorCore.scala @@ -101,7 +101,15 @@ private[mill] trait EvaluatorCore extends GroupEvaluator { for (terminal <- terminals) { val deps = interGroupDeps(terminal) futures(terminal) = Future.sequence(deps.map(futures)).map { upstreamValues => - val counterMsg = s"${count.getAndIncrement()}/${terminals.size}" + val paddedCount = count + .getAndIncrement() + .toString + .reverse + .padTo(terminals.size.toString.length, '0') + .reverse + val countMsg = s"[$paddedCount]" + + val counterMsg = s"[$paddedCount/${terminals0.size}]" logger.globalTicker(counterMsg) if (failed.get()) None else { @@ -111,11 +119,10 @@ private[mill] trait EvaluatorCore extends GroupEvaluator { .toMap val startTime = System.nanoTime() / 1000 - val threadId = threadNumberer.getThreadId(Thread.currentThread()) val contextLogger = PrefixLogger( out = logger, - context = contextLoggerMsg(threadId), + context = countMsg, tickerContext = GroupEvaluator.dynamicTickerPrefix.value ) @@ -123,7 +130,8 @@ private[mill] trait EvaluatorCore extends GroupEvaluator { terminal = terminal, group = sortedGroups.lookupKey(terminal), results = upstreamResults, - counterMsg = counterMsg, + counterMsg = countMsg, + identSuffix = counterMsg, zincProblemReporter = reporter, testReporter = testReporter, logger = contextLogger, diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 5b36d6cd2d7..da70ea9e1bb 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -50,6 +50,7 @@ private[mill] trait GroupEvaluator { group: Agg[Task[_]], results: Map[Task[_], TaskResult[(Val, Int)]], counterMsg: String, + identSuffix: String, zincProblemReporter: Int => Option[CompileProblemReporter], testReporter: TestReporter, logger: ColorLogger, @@ -64,6 +65,7 @@ private[mill] trait GroupEvaluator { val sideHashes = MurmurHash3.orderedHash(group.iterator.map(_.sideHash)) + val scriptsHash = if (disableCallgraph) 0 else group @@ -114,6 +116,7 @@ private[mill] trait GroupEvaluator { } ) + methodCodeHashSignatures.get(expectedName) ++ constructorHashes } .flatten @@ -130,10 +133,10 @@ private[mill] trait GroupEvaluator { paths = None, maybeTargetLabel = None, counterMsg = counterMsg, + identSuffix = identSuffix, zincProblemReporter, testReporter, logger, - terminal.task.asWorker.nonEmpty ) GroupEvaluator.Results(newResults, newEvaluated.toSeq, null, inputsHash, -1) @@ -180,10 +183,10 @@ private[mill] trait GroupEvaluator { paths = Some(paths), maybeTargetLabel = Some(targetLabel), counterMsg = counterMsg, + identSuffix = identSuffix, zincProblemReporter, testReporter, logger, - terminal.task.asWorker.nonEmpty ) } @@ -221,10 +224,10 @@ private[mill] trait GroupEvaluator { paths: Option[EvaluatorPaths], maybeTargetLabel: Option[String], counterMsg: String, + identSuffix: String, reporter: Int => Option[CompileProblemReporter], testReporter: TestReporter, logger: mill.api.Logger, - isWorker: Boolean ): (Map[Task[_], TaskResult[(Val, Int)]], mutable.Buffer[Task[_]]) = { def computeAll(enableTicker: Boolean) = { @@ -248,14 +251,14 @@ private[mill] trait GroupEvaluator { def withTicker[T](s: Option[String])(t: => T): T = s match { case None => t case Some(s) => - logger.ticker(s) + logger.ticker(counterMsg, identSuffix, s) try t finally logger.endTicker() } withTicker(tickerPrefix) { val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { override def ticker(s: String): Unit = { - if (enableTicker) super.ticker(tickerPrefix.map(_ + " ").getOrElse("") + s) + if (enableTicker) super.ticker(s) else () // do nothing } diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index 14685983ea5..5249a1759ad 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -18,5 +18,6 @@ object DummyLogger extends Logger { def error(s: String) = () def ticker(s: String) = () def debug(s: String) = () + override def reportPrefix(s: String) = () override val debugEnabled: Boolean = false } diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index f887bc49760..1ed3bbaf962 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -53,6 +53,5 @@ class FileLogger( if (outputStreamUsed) outputStream.close() } - override def rawOutputStream: PrintStream = outputStream } diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 531d4963fbd..c59725e7a28 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -13,7 +13,8 @@ import java.io.{ByteArrayOutputStream, OutputStream} */ class LinePrefixOutputStream( linePrefix: String, - out: OutputStream + out: OutputStream, + reportPrefix: () => Unit ) extends OutputStream { private[this] val linePrefixBytes = linePrefix.getBytes("UTF-8") @@ -25,6 +26,7 @@ class LinePrefixOutputStream( if (isNewLine && linePrefixNonEmpty) { isNewLine = false buffer.write(linePrefixBytes) + reportPrefix() } } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 644d246031d..8d8d8de99fb 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -31,6 +31,11 @@ class MultiLogger( logger2.ticker(s) } + override def ticker(identifier: String, identSuffix: String, message: String): Unit = { + logger1.ticker(identifier, identSuffix, message) + logger2.ticker(identifier, identSuffix, message) + } + def debug(s: String): Unit = { logger1.debug(s) logger2.debug(s) @@ -40,6 +45,10 @@ class MultiLogger( logger1.close() logger2.close() } + override def reportPrefix(s: String): Unit = { + logger1.reportPrefix(s) + logger2.reportPrefix(s) + } override def rawOutputStream: PrintStream = systemStreams.out diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 4d26c3c30f9..e3cdf760e86 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -75,6 +75,22 @@ private[mill] class MultilinePromptLogger( def ticker(s: String): Unit = synchronized { state.updateCurrent(Some(s)) } + override def reportPrefix(s: String): Unit = synchronized { + if (!reportedIdentifiers(s)) { + reportedIdentifiers.add(s) + for ((identSuffix, message) <- seenIdentifiers.get(s)) { + systemStreams.err.println(infoColor(s"$identSuffix $message")) + } + } + } + + private val seenIdentifiers = collection.mutable.Map.empty[String, (String, String)] + private val reportedIdentifiers = collection.mutable.Set.empty[String] + override def ticker(identifier: String, identSuffix: String, message: String): Unit = synchronized { + seenIdentifiers(identifier) = (identSuffix, message) + super.ticker(infoColor(identifier).toString(), identSuffix, message) + + } def debug(s: String): Unit = synchronized { if (debugEnabled) systemStreams.err.println(s) } override def rawOutputStream: PrintStream = systemStreams0.out @@ -229,7 +245,8 @@ private object MultilinePromptLogger { headerPrefix, titleText, statuses, - interactive = consoleDims()._1.nonEmpty + interactive = consoleDims()._1.nonEmpty, + ending = ending ) val currentPromptStr = @@ -308,7 +325,8 @@ private object MultilinePromptLogger { headerPrefix: String, titleText: String, statuses: collection.SortedMap[Int, Status], - interactive: Boolean + interactive: Boolean, + ending: Boolean = false ): List[String] = { // -1 to leave a bit of buffer val maxWidth = consoleWidth - 1 @@ -316,7 +334,7 @@ private object MultilinePromptLogger { val maxHeight = math.max(1, consoleHeight / 3 - 1) val headerSuffix = renderSeconds(now - startTimeMillis) - val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth) + val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth, ending) val body0 = statuses .map { case (threadId, status) => @@ -354,9 +372,10 @@ private object MultilinePromptLogger { headerPrefix0: String, titleText0: String, headerSuffix0: String, - maxWidth: Int + maxWidth: Int, + ending: Boolean = false ): String = { - val headerPrefixStr = s" $headerPrefix0 " + val headerPrefixStr = if (ending) s"$headerPrefix0 " else s" $headerPrefix0 " val headerSuffixStr = s" $headerSuffix0" val titleText = s" $titleText0 " // -12 just to ensure we always have some ==== divider on each side of the title diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 054d2c6253b..00b4db004c4 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -8,12 +8,12 @@ import java.io.PrintStream class PrefixLogger( val logger0: ColorLogger, - context: String, + context0: String, tickerContext: String = "", outStream0: Option[PrintStream] = None, errStream0: Option[PrintStream] = None ) extends ColorLogger { - + val context = if(context0 == "") "" else context0 + " " override def toString = s"PrefixLogger($logger0, ${literalize(context)}, ${literalize(tickerContext)})" def this(logger0: ColorLogger, context: String, tickerContext: String) = this(logger0, context, tickerContext, None, None) @@ -27,13 +27,15 @@ class PrefixLogger( out = outStream0.getOrElse( new PrintStream(new LinePrefixOutputStream( infoColor(context).render, - logger0.systemStreams.out + logger0.systemStreams.out, + () => reportPrefix(context0) )) ), err = errStream0.getOrElse( new PrintStream(new LinePrefixOutputStream( infoColor(context).render, - logger0.systemStreams.err + logger0.systemStreams.err, + () => reportPrefix(context0) )) ), logger0.systemStreams.in @@ -41,19 +43,32 @@ class PrefixLogger( override def rawOutputStream = logger0.rawOutputStream - override def info(s: String): Unit = logger0.info(context + s) - override def error(s: String): Unit = logger0.error(context + s) + override def info(s: String): Unit = { + reportPrefix(context0) + logger0.info(infoColor(context) + s) + } + override def error(s: String): Unit = { + reportPrefix(context0) + logger0.error(infoColor(context) + s) + } override def ticker(s: String): Unit = logger0.ticker(context + tickerContext + s) - override def debug(s: String): Unit = logger0.debug(context + s) + override def ticker(identifier: String, identSuffix: String, message: String): Unit = logger0.ticker(identifier, identSuffix, message) + override def debug(s: String): Unit = { + reportPrefix(context0) + logger0.debug(infoColor(context) + s) + } override def debugEnabled: Boolean = logger0.debugEnabled override def withOutStream(outStream: PrintStream): PrefixLogger = new PrefixLogger( logger0.withOutStream(outStream), - context, - tickerContext, + infoColor(context).toString(), + infoColor(tickerContext).toString(), outStream0 = Some(outStream), errStream0 = Some(systemStreams.err) ) + override def reportPrefix(s: String) = { + logger0.reportPrefix(s) + } override def endTicker(): Unit = logger0.endTicker() override def globalTicker(s: String): Unit = logger0.globalTicker(s) } diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index db3f556673d..440ec40530f 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -17,11 +17,13 @@ class ProxyLogger(logger: Logger) extends Logger { def info(s: String): Unit = logger.info(s) def error(s: String): Unit = logger.error(s) def ticker(s: String): Unit = logger.ticker(s) + override def ticker(identifier: String, identSuffix: String, message: String): Unit = logger.ticker(identifier, identSuffix, message) def debug(s: String): Unit = logger.debug(s) override def debugEnabled: Boolean = logger.debugEnabled override def close(): Unit = logger.close() + override def reportPrefix(s: String): Unit = logger.reportPrefix(s) override def rawOutputStream: PrintStream = logger.rawOutputStream override def endTicker(): Unit = logger.endTicker() From 6dd4e17a2df1bb2179314553c09888ddb6718905 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 08:20:38 +0800 Subject: [PATCH 085/116] . --- build.mill | 2 +- main/util/src/mill/util/LinePrefixOutputStream.scala | 4 ++-- main/util/src/mill/util/PrefixLogger.scala | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.mill b/build.mill index 2be5f92276c..0095d56e48d 100644 --- a/build.mill +++ b/build.mill @@ -20,7 +20,7 @@ import mill.define.Cross import $meta._ import $file.ci.shared import $file.ci.upload -import $packages._ +//import $packages._ object Settings { val pomOrg = "com.lihaoyi" val githubOrg = "com-lihaoyi" diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index c59725e7a28..430b8449c71 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -26,12 +26,12 @@ class LinePrefixOutputStream( if (isNewLine && linePrefixNonEmpty) { isNewLine = false buffer.write(linePrefixBytes) - reportPrefix() } } def writeOutBuffer() = { - out.synchronized { out.write(buffer.toByteArray) } + if (buffer.size() > 0) reportPrefix() + out.synchronized { buffer.writeTo(out) } buffer.reset() } diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 00b4db004c4..2ecb0e9be6e 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -54,7 +54,7 @@ class PrefixLogger( override def ticker(s: String): Unit = logger0.ticker(context + tickerContext + s) override def ticker(identifier: String, identSuffix: String, message: String): Unit = logger0.ticker(identifier, identSuffix, message) override def debug(s: String): Unit = { - reportPrefix(context0) + if (debugEnabled) reportPrefix(context0) logger0.debug(infoColor(context) + s) } override def debugEnabled: Boolean = logger0.debugEnabled From 0cce6b1fecb84bdc841ac0af95adaede28e4b00f Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 10:37:47 +0800 Subject: [PATCH 086/116] split out MultilinePromptLoggerUtil --- .../src/mill/main/client/ServerFiles.java | 2 +- .../mill/util/LinePrefixOutputStream.scala | 2 +- .../mill/util/MultiLinePromptLoggerUtil.scala | 169 +++++++++ .../src/mill/util/MultilinePromptLogger.scala | 188 +--------- .../util/MultilinePromptLoggerTests.scala | 325 +----------------- .../util/MultilinePromptLoggerUtilTests.scala | 320 +++++++++++++++++ 6 files changed, 522 insertions(+), 484 deletions(-) create mode 100644 main/util/src/mill/util/MultiLinePromptLoggerUtil.scala create mode 100644 main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala diff --git a/main/client/src/mill/main/client/ServerFiles.java b/main/client/src/mill/main/client/ServerFiles.java index 19f6efd19ad..bd7ac1a289c 100644 --- a/main/client/src/mill/main/client/ServerFiles.java +++ b/main/client/src/mill/main/client/ServerFiles.java @@ -1,7 +1,7 @@ package mill.main.client; /** - * Central place containing all the files that live inside the `out/mill-serer-*` folder + * Central place containing all the files that live inside the `out/mill-server-*` folder * and documentation about what they do */ public class ServerFiles { diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 430b8449c71..e2cfd59c675 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -16,7 +16,7 @@ class LinePrefixOutputStream( out: OutputStream, reportPrefix: () => Unit ) extends OutputStream { - + def this(linePrefix: String, out: OutputStream) = this(linePrefix, out, () => ()) private[this] val linePrefixBytes = linePrefix.getBytes("UTF-8") private[this] val linePrefixNonEmpty = linePrefixBytes.length != 0 private[this] var isNewLine = true diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala new file mode 100644 index 00000000000..ec5ddb63cf7 --- /dev/null +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -0,0 +1,169 @@ +package mill.util + +private object MultilinePromptLoggerUtil { + + private[mill] val defaultTermWidth = 119 + private[mill] val defaultTermHeight = 50 + + /** + * How often to update the multiline status prompt on the terminal. + * Too frequent is bad because it causes a lot of visual noise, + * but too infrequent results in latency. 10 times per second seems reasonable + */ + private[mill] val promptUpdateIntervalMillis = 100 + + /** + * How often to update the multiline status prompt in noninteractive scenarios, + * e.g. background job logs or piped to a log file. Much less frequent than the + * interactive scenario because we cannot rely on ANSI codes to over-write the + * previous prompt, so we have to be a lot more conservative to avoid spamming + * the logs, but we still want to print it occasionally so people can debug stuck + * background or CI jobs and see what tasks it is running when stuck + */ + private[mill] val nonInteractivePromptUpdateIntervalMillis = 60000 + + /** + * Add some extra latency delay to the process of removing an entry from the status + * prompt entirely, because removing an entry changes the height of the prompt, which + * is even more distracting than changing the contents of a line, so we want to minimize + * those occurrences even further. + */ + val statusRemovalHideDelayMillis = 500 + + /** + * How long to wait before actually removing the blank line left by a removed status + * and reducing the height of the prompt. Having the prompt change height is even more + * distracting than having entries in the prompt disappear, so give it a longer timeout + * so it happens less. + */ + val statusRemovalRemoveDelayMillis = 2000 + + private[mill] case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) + + private[mill] val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes + + private def renderSeconds(millis: Long) = (millis / 1000).toInt match { + case 0 => "" + case n => s"${n}s" + } + + def readTerminalDims(terminfoPath: os.Path): Option[(Option[Int], Option[Int])] = { + try { + val s"$termWidth0 $termHeight0" = os.read(terminfoPath) + Some( + Tuple2( + termWidth0.toInt match { + case -1 | 0 => None + case n => Some(n) + }, + termHeight0.toInt match { + case -1 | 0 => None + case n => Some(n) + } + ) + ) + } catch { case e => None } + } + + def renderPrompt( + consoleWidth: Int, + consoleHeight: Int, + now: Long, + startTimeMillis: Long, + headerPrefix: String, + titleText: String, + statuses: collection.SortedMap[Int, Status], + interactive: Boolean, + ending: Boolean = false + ): List[String] = { + // -1 to leave a bit of buffer + val maxWidth = consoleWidth - 1 + // -1 to account for header + val maxHeight = math.max(1, consoleHeight / 3 - 1) + val headerSuffix = renderSeconds(now - startTimeMillis) + + val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth, ending) + val body0 = statuses + .map { + case (threadId, status) => + if (now - status.removedTimeMillis > statusRemovalHideDelayMillis) "" + else splitShorten( + status.text + " " + renderSeconds(now - status.startTimeMillis), + maxWidth + ) + } + // For non-interactive jobs, we do not need to preserve the height of the prompt + // between renderings, since consecutive prompts do not appear at the same place + // in the log file. Thus we can aggressively remove all blank spacer lines + .filter(_.nonEmpty || interactive) + .toList + // Sort alphabetically because the `#nn` prefix is part of the string, and then + // put all empty strings last since those are less important and can be ignored + .sortBy(x => (x.isEmpty, x)) + + val nonEmptyBodyCount = body0.count(_.nonEmpty) + val body = + if (nonEmptyBodyCount <= maxHeight) body0.take(maxHeight) + else body0.take(maxHeight - 1) ++ Seq( + s"... and ${nonEmptyBodyCount - maxHeight + 1} more threads" + ) + + // For non-interactive jobs, the prompt won't be at the bottom of the terminal but + // will instead be in the middle of a big log file with logs above and below, so we + // need some kind of footer to tell the reader when the prompt ends and logs begin + val footer = Option.when(!interactive)("=" * maxWidth).toList + + header :: body ::: footer + } + + def renderHeader( + headerPrefix0: String, + titleText0: String, + headerSuffix0: String, + maxWidth: Int, + ending: Boolean = false + ): String = { + val headerPrefixStr = if (ending) s"$headerPrefix0 " else s" $headerPrefix0 " + val headerSuffixStr = s" $headerSuffix0" + val titleText = s" $titleText0 " + // -12 just to ensure we always have some ==== divider on each side of the title + val maxTitleLength = + maxWidth - math.max(headerPrefixStr.length, headerSuffixStr.length) * 2 - 12 + val shortenedTitle = splitShorten(titleText, maxTitleLength) + + // +2 to offset the title a bit to the right so it looks centered, as the `headerPrefixStr` + // is usually longer than `headerSuffixStr`. We use a fixed offset rather than dynamically + // offsetting by `headerPrefixStr.length` to prevent the title from shifting left and right + // as the `headerPrefixStr` changes, even at the expense of it not being perfectly centered. + val leftDivider = "=" * ((maxWidth / 2) - (titleText.length / 2) - headerPrefixStr.length + 2) + val rightDivider = + "=" * (maxWidth - headerPrefixStr.length - leftDivider.length - shortenedTitle.length - headerSuffixStr.length) + val headerString = + headerPrefixStr + leftDivider + shortenedTitle + rightDivider + headerSuffixStr + assert( + headerString.length == maxWidth, + s"${pprint.apply(headerString)} is length ${headerString.length}, requires $maxWidth" + ) + headerString + } + + def splitShorten(s: String, maxLength: Int): String = { + if (s.length <= maxLength) s + else { + val ellipses = "..." + val halfWidth = (maxLength - ellipses.length) / 2 + s.take(halfWidth) + ellipses + s.takeRight(halfWidth) + } + } + + def lastIndexOfNewline(b: Array[Byte], off: Int, len: Int): Int = { + var index = off + len - 1 + while (true) { + if (index < off) return -1 + else if (b(index) == '\n') return index + else index -= 1 + } + ??? + } +} + diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index e3cdf760e86..8b8064b8b14 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -2,9 +2,12 @@ package mill.util import mill.api.SystemStreams import mill.main.client.ProxyStream +import mill.util.MultilinePromptLoggerUtil.{Status, clearScreenToEndBytes, defaultTermHeight, defaultTermWidth, renderPrompt} import pprint.Util.literalize import java.io._ +import MultilinePromptLoggerUtil._ + private[mill] class MultilinePromptLogger( override val colored: Boolean, @@ -104,54 +107,14 @@ private[mill] class MultilinePromptLogger( def systemStreams = streams.systemStreams } -private object MultilinePromptLogger { - - private val defaultTermWidth = 119 - private val defaultTermHeight = 50 - - /** - * How often to update the multiline status prompt on the terminal. - * Too frequent is bad because it causes a lot of visual noise, - * but too infrequent results in latency. 10 times per second seems reasonable - */ - private val promptUpdateIntervalMillis = 100 - - /** - * How often to update the multiline status prompt in noninteractive scenarios, - * e.g. background job logs or piped to a log file. Much less frequent than the - * interactive scenario because we cannot rely on ANSI codes to over-write the - * previous prompt, so we have to be a lot more conservative to avoid spamming - * the logs, but we still want to print it occasionally so people can debug stuck - * background or CI jobs and see what tasks it is running when stuck - */ - private val nonInteractivePromptUpdateIntervalMillis = 60000 - - /** - * Add some extra latency delay to the process of removing an entry from the status - * prompt entirely, because removing an entry changes the height of the prompt, which - * is even more distracting than changing the contents of a line, so we want to minimize - * those occurrences even further. - */ - val statusRemovalHideDelayMillis = 500 - - /** - * How long to wait before actually removing the blank line left by a removed status - * and reducing the height of the prompt. Having the prompt change height is even more - * distracting than having entries in the prompt disappear, so give it a longer timeout - * so it happens less. - */ - val statusRemovalRemoveDelayMillis = 2000 - - private[mill] case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) - - private val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes +object MultilinePromptLogger{ private class Streams( - enableTicker: Boolean, - systemStreams0: SystemStreams, - currentPromptBytes: () => Array[Byte], - interactive: () => Boolean - ) { + enableTicker: Boolean, + systemStreams0: SystemStreams, + currentPromptBytes: () => Array[Byte], + interactive: () => Boolean + ) { // We force both stdout and stderr streams into a single `Piped*Stream` pair via // `ProxyStream`, as we need to preserve the ordering of writes to each individual @@ -208,11 +171,11 @@ private object MultilinePromptLogger { } } private class State( - titleText: String, - systemStreams0: SystemStreams, - startTimeMillis: Long, - consoleDims: () => (Option[Int], Option[Int]) - ) { + titleText: String, + systemStreams0: SystemStreams, + startTimeMillis: Long, + consoleDims: () => (Option[Int], Option[Int]) + ) { private var lastRenderedPromptHash = 0 private val statuses = collection.mutable.SortedMap.empty[Int, Status] @@ -294,127 +257,4 @@ private object MultilinePromptLogger { } } - private def renderSeconds(millis: Long) = (millis / 1000).toInt match { - case 0 => "" - case n => s"${n}s" - } - - def readTerminalDims(terminfoPath: os.Path): Option[(Option[Int], Option[Int])] = { - try { - val s"$termWidth0 $termHeight0" = os.read(terminfoPath) - Some( - Tuple2( - termWidth0.toInt match { - case -1 | 0 => None - case n => Some(n) - }, - termHeight0.toInt match { - case -1 | 0 => None - case n => Some(n) - } - ) - ) - } catch { case e => None } - } - - def renderPrompt( - consoleWidth: Int, - consoleHeight: Int, - now: Long, - startTimeMillis: Long, - headerPrefix: String, - titleText: String, - statuses: collection.SortedMap[Int, Status], - interactive: Boolean, - ending: Boolean = false - ): List[String] = { - // -1 to leave a bit of buffer - val maxWidth = consoleWidth - 1 - // -1 to account for header - val maxHeight = math.max(1, consoleHeight / 3 - 1) - val headerSuffix = renderSeconds(now - startTimeMillis) - - val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth, ending) - val body0 = statuses - .map { - case (threadId, status) => - if (now - status.removedTimeMillis > statusRemovalHideDelayMillis) "" - else splitShorten( - status.text + " " + renderSeconds(now - status.startTimeMillis), - maxWidth - ) - } - // For non-interactive jobs, we do not need to preserve the height of the prompt - // between renderings, since consecutive prompts do not appear at the same place - // in the log file. Thus we can aggressively remove all blank spacer lines - .filter(_.nonEmpty || interactive) - .toList - // Sort alphabetically because the `#nn` prefix is part of the string, and then - // put all empty strings last since those are less important and can be ignored - .sortBy(x => (x.isEmpty, x)) - - val nonEmptyBodyCount = body0.count(_.nonEmpty) - val body = - if (nonEmptyBodyCount <= maxHeight) body0.take(maxHeight) - else body0.take(maxHeight - 1) ++ Seq( - s"... and ${nonEmptyBodyCount - maxHeight + 1} more threads" - ) - - // For non-interactive jobs, the prompt won't be at the bottom of the terminal but - // will instead be in the middle of a big log file with logs above and below, so we - // need some kind of footer to tell the reader when the prompt ends and logs begin - val footer = Option.when(!interactive)("=" * maxWidth).toList - - header :: body ::: footer - } - - def renderHeader( - headerPrefix0: String, - titleText0: String, - headerSuffix0: String, - maxWidth: Int, - ending: Boolean = false - ): String = { - val headerPrefixStr = if (ending) s"$headerPrefix0 " else s" $headerPrefix0 " - val headerSuffixStr = s" $headerSuffix0" - val titleText = s" $titleText0 " - // -12 just to ensure we always have some ==== divider on each side of the title - val maxTitleLength = - maxWidth - math.max(headerPrefixStr.length, headerSuffixStr.length) * 2 - 12 - val shortenedTitle = splitShorten(titleText, maxTitleLength) - - // +2 to offset the title a bit to the right so it looks centered, as the `headerPrefixStr` - // is usually longer than `headerSuffixStr`. We use a fixed offset rather than dynamically - // offsetting by `headerPrefixStr.length` to prevent the title from shifting left and right - // as the `headerPrefixStr` changes, even at the expense of it not being perfectly centered. - val leftDivider = "=" * ((maxWidth / 2) - (titleText.length / 2) - headerPrefixStr.length + 2) - val rightDivider = - "=" * (maxWidth - headerPrefixStr.length - leftDivider.length - shortenedTitle.length - headerSuffixStr.length) - val headerString = - headerPrefixStr + leftDivider + shortenedTitle + rightDivider + headerSuffixStr - assert( - headerString.length == maxWidth, - s"${pprint.apply(headerString)} is length ${headerString.length}, requires $maxWidth" - ) - headerString - } - - def splitShorten(s: String, maxLength: Int): String = { - if (s.length <= maxLength) s - else { - val ellipses = "..." - val halfWidth = (maxLength - ellipses.length) / 2 - s.take(halfWidth) + ellipses + s.takeRight(halfWidth) - } - } - - def lastIndexOfNewline(b: Array[Byte], off: Int, len: Int): Int = { - var index = off + len - 1 - while (true) { - if (index < off) return -1 - else if (b(index) == '\n') return index - else index -= 1 - } - ??? - } } diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 6ef776a8c31..4f00904e88e 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -1,321 +1,30 @@ package mill.util +import mill.api.SystemStreams +import mill.main.client.ProxyStream import utest._ -import scala.collection.immutable.SortedMap +import java.io.{ByteArrayOutputStream, PrintStream} object MultilinePromptLoggerTests extends TestSuite { val tests = Tests { - test("lastIndexOfNewline") { - // Fuzz test to make sure our custom fast `lastIndexOfNewline` logic behaves - // the same as a slower generic implementation using `.slice.lastIndexOf` - val allSampleByteArrays = Seq[Array[Byte]]( - Array(1), - Array('\n'), - Array(1, 1), - Array(1, '\n'), - Array('\n', 1), - Array('\n', '\n'), - Array(1, 1, 1), - Array(1, 1, '\n'), - Array(1, '\n', 1), - Array('\n', 1, 1), - Array(1, '\n', '\n'), - Array('\n', 1, '\n'), - Array('\n', '\n', 1), - Array('\n', '\n', '\n'), - Array(1, 1, 1, 1), - Array(1, 1, 1, '\n'), - Array(1, 1, '\n', 1), - Array(1, '\n', 1, 1), - Array('\n', 1, 1, 1), - Array(1, 1, '\n', '\n'), - Array(1, '\n', '\n', 1), - Array('\n', '\n', 1, 1), - Array(1, '\n', 1, '\n'), - Array('\n', 1, '\n', 1), - Array('\n', 1, 1, '\n'), - Array('\n', '\n', '\n', 1), - Array('\n', '\n', 1, '\n'), - Array('\n', 1, '\n', '\n'), - Array(1, '\n', '\n', '\n'), - Array('\n', '\n', '\n', '\n') + test { + val baos = new ByteArrayOutputStream() + val baosOut = new PrintStream(new ProxyStream.Output(baos, ProxyStream.OUT)) + val baosErr = new PrintStream(new ProxyStream.Output(baos, ProxyStream.ERR)) + val logger = new MultilinePromptLogger( + colored = false, + enableTicker = true, + infoColor = fansi.Attrs.Empty, + errorColor = fansi.Attrs.Empty, + systemStreams0 = new SystemStreams(baosOut, baosErr, System.in), + debugEnabled = false, + titleText = "TITLE", + terminfoPath = os.temp() ) - for (sample <- allSampleByteArrays) { - for (start <- Range(0, sample.length)) { - for (len <- Range(0, sample.length - start)) { - val found = MultilinePromptLogger.lastIndexOfNewline(sample, start, len) - val expected0 = sample.slice(start, start + len).lastIndexOf('\n') - val expected = expected0 + start - def assertMsg = - s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" - if (expected0 == -1) Predef.assert(found == -1, assertMsg) - else Predef.assert(found == expected, assertMsg) - } - } - } - } - test("renderHeader") { - import MultilinePromptLogger.renderHeader - - def check(prefix: String, title: String, suffix: String, maxWidth: Int, expected: String) = { - val rendered = renderHeader(prefix, title, suffix, maxWidth) - // leave two spaces open on the left so there's somewhere to park the cursor - assert(expected == rendered) - assert(rendered.length == maxWidth) - rendered - } - test("simple") - check( - "PREFIX", - "TITLE", - "SUFFIX", - 60, - expected = " PREFIX ==================== TITLE ================= SUFFIX" - ) - - test("short") - check( - "PREFIX", - "TITLE", - "SUFFIX", - 40, - expected = " PREFIX ========== TITLE ======= SUFFIX" - ) - - test("shorter") - check( - "PREFIX", - "TITLE", - "SUFFIX", - 25, - expected = " PREFIX ==...==== SUFFIX" - ) - - test("truncateTitle") - check( - "PREFIX", - "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "SUFFIX", - 60, - expected = " PREFIX ====== TITLE_ABCDEF...OPQRSTUVWXYZ ========= SUFFIX" - ) - - test("asymmetricTruncateTitle") - check( - "PREFIX_LONG", - "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "SUFFIX", - 60, - expected = " PREFIX_LONG = TITLE_A...TUVWXYZ =================== SUFFIX" - ) - } - - test("renderPrompt") { - import MultilinePromptLogger._ - val now = System.currentTimeMillis() - test("simple") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile", - statuses = SortedMap( - 0 -> Status(now - 1000, "hello", Long.MaxValue), - 1 -> Status(now - 2000, "world", Long.MaxValue) - ), - interactive = true - ) - val expected = List( - " 123/456 =============== __.compile ================ 1337s", - "hello 1s", - "world 2s" - ) - assert(rendered == expected) - } - - test("maxWithoutTruncation") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmn", - statuses = SortedMap( - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123", - Long.MaxValue - ), - 1 -> Status(now - 2000, "#2 world", Long.MaxValue), - 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue) - ), - interactive = true - ) - - val expected = List( - " 123/456 ======== __.compile.abcdefghijklmn ======== 1337s", - "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123 1s", - "#2 world 2s", - "#3 i am cow 3s", - "#4 hear me moo 4s", - "#5 i weigh twice as much as you 5s" - ) - assert(rendered == expected) - } - test("minAfterTruncation") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmno", - statuses = SortedMap( - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz12345678901234", - Long.MaxValue - ), - 1 -> Status(now - 2000, "#2 world", Long.MaxValue), - 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), - 5 -> Status(now - 6000, "#6 and I look good on the barbecue", Long.MaxValue) - ), - interactive = true - ) - - val expected = List( - " 123/456 ======= __.compile....efghijklmno ========= 1337s", - "#1 hello1234567890abcefghijk...pqrstuvwxyz12345678901234 1s", - "#2 world 2s", - "#3 i am cow 3s", - "#4 hear me moo 4s", - "... and 2 more threads" - ) - assert(rendered == expected) - } - - test("truncated") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", - statuses = SortedMap( - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, - Long.MaxValue - ), - 1 -> Status(now - 2000, "#2 world", Long.MaxValue), - 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), - 5 -> Status(now - 6000, "#6 and i look good on the barbecue", Long.MaxValue), - 6 -> Status(now - 7000, "#7 yoghurt curds cream cheese and butter", Long.MaxValue) - ), - interactive = true - ) - val expected = List( - " 123/456 __.compile....z1234567890 ================ 1337s", - "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", - "#2 world 2s", - "#3 i am cow 3s", - "#4 hear me moo 4s", - "... and 3 more threads" - ) - assert(rendered == expected) - } - - test("removalDelay") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 23, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", - statuses = SortedMap( - // Not yet removed, should be shown - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, - Long.MaxValue - ), - // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), - 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), - // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not - // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), - 4 -> Status(now - 5000, "#5i weigh twice", now - statusRemovalRemoveDelayMillis + 1), - 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1), - 6 -> Status( - now - 7000, - "#7 and I look good on the barbecue", - now - statusRemovalRemoveDelayMillis + 1 - ) - ), - interactive = true - ) - - val expected = List( - " 123/456 __.compile....z1234567890 ================ 1337s", - "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", - "#2 world 2s", - "#3 i am cow 3s", - "", - "", - "" - ) - - assert(rendered == expected) - } - - test("nonInteractive") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 23, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", - statuses = SortedMap( - // Not yet removed, should be shown - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, - Long.MaxValue - ), - // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), - 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), - // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not - // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), - 4 -> Status(now - 5000, "#5 i weigh twice", now - statusRemovalRemoveDelayMillis + 1), - 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1) - ), - interactive = false - ) - - // Make sure the non-interactive prompt does not show the blank lines, - // and it contains a footer line to mark the end of the prompt in logs - val expected = List( - " 123/456 __.compile....z1234567890 ================ 1337s", - "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", - "#2 world 2s", - "#3 i am cow 3s", - "===========================================================" - ) - assert(rendered == expected) - } +// logger. } } } diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala new file mode 100644 index 00000000000..7fa063d2c19 --- /dev/null +++ b/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala @@ -0,0 +1,320 @@ +package mill.util + +import utest._ + +import scala.collection.immutable.SortedMap +import MultilinePromptLoggerUtil._ +object MultilinePromptLoggerUtilTests extends TestSuite { + + val tests = Tests { + test("lastIndexOfNewline") { + // Fuzz test to make sure our custom fast `lastIndexOfNewline` logic behaves + // the same as a slower generic implementation using `.slice.lastIndexOf` + val allSampleByteArrays = Seq[Array[Byte]]( + Array(1), + Array('\n'), + Array(1, 1), + Array(1, '\n'), + Array('\n', 1), + Array('\n', '\n'), + Array(1, 1, 1), + Array(1, 1, '\n'), + Array(1, '\n', 1), + Array('\n', 1, 1), + Array(1, '\n', '\n'), + Array('\n', 1, '\n'), + Array('\n', '\n', 1), + Array('\n', '\n', '\n'), + Array(1, 1, 1, 1), + Array(1, 1, 1, '\n'), + Array(1, 1, '\n', 1), + Array(1, '\n', 1, 1), + Array('\n', 1, 1, 1), + Array(1, 1, '\n', '\n'), + Array(1, '\n', '\n', 1), + Array('\n', '\n', 1, 1), + Array(1, '\n', 1, '\n'), + Array('\n', 1, '\n', 1), + Array('\n', 1, 1, '\n'), + Array('\n', '\n', '\n', 1), + Array('\n', '\n', 1, '\n'), + Array('\n', 1, '\n', '\n'), + Array(1, '\n', '\n', '\n'), + Array('\n', '\n', '\n', '\n') + ) + + for (sample <- allSampleByteArrays) { + for (start <- Range(0, sample.length)) { + for (len <- Range(0, sample.length - start)) { + val found = lastIndexOfNewline(sample, start, len) + val expected0 = sample.slice(start, start + len).lastIndexOf('\n') + val expected = expected0 + start + def assertMsg = + s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" + if (expected0 == -1) Predef.assert(found == -1, assertMsg) + else Predef.assert(found == expected, assertMsg) + } + } + } + } + test("renderHeader") { + + def check(prefix: String, title: String, suffix: String, maxWidth: Int, expected: String) = { + val rendered = renderHeader(prefix, title, suffix, maxWidth) + // leave two spaces open on the left so there's somewhere to park the cursor + assert(expected == rendered) + assert(rendered.length == maxWidth) + rendered + } + test("simple") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 60, + expected = " PREFIX ==================== TITLE ================= SUFFIX" + ) + + test("short") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 40, + expected = " PREFIX ========== TITLE ======= SUFFIX" + ) + + test("shorter") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 25, + expected = " PREFIX ==...==== SUFFIX" + ) + + test("truncateTitle") - check( + "PREFIX", + "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "SUFFIX", + 60, + expected = " PREFIX ====== TITLE_ABCDEF...OPQRSTUVWXYZ ========= SUFFIX" + ) + + test("asymmetricTruncateTitle") - check( + "PREFIX_LONG", + "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "SUFFIX", + 60, + expected = " PREFIX_LONG = TITLE_A...TUVWXYZ =================== SUFFIX" + ) + } + + test("renderPrompt") { + import MultilinePromptLogger._ + val now = System.currentTimeMillis() + test("simple") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile", + statuses = SortedMap( + 0 -> Status(now - 1000, "hello", Long.MaxValue), + 1 -> Status(now - 2000, "world", Long.MaxValue) + ), + interactive = true + ) + val expected = List( + " 123/456 =============== __.compile ================ 1337s", + "hello 1s", + "world 2s" + ) + assert(rendered == expected) + } + + test("maxWithoutTruncation") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmn", + statuses = SortedMap( + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123", + Long.MaxValue + ), + 1 -> Status(now - 2000, "#2 world", Long.MaxValue), + 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue) + ), + interactive = true + ) + + val expected = List( + " 123/456 ======== __.compile.abcdefghijklmn ======== 1337s", + "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", + "#5 i weigh twice as much as you 5s" + ) + assert(rendered == expected) + } + test("minAfterTruncation") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmno", + statuses = SortedMap( + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz12345678901234", + Long.MaxValue + ), + 1 -> Status(now - 2000, "#2 world", Long.MaxValue), + 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), + 5 -> Status(now - 6000, "#6 and I look good on the barbecue", Long.MaxValue) + ), + interactive = true + ) + + val expected = List( + " 123/456 ======= __.compile....efghijklmno ========= 1337s", + "#1 hello1234567890abcefghijk...pqrstuvwxyz12345678901234 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", + "... and 2 more threads" + ) + assert(rendered == expected) + } + + test("truncated") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", + statuses = SortedMap( + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, + Long.MaxValue + ), + 1 -> Status(now - 2000, "#2 world", Long.MaxValue), + 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), + 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), + 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), + 5 -> Status(now - 6000, "#6 and i look good on the barbecue", Long.MaxValue), + 6 -> Status(now - 7000, "#7 yoghurt curds cream cheese and butter", Long.MaxValue) + ), + interactive = true + ) + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", + "... and 3 more threads" + ) + assert(rendered == expected) + } + + test("removalDelay") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 23, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", + statuses = SortedMap( + // Not yet removed, should be shown + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, + Long.MaxValue + ), + // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown + 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), + 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), + // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not + // yet removed, so rendered as blank lines to prevent terminal jumping around too much + 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), + 4 -> Status(now - 5000, "#5i weigh twice", now - statusRemovalRemoveDelayMillis + 1), + 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1), + 6 -> Status( + now - 7000, + "#7 and I look good on the barbecue", + now - statusRemovalRemoveDelayMillis + 1 + ) + ), + interactive = true + ) + + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "", + "", + "" + ) + + assert(rendered == expected) + } + + test("nonInteractive") { + val rendered = renderPrompt( + consoleWidth = 60, + consoleHeight = 23, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", + statuses = SortedMap( + // Not yet removed, should be shown + 0 -> Status( + now - 1000, + "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, + Long.MaxValue + ), + // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown + 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), + 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), + // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not + // yet removed, so rendered as blank lines to prevent terminal jumping around too much + 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), + 4 -> Status(now - 5000, "#5 i weigh twice", now - statusRemovalRemoveDelayMillis + 1), + 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1) + ), + interactive = false + ) + + // Make sure the non-interactive prompt does not show the blank lines, + // and it contains a footer line to mark the end of the prompt in logs + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "===========================================================" + ) + assert(rendered == expected) + } + } + } +} From f4dbbe082f3409cd3b94ef89f8c39734d76471f0 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 11:15:55 +0800 Subject: [PATCH 087/116] add simple integration test for MultilinePromptLogger --- .../mill/util/MultiLinePromptLoggerUtil.scala | 7 ++- .../src/mill/util/MultilinePromptLogger.scala | 18 +++--- .../util/MultilinePromptLoggerTests.scala | 59 +++++++++++++++++-- runner/src/mill/runner/MillMain.scala | 3 +- 4 files changed, 71 insertions(+), 16 deletions(-) diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala index ec5ddb63cf7..c8ec03ebc52 100644 --- a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -82,7 +82,7 @@ private object MultilinePromptLoggerUtil { val maxHeight = math.max(1, consoleHeight / 3 - 1) val headerSuffix = renderSeconds(now - startTimeMillis) - val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth, ending) + val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth, ending, interactive) val body0 = statuses .map { case (threadId, status) => @@ -121,9 +121,10 @@ private object MultilinePromptLoggerUtil { titleText0: String, headerSuffix0: String, maxWidth: Int, - ending: Boolean = false + ending: Boolean = false, + interactive: Boolean = true ): String = { - val headerPrefixStr = if (ending) s"$headerPrefix0 " else s" $headerPrefix0 " + val headerPrefixStr = if (!interactive || ending) s"$headerPrefix0 " else s" $headerPrefix0 " val headerSuffixStr = s" $headerSuffix0" val titleText = s" $titleText0 " // -12 just to ensure we always have some ==== divider on each side of the title diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 8b8064b8b14..82a7f7771dc 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -17,7 +17,8 @@ private[mill] class MultilinePromptLogger( systemStreams0: SystemStreams, override val debugEnabled: Boolean, titleText: String, - terminfoPath: os.Path + terminfoPath: os.Path, + currentTimeMillis: () => Long ) extends ColorLogger with AutoCloseable { override def toString = s"MultilinePromptLogger(${literalize(titleText)}" import MultilinePromptLogger._ @@ -29,8 +30,9 @@ private[mill] class MultilinePromptLogger( private val state = new State( titleText, systemStreams0, - System.currentTimeMillis(), - () => termDimensions + currentTimeMillis(), + () => termDimensions, + currentTimeMillis ) private val streams = new Streams( @@ -54,12 +56,13 @@ private[mill] class MultilinePromptLogger( if (!paused) { synchronized { readTerminalDims(terminfoPath).foreach(termDimensions = _) - state.refreshPrompt() + refreshPrompt() } } } ) + def refreshPrompt() = state.refreshPrompt() if (enableTicker) promptUpdaterThread.start() override def withPaused[T](t: => T): T = { @@ -174,7 +177,8 @@ object MultilinePromptLogger{ titleText: String, systemStreams0: SystemStreams, startTimeMillis: Long, - consoleDims: () => (Option[Int], Option[Int]) + consoleDims: () => (Option[Int], Option[Int]), + currentTimeMillis: () => Long ) { private var lastRenderedPromptHash = 0 private val statuses = collection.mutable.SortedMap.empty[Int, Status] @@ -186,7 +190,7 @@ object MultilinePromptLogger{ @volatile var currentPromptBytes: Array[Byte] = Array[Byte]() private def updatePromptBytes(ending: Boolean = false) = { - val now = System.currentTimeMillis() + val now = currentTimeMillis() for (k <- statuses.keySet) { val removedTime = statuses(k).removedTimeMillis if (now - removedTime > statusRemovalRemoveDelayMillis) { @@ -236,7 +240,7 @@ object MultilinePromptLogger{ def updateCurrent(sOpt: Option[String]): Unit = synchronized { val threadId = Thread.currentThread().getId.toInt - val now = System.currentTimeMillis() + val now = currentTimeMillis() sOpt match { case None => statuses.get(threadId).foreach(_.removedTimeMillis = now) case Some(s) => statuses(threadId) = Status(now, s, Long.MaxValue) diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 4f00904e88e..7e7b602f95b 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -4,16 +4,18 @@ import mill.api.SystemStreams import mill.main.client.ProxyStream import utest._ -import java.io.{ByteArrayOutputStream, PrintStream} +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} object MultilinePromptLoggerTests extends TestSuite { val tests = Tests { - test { + test("nonInteractive") { + var now = 0L + val baos = new ByteArrayOutputStream() val baosOut = new PrintStream(new ProxyStream.Output(baos, ProxyStream.OUT)) val baosErr = new PrintStream(new ProxyStream.Output(baos, ProxyStream.ERR)) - val logger = new MultilinePromptLogger( + val promptLogger = new MultilinePromptLogger( colored = false, enableTicker = true, infoColor = fansi.Attrs.Empty, @@ -21,10 +23,57 @@ object MultilinePromptLoggerTests extends TestSuite { systemStreams0 = new SystemStreams(baosOut, baosErr, System.in), debugEnabled = false, titleText = "TITLE", - terminfoPath = os.temp() + terminfoPath = os.temp(), + currentTimeMillis = () => now + ) + val prefixLogger = new PrefixLogger(promptLogger, "[1]") + + promptLogger.globalTicker("123/456") + promptLogger.ticker("[1]", "[1/456]", "my-task") + + now += 10000 + + prefixLogger.outputStream.println("HELLO") + + promptLogger.refreshPrompt() + + prefixLogger.outputStream.println("WORLD") + + promptLogger.endTicker() + + now += 10000 + promptLogger.refreshPrompt() + now += 10000 + promptLogger.close() + + val finalBaos = new ByteArrayOutputStream() + val pumper = new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) + pumper.run() + val lines = finalBaos.toString.linesIterator.toSeq + val expected = Seq( + // Make sure that the first time a prefix is reported, + // we print the verbose prefix along with the ticker string + "[1/456] my-task", + // Further `println`s come with the prefix + "[1] HELLO", + // Calling `refreshPrompt()` prints the header with the given `globalTicker` without + // the double space prefix (since it's non-interactive and we don't need space for a cursor), + // the time elapsed, the reported title and ticker, the list of active tickers, followed by the + // footer + "123/456 ================================================== TITLE ================================================= 10s", + "[1] my-task 10s", + "======================================================================================================================", + "[1] WORLD", + // Calling `refreshPrompt()` after closing the ticker shows the prompt without + // the ticker in the list, with an updated time elapsed + "123/456 ================================================== TITLE ================================================= 20s", + "======================================================================================================================", + // Closing the prompt prints the prompt one last time with an updated time elapsed + "123/456 ================================================== TITLE ================================================= 30s", + "======================================================================================================================", ) -// logger. + assert(lines == expected) } } } diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 96e43dbfc90..e600cbcc261 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -339,7 +339,8 @@ object MillMain { systemStreams0 = streams, debugEnabled = config.debugLog.value, titleText = config.leftoverArgs.value.mkString(" "), - terminfoPath = serverDir / ServerFiles.terminfo + terminfoPath = serverDir / ServerFiles.terminfo, + currentTimeMillis = () => System.currentTimeMillis() ) } From 101802ae2e5e24b55a580d125cb75546f82172d9 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 12:24:20 +0800 Subject: [PATCH 088/116] implement test terminal --- .../util/MultilinePromptLoggerTests.scala | 132 +++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 7e7b602f95b..84d3d410ec0 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -5,7 +5,6 @@ import mill.main.client.ProxyStream import utest._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} - object MultilinePromptLoggerTests extends TestSuite { val tests = Tests { @@ -75,5 +74,136 @@ object MultilinePromptLoggerTests extends TestSuite { assert(lines == expected) } + test("testTerminal"){ + test("wrap") { + val t = new TestTerminal(width = 10) + t.writeAll("1234567890abcdef") + t.grid ==> Seq( + "1234567890", + "abcdef" + ) + } + test("newline") { + val t = new TestTerminal(width = 10) + t.writeAll("12345\n67890") + t.grid ==> Seq( + "12345", + "67890" + ) + } + test("wrapNewline") { + val t = new TestTerminal(width = 10) + t.writeAll("1234567890\nabcdef") + t.grid ==> Seq( + "1234567890", + "abcdef" + ) + } + test("wrapNewline2") { + val t = new TestTerminal(width = 10) + t.writeAll("1234567890\n\nabcdef") + t.grid ==> Seq( + "1234567890", + "", + "abcdef" + ) + } + test("up") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}X") + t.grid ==> Seq( + "123456X890", + "abcdef", + ) + } + test("left") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.left(3)}X") + t.grid ==> Seq( + "1234567890", + "abcXef", + ) + } + test("upLeftClearLine") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearLine(0)}") + t.grid ==> Seq( + "123X", + "abcdef", + ) + } + test("upLeftClearScreen") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearScreen(0)}") + t.grid ==> Seq("123X") + } + } + } +} + +class TestTerminal(width: Int){ + var grid = collection.mutable.Buffer("") + var cursorX = 0 + var cursorY = 0 + + def writeAll(data0: String): Unit = { + + def rec(data: String): Unit = data match{ // pprint.log((grid, cursorX, cursorY, data.head)) + case "" => // end + case s"\u001b[${n}A$rest" => // up + cursorY = math.max(cursorY - n.toInt, 0) + rec(rest) + + case s"\u001b[${n}B$rest" => // down + cursorY = math.min(cursorY + n.toInt, grid.size) + rec(rest) + + case s"\u001b[${n}C$rest" => // right + cursorX = math.min(cursorX + n.toInt, width) + rec(rest) + + case s"\u001b[${n}D$rest" => // left + cursorX = math.max(cursorX - n.toInt, 0) + rec(rest) + + case s"\u001b[${n}J$rest" => // clearscreen + n match{ + case "0" => + grid(cursorY) = grid(cursorY).take(cursorX) + grid = grid.take(cursorY + 1) + rec(rest) + } + + case s"\u001b[${n}K$rest" => // clearline + n match{ + case "0" => + grid(cursorY) = grid(cursorY).take(cursorX) + rec(rest) + } + + case normal => // normal text + if (normal.head == '\n'){ + cursorX = 0 + if (cursorY >= grid.length) grid.append("") + cursorY += 1 + }else { + if (cursorX == width){ + cursorX = 0 + cursorY += 1 + } + if (cursorY >= grid.length) grid.append("") + grid(cursorY) = grid(cursorY).patch(cursorX, Seq(normal.head), 1) + if (cursorX < width) { + cursorX += 1 + } else { + cursorX = 0 + cursorY += 1 + } + + } + rec(normal.tail) + } + rec(data0) } } + From 048f00bc4631bfc2e3cdb26d98ddcfb52c901866 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 12:26:43 +0800 Subject: [PATCH 089/116] wip --- .../src/mill/util/MultilinePromptLoggerTests.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 84d3d410ec0..fa5e092d251 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -137,6 +137,19 @@ object MultilinePromptLoggerTests extends TestSuite { t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearScreen(0)}") t.grid ==> Seq("123X") } + test("wrapUpClearLine") { + val t = new TestTerminal(width = 10) + t.writeAll(s"1234567890abcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearLine(0)}") + t.grid ==> Seq( + "123X", + "abcdef", + ) + } + test("wrapUpClearScreen") { + val t = new TestTerminal(width = 10) + t.writeAll(s"1234567890abcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearScreen(0)}") + t.grid ==> Seq("123X") + } } } } From 470caf51c69e26c06b45cd510ab120c632e406a8 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 12:47:29 +0800 Subject: [PATCH 090/116] wip --- main/api/src/mill/api/Logger.scala | 5 +- main/eval/src/mill/eval/GroupEvaluator.scala | 8 +- main/util/src/mill/util/FileLogger.scala | 2 +- .../mill/util/LinePrefixOutputStream.scala | 5 +- .../mill/util/MultiLinePromptLoggerUtil.scala | 35 ++- main/util/src/mill/util/MultiLogger.scala | 2 +- .../src/mill/util/MultilinePromptLogger.scala | 46 ++-- main/util/src/mill/util/PrefixLogger.scala | 11 +- main/util/src/mill/util/PrintLogger.scala | 2 +- main/util/src/mill/util/ProxyLogger.scala | 5 +- .../util/LinePrefixOutputStreamTests.scala | 11 +- .../util/MultilinePromptLoggerTests.scala | 244 ++++++------------ .../util/MultilinePromptLoggerUtilTests.scala | 2 +- .../src/mill/util/TestTerminalTests.scala | 157 +++++++++++ 14 files changed, 303 insertions(+), 232 deletions(-) create mode 100644 main/util/test/src/mill/util/TestTerminalTests.scala diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 520baf24c2d..dc1c027097d 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -45,9 +45,8 @@ trait Logger { def error(s: String): Unit def ticker(s: String): Unit def reportPrefix(s: String): Unit = () - def ticker(identifier: String, - identSuffix: String, - message: String): Unit = ticker(s"$identifier $message") + def ticker(identifier: String, identSuffix: String, message: String): Unit = + ticker(s"$identifier $message") def globalTicker(s: String): Unit = () def clearAllTickers(): Unit = () def endTicker(): Unit = () diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index da70ea9e1bb..b9d13554857 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -65,7 +65,6 @@ private[mill] trait GroupEvaluator { val sideHashes = MurmurHash3.orderedHash(group.iterator.map(_.sideHash)) - val scriptsHash = if (disableCallgraph) 0 else group @@ -116,7 +115,6 @@ private[mill] trait GroupEvaluator { } ) - methodCodeHashSignatures.get(expectedName) ++ constructorHashes } .flatten @@ -136,7 +134,7 @@ private[mill] trait GroupEvaluator { identSuffix = identSuffix, zincProblemReporter, testReporter, - logger, + logger ) GroupEvaluator.Results(newResults, newEvaluated.toSeq, null, inputsHash, -1) @@ -186,7 +184,7 @@ private[mill] trait GroupEvaluator { identSuffix = identSuffix, zincProblemReporter, testReporter, - logger, + logger ) } @@ -227,7 +225,7 @@ private[mill] trait GroupEvaluator { identSuffix: String, reporter: Int => Option[CompileProblemReporter], testReporter: TestReporter, - logger: mill.api.Logger, + logger: mill.api.Logger ): (Map[Task[_], TaskResult[(Val, Int)]], mutable.Buffer[Task[_]]) = { def computeAll(enableTicker: Boolean) = { diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index 1ed3bbaf962..6c67d1fc1fd 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -11,7 +11,7 @@ class FileLogger( override val debugEnabled: Boolean, append: Boolean = false ) extends Logger { - override def toString = s"FileLogger($file)" + override def toString: String = s"FileLogger($file)" private[this] var outputStreamUsed: Boolean = false lazy val fileStream: PrintStream = { diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index e2cfd59c675..837dfb1a54a 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -1,6 +1,5 @@ package mill.util - import java.io.{ByteArrayOutputStream, OutputStream} /** @@ -29,7 +28,7 @@ class LinePrefixOutputStream( } } - def writeOutBuffer() = { + def writeOutBuffer(): Unit = { if (buffer.size() > 0) reportPrefix() out.synchronized { buffer.writeTo(out) } buffer.reset() @@ -51,7 +50,7 @@ class LinePrefixOutputStream( i += 1 } - if (math.min(i, max) - start > 0){ + if (math.min(i, max) - start > 0) { writeLinePrefixIfNecessary() buffer.write(b, start, math.min(i, max) - start) if (b(max - 1) == '\n') writeOutBuffer() diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala index c8ec03ebc52..f07d016f365 100644 --- a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -66,16 +66,16 @@ private object MultilinePromptLoggerUtil { } def renderPrompt( - consoleWidth: Int, - consoleHeight: Int, - now: Long, - startTimeMillis: Long, - headerPrefix: String, - titleText: String, - statuses: collection.SortedMap[Int, Status], - interactive: Boolean, - ending: Boolean = false - ): List[String] = { + consoleWidth: Int, + consoleHeight: Int, + now: Long, + startTimeMillis: Long, + headerPrefix: String, + titleText: String, + statuses: collection.SortedMap[Int, Status], + interactive: Boolean, + ending: Boolean = false + ): List[String] = { // -1 to leave a bit of buffer val maxWidth = consoleWidth - 1 // -1 to account for header @@ -117,13 +117,13 @@ private object MultilinePromptLoggerUtil { } def renderHeader( - headerPrefix0: String, - titleText0: String, - headerSuffix0: String, - maxWidth: Int, - ending: Boolean = false, - interactive: Boolean = true - ): String = { + headerPrefix0: String, + titleText0: String, + headerSuffix0: String, + maxWidth: Int, + ending: Boolean = false, + interactive: Boolean = true + ): String = { val headerPrefixStr = if (!interactive || ending) s"$headerPrefix0 " else s" $headerPrefix0 " val headerSuffixStr = s" $headerSuffix0" val titleText = s" $titleText0 " @@ -167,4 +167,3 @@ private object MultilinePromptLoggerUtil { ??? } } - diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 8d8d8de99fb..0ecd4e4b59f 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -11,7 +11,7 @@ class MultiLogger( val inStream0: InputStream, override val debugEnabled: Boolean ) extends Logger { - override def toString = s"MultiLogger($logger1, $logger2)" + override def toString: String = s"MultiLogger($logger1, $logger2)" lazy val systemStreams = new SystemStreams( new MultiStream(logger1.systemStreams.out, logger2.systemStreams.out), new MultiStream(logger1.systemStreams.err, logger2.systemStreams.err), diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 82a7f7771dc..cf1e518595f 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -2,13 +2,18 @@ package mill.util import mill.api.SystemStreams import mill.main.client.ProxyStream -import mill.util.MultilinePromptLoggerUtil.{Status, clearScreenToEndBytes, defaultTermHeight, defaultTermWidth, renderPrompt} +import mill.util.MultilinePromptLoggerUtil.{ + Status, + clearScreenToEndBytes, + defaultTermHeight, + defaultTermWidth, + renderPrompt +} import pprint.Util.literalize import java.io._ import MultilinePromptLoggerUtil._ - private[mill] class MultilinePromptLogger( override val colored: Boolean, val enableTicker: Boolean, @@ -20,7 +25,7 @@ private[mill] class MultilinePromptLogger( terminfoPath: os.Path, currentTimeMillis: () => Long ) extends ColorLogger with AutoCloseable { - override def toString = s"MultilinePromptLogger(${literalize(titleText)}" + override def toString: String = s"MultilinePromptLogger(${literalize(titleText)}" import MultilinePromptLogger._ private var termDimensions: (Option[Int], Option[Int]) = (None, None) @@ -62,7 +67,7 @@ private[mill] class MultilinePromptLogger( } ) - def refreshPrompt() = state.refreshPrompt() + def refreshPrompt(): Unit = state.refreshPrompt() if (enableTicker) promptUpdaterThread.start() override def withPaused[T](t: => T): T = { @@ -92,11 +97,12 @@ private[mill] class MultilinePromptLogger( private val seenIdentifiers = collection.mutable.Map.empty[String, (String, String)] private val reportedIdentifiers = collection.mutable.Set.empty[String] - override def ticker(identifier: String, identSuffix: String, message: String): Unit = synchronized { - seenIdentifiers(identifier) = (identSuffix, message) - super.ticker(infoColor(identifier).toString(), identSuffix, message) + override def ticker(identifier: String, identSuffix: String, message: String): Unit = + synchronized { + seenIdentifiers(identifier) = (identSuffix, message) + super.ticker(infoColor(identifier).toString(), identSuffix, message) - } + } def debug(s: String): Unit = synchronized { if (debugEnabled) systemStreams.err.println(s) } override def rawOutputStream: PrintStream = systemStreams0.out @@ -110,14 +116,14 @@ private[mill] class MultilinePromptLogger( def systemStreams = streams.systemStreams } -object MultilinePromptLogger{ +object MultilinePromptLogger { private class Streams( - enableTicker: Boolean, - systemStreams0: SystemStreams, - currentPromptBytes: () => Array[Byte], - interactive: () => Boolean - ) { + enableTicker: Boolean, + systemStreams0: SystemStreams, + currentPromptBytes: () => Array[Byte], + interactive: () => Boolean + ) { // We force both stdout and stderr streams into a single `Piped*Stream` pair via // `ProxyStream`, as we need to preserve the ordering of writes to each individual @@ -174,12 +180,12 @@ object MultilinePromptLogger{ } } private class State( - titleText: String, - systemStreams0: SystemStreams, - startTimeMillis: Long, - consoleDims: () => (Option[Int], Option[Int]), - currentTimeMillis: () => Long - ) { + titleText: String, + systemStreams0: SystemStreams, + startTimeMillis: Long, + consoleDims: () => (Option[Int], Option[Int]), + currentTimeMillis: () => Long + ) { private var lastRenderedPromptHash = 0 private val statuses = collection.mutable.SortedMap.empty[Int, Status] diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 2ecb0e9be6e..7b14a570e92 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -1,7 +1,6 @@ package mill.util import mill.api.SystemStreams -import mill.main.client.DebugLog import pprint.Util.literalize import java.io.PrintStream @@ -13,8 +12,9 @@ class PrefixLogger( outStream0: Option[PrintStream] = None, errStream0: Option[PrintStream] = None ) extends ColorLogger { - val context = if(context0 == "") "" else context0 + " " - override def toString = s"PrefixLogger($logger0, ${literalize(context)}, ${literalize(tickerContext)})" + val context: String = if (context0 == "") "" else context0 + " " + override def toString: String = + s"PrefixLogger($logger0, ${literalize(context)}, ${literalize(tickerContext)})" def this(logger0: ColorLogger, context: String, tickerContext: String) = this(logger0, context, tickerContext, None, None) @@ -52,7 +52,8 @@ class PrefixLogger( logger0.error(infoColor(context) + s) } override def ticker(s: String): Unit = logger0.ticker(context + tickerContext + s) - override def ticker(identifier: String, identSuffix: String, message: String): Unit = logger0.ticker(identifier, identSuffix, message) + override def ticker(identifier: String, identSuffix: String, message: String): Unit = + logger0.ticker(identifier, identSuffix, message) override def debug(s: String): Unit = { if (debugEnabled) reportPrefix(context0) logger0.debug(infoColor(context) + s) @@ -66,7 +67,7 @@ class PrefixLogger( outStream0 = Some(outStream), errStream0 = Some(systemStreams.err) ) - override def reportPrefix(s: String) = { + override def reportPrefix(s: String): Unit = { logger0.reportPrefix(s) } override def endTicker(): Unit = logger0.endTicker() diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index c3f5569a318..f8b2f965fdd 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -13,7 +13,7 @@ class PrintLogger( val context: String, printLoggerState: PrintLogger.State ) extends ColorLogger { - override def toString = s"PrintLogger($colored, $enableTicker)" + override def toString: String = s"PrintLogger($colored, $enableTicker)" def info(s: String): Unit = synchronized { printLoggerState.value = PrintLogger.State.Newline systemStreams.err.println(infoColor(context + s)) diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 440ec40530f..6282bfce264 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -9,7 +9,7 @@ import java.io.PrintStream * used as a base class for wrappers that modify logging behavior. */ class ProxyLogger(logger: Logger) extends Logger { - override def toString = s"ProxyLogger($logger)" + override def toString: String = s"ProxyLogger($logger)" def colored = logger.colored lazy val systemStreams = logger.systemStreams @@ -17,7 +17,8 @@ class ProxyLogger(logger: Logger) extends Logger { def info(s: String): Unit = logger.info(s) def error(s: String): Unit = logger.error(s) def ticker(s: String): Unit = logger.ticker(s) - override def ticker(identifier: String, identSuffix: String, message: String): Unit = logger.ticker(identifier, identSuffix, message) + override def ticker(identifier: String, identSuffix: String, message: String): Unit = + logger.ticker(identifier, identSuffix, message) def debug(s: String): Unit = logger.debug(s) override def debugEnabled: Boolean = logger.debugEnabled diff --git a/main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala b/main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala index 6ecda54f4c6..ca114de48dd 100644 --- a/main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala +++ b/main/util/test/src/mill/util/LinePrefixOutputStreamTests.scala @@ -4,13 +4,12 @@ import utest._ import java.io.ByteArrayOutputStream - object LinePrefixOutputStreamTests extends TestSuite { val tests = Tests { test("charByChar") { val baos = new ByteArrayOutputStream() val lpos = new LinePrefixOutputStream("PREFIX", baos) - for(b <- "hello\nworld\n!".getBytes()) lpos.write(b) + for (b <- "hello\nworld\n!".getBytes()) lpos.write(b) lpos.flush() assert(baos.toString == "PREFIXhello\nPREFIXworld\nPREFIX!") } @@ -18,7 +17,7 @@ object LinePrefixOutputStreamTests extends TestSuite { test("charByCharTrailingNewline") { val baos = new ByteArrayOutputStream() val lpos = new LinePrefixOutputStream("PREFIX", baos) - for(b <- "hello\nworld\n".getBytes()) lpos.write(b) + for (b <- "hello\nworld\n".getBytes()) lpos.write(b) lpos.flush() assert(baos.toString == "PREFIXhello\nPREFIXworld\n") } @@ -44,9 +43,9 @@ object LinePrefixOutputStreamTests extends TestSuite { } test("ranges") { - for(str <- Seq("hello\nworld\n")){ + for (str <- Seq("hello\nworld\n")) { val arr = str.getBytes() - for(i1 <- Range(0, arr.length)) { + for (i1 <- Range(0, arr.length)) { for (i2 <- Range(i1, arr.length)) { for (i3 <- Range(i2, arr.length)) { val baos = new ByteArrayOutputStream() @@ -62,8 +61,6 @@ object LinePrefixOutputStreamTests extends TestSuite { } } - - } } } diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index fa5e092d251..29aa1536257 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -7,25 +7,41 @@ import utest._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} object MultilinePromptLoggerTests extends TestSuite { + def setup(now: () => Long, terminfoPath: os.Path) = { + val baos = new ByteArrayOutputStream() + val baosOut = new PrintStream(new ProxyStream.Output(baos, ProxyStream.OUT)) + val baosErr = new PrintStream(new ProxyStream.Output(baos, ProxyStream.ERR)) + val promptLogger = new MultilinePromptLogger( + colored = false, + enableTicker = true, + infoColor = fansi.Attrs.Empty, + errorColor = fansi.Attrs.Empty, + systemStreams0 = new SystemStreams(baosOut, baosErr, System.in), + debugEnabled = false, + titleText = "TITLE", + terminfoPath = terminfoPath, + currentTimeMillis = now + ) + val prefixLogger = new PrefixLogger(promptLogger, "[1]") + (baos, promptLogger, prefixLogger) + } + + def check(baos: ByteArrayOutputStream, width: Int = 80)(expected: String*) = { + val finalBaos = new ByteArrayOutputStream() + val pumper = + new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) + pumper.run() + val term = new TestTerminal(width) + term.writeAll(finalBaos.toString) + val lines = term.grid + assert(lines == expected) + } + val tests = Tests { test("nonInteractive") { var now = 0L - val baos = new ByteArrayOutputStream() - val baosOut = new PrintStream(new ProxyStream.Output(baos, ProxyStream.OUT)) - val baosErr = new PrintStream(new ProxyStream.Output(baos, ProxyStream.ERR)) - val promptLogger = new MultilinePromptLogger( - colored = false, - enableTicker = true, - infoColor = fansi.Attrs.Empty, - errorColor = fansi.Attrs.Empty, - systemStreams0 = new SystemStreams(baosOut, baosErr, System.in), - debugEnabled = false, - titleText = "TITLE", - terminfoPath = os.temp(), - currentTimeMillis = () => now - ) - val prefixLogger = new PrefixLogger(promptLogger, "[1]") + val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp()) promptLogger.globalTicker("123/456") promptLogger.ticker("[1]", "[1/456]", "my-task") @@ -45,11 +61,7 @@ object MultilinePromptLoggerTests extends TestSuite { now += 10000 promptLogger.close() - val finalBaos = new ByteArrayOutputStream() - val pumper = new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) - pumper.run() - val lines = finalBaos.toString.linesIterator.toSeq - val expected = Seq( + check(baos, width = 999 /*log file has no line wrapping*/ )( // Make sure that the first time a prefix is reported, // we print the verbose prefix along with the ticker string "[1/456] my-task", @@ -69,154 +81,56 @@ object MultilinePromptLoggerTests extends TestSuite { "======================================================================================================================", // Closing the prompt prints the prompt one last time with an updated time elapsed "123/456 ================================================== TITLE ================================================= 30s", - "======================================================================================================================", + "======================================================================================================================" ) - - assert(lines == expected) - } - test("testTerminal"){ - test("wrap") { - val t = new TestTerminal(width = 10) - t.writeAll("1234567890abcdef") - t.grid ==> Seq( - "1234567890", - "abcdef" - ) - } - test("newline") { - val t = new TestTerminal(width = 10) - t.writeAll("12345\n67890") - t.grid ==> Seq( - "12345", - "67890" - ) - } - test("wrapNewline") { - val t = new TestTerminal(width = 10) - t.writeAll("1234567890\nabcdef") - t.grid ==> Seq( - "1234567890", - "abcdef" - ) - } - test("wrapNewline2") { - val t = new TestTerminal(width = 10) - t.writeAll("1234567890\n\nabcdef") - t.grid ==> Seq( - "1234567890", - "", - "abcdef" - ) - } - test("up") { - val t = new TestTerminal(width = 15) - t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}X") - t.grid ==> Seq( - "123456X890", - "abcdef", - ) - } - test("left") { - val t = new TestTerminal(width = 15) - t.writeAll(s"1234567890\nabcdef${AnsiNav.left(3)}X") - t.grid ==> Seq( - "1234567890", - "abcXef", - ) - } - test("upLeftClearLine") { - val t = new TestTerminal(width = 15) - t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearLine(0)}") - t.grid ==> Seq( - "123X", - "abcdef", - ) - } - test("upLeftClearScreen") { - val t = new TestTerminal(width = 15) - t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearScreen(0)}") - t.grid ==> Seq("123X") - } - test("wrapUpClearLine") { - val t = new TestTerminal(width = 10) - t.writeAll(s"1234567890abcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearLine(0)}") - t.grid ==> Seq( - "123X", - "abcdef", - ) - } - test("wrapUpClearScreen") { - val t = new TestTerminal(width = 10) - t.writeAll(s"1234567890abcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearScreen(0)}") - t.grid ==> Seq("123X") - } } - } -} -class TestTerminal(width: Int){ - var grid = collection.mutable.Buffer("") - var cursorX = 0 - var cursorY = 0 - - def writeAll(data0: String): Unit = { - - def rec(data: String): Unit = data match{ // pprint.log((grid, cursorX, cursorY, data.head)) - case "" => // end - case s"\u001b[${n}A$rest" => // up - cursorY = math.max(cursorY - n.toInt, 0) - rec(rest) - - case s"\u001b[${n}B$rest" => // down - cursorY = math.min(cursorY + n.toInt, grid.size) - rec(rest) - - case s"\u001b[${n}C$rest" => // right - cursorX = math.min(cursorX + n.toInt, width) - rec(rest) - - case s"\u001b[${n}D$rest" => // left - cursorX = math.max(cursorX - n.toInt, 0) - rec(rest) - - case s"\u001b[${n}J$rest" => // clearscreen - n match{ - case "0" => - grid(cursorY) = grid(cursorY).take(cursorX) - grid = grid.take(cursorY + 1) - rec(rest) - } - - case s"\u001b[${n}K$rest" => // clearline - n match{ - case "0" => - grid(cursorY) = grid(cursorY).take(cursorX) - rec(rest) - } - - case normal => // normal text - if (normal.head == '\n'){ - cursorX = 0 - if (cursorY >= grid.length) grid.append("") - cursorY += 1 - }else { - if (cursorX == width){ - cursorX = 0 - cursorY += 1 - } - if (cursorY >= grid.length) grid.append("") - grid(cursorY) = grid(cursorY).patch(cursorX, Seq(normal.head), 1) - if (cursorX < width) { - cursorX += 1 - } else { - cursorX = 0 - cursorY += 1 - } - - } - rec(normal.tail) + test("interactive") { + var now = 0L + val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) + + promptLogger.globalTicker("123/456") + promptLogger.ticker("[1]", "[1/456]", "my-task") + + now += 10000 + + prefixLogger.outputStream.println("HELLO") + + promptLogger.refreshPrompt() + check(baos)( + "[1/456] my-task", + "[1] HELLO", + " 123/456 ============================ TITLE ============================== 10s", + "[1] my-task 10s" + ) + prefixLogger.outputStream.println("WORLD") + + promptLogger.refreshPrompt() + check(baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + " 123/456 ============================ TITLE ============================== 10s", + "[1] my-task 10s" + ) + promptLogger.endTicker() + + now += 10000 + promptLogger.refreshPrompt() + check(baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + " 123/456 ============================ TITLE ============================== 20s" + ) + now += 10000 + promptLogger.close() + check(baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "123/456 ============================== TITLE ============================== 30s" + ) } - rec(data0) } } - diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala index 7fa063d2c19..ee88f087c09 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala @@ -307,7 +307,7 @@ object MultilinePromptLoggerUtilTests extends TestSuite { // Make sure the non-interactive prompt does not show the blank lines, // and it contains a footer line to mark the end of the prompt in logs val expected = List( - " 123/456 __.compile....z1234567890 ================ 1337s", + "123/456 __.compile.ab...xyz1234567890 ============== 1337s", "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", "#2 world 2s", "#3 i am cow 3s", diff --git a/main/util/test/src/mill/util/TestTerminalTests.scala b/main/util/test/src/mill/util/TestTerminalTests.scala new file mode 100644 index 00000000000..d57cbcdbd45 --- /dev/null +++ b/main/util/test/src/mill/util/TestTerminalTests.scala @@ -0,0 +1,157 @@ +package mill.util + +import utest._ + +/** + * Minimal implementation of a terminal emulator that handles ANSI navigation + * codes, so we can feed in terminal strings and assert that the terminal looks + * like what it should look like including cleared lines/screens and overridden text + */ +class TestTerminal(width: Int) { + var grid = collection.mutable.Buffer("") + var cursorX = 0 + var cursorY = 0 + + def writeAll(data0: String): Unit = { + + def rec(data: String): Unit = data match { // pprint.log((grid, cursorX, cursorY, data.head)) + case "" => // end + case s"\u001b[$rest0" => + val num0 = rest0.takeWhile(_.isDigit) + val n = num0.toInt + val char = rest0(num0.length) + val rest = rest0.drop(num0.length + 1) + char match { + case 'A' => // up + cursorY = math.max(cursorY - n.toInt, 0) + rec(rest) + case 'B' => // down + cursorY = math.min(cursorY + n.toInt, grid.size) + rec(rest) + case 'C' => // right + cursorX = math.min(cursorX + n.toInt, width) + rec(rest) + case 'D' => // left + cursorX = math.max(cursorX - n.toInt, 0) + rec(rest) + case 'J' => // clearscreen + n match { + case 0 => + if (cursorY < grid.length) grid(cursorY) = grid(cursorY).take(cursorX) + grid = grid.take(cursorY + 1) + rec(rest) + } + case 'K' => // clearline + n match { + case 0 => + grid(cursorY) = grid(cursorY).take(cursorX) + rec(rest) + } + } + + case normal => // normal text + if (normal.head == '\n') { + cursorX = 0 + if (cursorY >= grid.length) grid.append("") + cursorY += 1 + } else { + if (cursorX == width) { + cursorX = 0 + cursorY += 1 + } + if (cursorY >= grid.length) grid.append("") + grid(cursorY) = grid(cursorY).patch(cursorX, Seq(normal.head), 1) + if (cursorX < width) { + cursorX += 1 + } else { + cursorX = 0 + cursorY += 1 + } + + } + rec(normal.tail) + } + rec(data0) + } +} + +object TestTerminalTests extends TestSuite { + + val tests = Tests { + test("wrap") { + val t = new TestTerminal(width = 10) + t.writeAll("1234567890abcdef") + t.grid ==> Seq( + "1234567890", + "abcdef" + ) + } + test("newline") { + val t = new TestTerminal(width = 10) + t.writeAll("12345\n67890") + t.grid ==> Seq( + "12345", + "67890" + ) + } + test("wrapNewline") { + val t = new TestTerminal(width = 10) + t.writeAll("1234567890\nabcdef") + t.grid ==> Seq( + "1234567890", + "abcdef" + ) + } + test("wrapNewline2") { + val t = new TestTerminal(width = 10) + t.writeAll("1234567890\n\nabcdef") + t.grid ==> Seq( + "1234567890", + "", + "abcdef" + ) + } + test("up") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}X") + t.grid ==> Seq( + "123456X890", + "abcdef" + ) + } + test("left") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.left(3)}X") + t.grid ==> Seq( + "1234567890", + "abcXef" + ) + } + test("upLeftClearLine") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearLine(0)}") + t.grid ==> Seq( + "123X", + "abcdef" + ) + } + test("upLeftClearScreen") { + val t = new TestTerminal(width = 15) + t.writeAll(s"1234567890\nabcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearScreen(0)}") + t.grid ==> Seq("123X") + } + test("wrapUpClearLine") { + val t = new TestTerminal(width = 10) + t.writeAll(s"1234567890abcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearLine(0)}") + t.grid ==> Seq( + "123X", + "abcdef" + ) + } + test("wrapUpClearScreen") { + val t = new TestTerminal(width = 10) + t.writeAll(s"1234567890abcdef${AnsiNav.up(1)}${AnsiNav.left(3)}X${AnsiNav.clearScreen(0)}") + t.grid ==> Seq("123X") + } + } +} From a211350b82ee65a5ee9336e4c92de8849572da3b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 14:32:05 +0800 Subject: [PATCH 091/116] disable prompt updater in integration tests --- main/util/src/mill/util/MultilinePromptLogger.scala | 5 +++-- .../test/src/mill/util/MultilinePromptLoggerTests.scala | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index cf1e518595f..4cf1ebe3501 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -23,7 +23,8 @@ private[mill] class MultilinePromptLogger( override val debugEnabled: Boolean, titleText: String, terminfoPath: os.Path, - currentTimeMillis: () => Long + currentTimeMillis: () => Long, + autoUpdate: Boolean = true ) extends ColorLogger with AutoCloseable { override def toString: String = s"MultilinePromptLogger(${literalize(titleText)}" import MultilinePromptLogger._ @@ -68,7 +69,7 @@ private[mill] class MultilinePromptLogger( ) def refreshPrompt(): Unit = state.refreshPrompt() - if (enableTicker) promptUpdaterThread.start() + if (enableTicker && autoUpdate) promptUpdaterThread.start() override def withPaused[T](t: => T): T = { paused = true diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index 29aa1536257..d0f721c6a29 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -20,13 +20,15 @@ object MultilinePromptLoggerTests extends TestSuite { debugEnabled = false, titleText = "TITLE", terminfoPath = terminfoPath, - currentTimeMillis = now + currentTimeMillis = now, + autoUpdate = false ) val prefixLogger = new PrefixLogger(promptLogger, "[1]") (baos, promptLogger, prefixLogger) } def check(baos: ByteArrayOutputStream, width: Int = 80)(expected: String*) = { + Thread.sleep(200) val finalBaos = new ByteArrayOutputStream() val pumper = new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) @@ -90,6 +92,10 @@ object MultilinePromptLoggerTests extends TestSuite { val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) promptLogger.globalTicker("123/456") + promptLogger.refreshPrompt() + check(baos)( + " 123/456 ============================ TITLE ================================= ", + ) promptLogger.ticker("[1]", "[1/456]", "my-task") now += 10000 From 66b389df4d570b34042be0d05c579d5dfdcfd0d6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 14:55:34 +0800 Subject: [PATCH 092/116] wip --- main/api/src/mill/api/Logger.scala | 10 +-- main/util/src/mill/util/DummyLogger.scala | 1 - main/util/src/mill/util/MultiLogger.scala | 12 ++-- .../src/mill/util/MultilinePromptLogger.scala | 4 ++ main/util/src/mill/util/PrefixLogger.scala | 12 ++-- main/util/src/mill/util/ProxyLogger.scala | 12 ++-- .../util/MultilinePromptLoggerTests.scala | 61 +++++++++++++++---- 7 files changed, 82 insertions(+), 30 deletions(-) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index dc1c027097d..76346e8ca86 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,12 +44,12 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit - def reportPrefix(s: String): Unit = () - def ticker(identifier: String, identSuffix: String, message: String): Unit = + private[mill] def reportPrefix(s: String): Unit = () + private[mill] def ticker(identifier: String, identSuffix: String, message: String): Unit = ticker(s"$identifier $message") - def globalTicker(s: String): Unit = () - def clearAllTickers(): Unit = () - def endTicker(): Unit = () + private[mill] def globalTicker(s: String): Unit = () + private[mill] def clearAllTickers(): Unit = () + private[mill] def endTicker(): Unit = () def debug(s: String): Unit diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index 5249a1759ad..14685983ea5 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -18,6 +18,5 @@ object DummyLogger extends Logger { def error(s: String) = () def ticker(s: String) = () def debug(s: String) = () - override def reportPrefix(s: String) = () override val debugEnabled: Boolean = false } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 0ecd4e4b59f..e3dfb112e2d 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -31,7 +31,11 @@ class MultiLogger( logger2.ticker(s) } - override def ticker(identifier: String, identSuffix: String, message: String): Unit = { + private[mill] override def ticker( + identifier: String, + identSuffix: String, + message: String + ): Unit = { logger1.ticker(identifier, identSuffix, message) logger2.ticker(identifier, identSuffix, message) } @@ -45,18 +49,18 @@ class MultiLogger( logger1.close() logger2.close() } - override def reportPrefix(s: String): Unit = { + private[mill] override def reportPrefix(s: String): Unit = { logger1.reportPrefix(s) logger2.reportPrefix(s) } override def rawOutputStream: PrintStream = systemStreams.out - override def endTicker(): Unit = { + private[mill] override def endTicker(): Unit = { logger1.endTicker() logger2.endTicker() } - override def globalTicker(s: String): Unit = { + private[mill] override def globalTicker(s: String): Unit = { logger1.globalTicker(s) logger2.globalTicker(s) } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 4cf1ebe3501..6f4a9deaba5 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -96,6 +96,7 @@ private[mill] class MultilinePromptLogger( } } + def streamsAwaitPumperEmpty(): Unit = streams.awaitPumperEmpty() private val seenIdentifiers = collection.mutable.Map.empty[String, (String, String)] private val reportedIdentifiers = collection.mutable.Set.empty[String] override def ticker(identifier: String, identSuffix: String, message: String): Unit = @@ -139,6 +140,9 @@ object MultilinePromptLogger { systemStreams0.in ) + def awaitPumperEmpty(): Unit = { + while (pipe.input.available() != 0) Thread.sleep(10) + } object pumper extends ProxyStream.Pumper(pipe.input, systemStreams0.out, systemStreams0.err) { object PumperState extends Enumeration { val init, prompt, cleared = Value diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 7b14a570e92..893a651ed92 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -52,7 +52,11 @@ class PrefixLogger( logger0.error(infoColor(context) + s) } override def ticker(s: String): Unit = logger0.ticker(context + tickerContext + s) - override def ticker(identifier: String, identSuffix: String, message: String): Unit = + private[mill] override def ticker( + identifier: String, + identSuffix: String, + message: String + ): Unit = logger0.ticker(identifier, identSuffix, message) override def debug(s: String): Unit = { if (debugEnabled) reportPrefix(context0) @@ -67,11 +71,11 @@ class PrefixLogger( outStream0 = Some(outStream), errStream0 = Some(systemStreams.err) ) - override def reportPrefix(s: String): Unit = { + private[mill] override def reportPrefix(s: String): Unit = { logger0.reportPrefix(s) } - override def endTicker(): Unit = logger0.endTicker() - override def globalTicker(s: String): Unit = logger0.globalTicker(s) + private[mill] override def endTicker(): Unit = logger0.endTicker() + private[mill] override def globalTicker(s: String): Unit = logger0.globalTicker(s) } object PrefixLogger { diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 6282bfce264..9ddf1caa9bb 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -17,16 +17,20 @@ class ProxyLogger(logger: Logger) extends Logger { def info(s: String): Unit = logger.info(s) def error(s: String): Unit = logger.error(s) def ticker(s: String): Unit = logger.ticker(s) - override def ticker(identifier: String, identSuffix: String, message: String): Unit = + private[mill] override def ticker( + identifier: String, + identSuffix: String, + message: String + ): Unit = logger.ticker(identifier, identSuffix, message) def debug(s: String): Unit = logger.debug(s) override def debugEnabled: Boolean = logger.debugEnabled override def close(): Unit = logger.close() - override def reportPrefix(s: String): Unit = logger.reportPrefix(s) + private[mill] override def reportPrefix(s: String): Unit = logger.reportPrefix(s) override def rawOutputStream: PrintStream = logger.rawOutputStream - override def endTicker(): Unit = logger.endTicker() - override def globalTicker(s: String): Unit = logger.globalTicker(s) + private[mill] override def endTicker(): Unit = logger.endTicker() + private[mill] override def globalTicker(s: String): Unit = logger.globalTicker(s) } diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala index d0f721c6a29..258304dd62e 100644 --- a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala @@ -27,8 +27,12 @@ object MultilinePromptLoggerTests extends TestSuite { (baos, promptLogger, prefixLogger) } - def check(baos: ByteArrayOutputStream, width: Int = 80)(expected: String*) = { - Thread.sleep(200) + def check( + promptLogger: MultilinePromptLogger, + baos: ByteArrayOutputStream, + width: Int = 80 + )(expected: String*) = { + promptLogger.streamsAwaitPumperEmpty() val finalBaos = new ByteArrayOutputStream() val pumper = new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) @@ -63,7 +67,7 @@ object MultilinePromptLoggerTests extends TestSuite { now += 10000 promptLogger.close() - check(baos, width = 999 /*log file has no line wrapping*/ )( + check(promptLogger, baos, width = 999 /*log file has no line wrapping*/ )( // Make sure that the first time a prefix is reported, // we print the verbose prefix along with the ticker string "[1/456] my-task", @@ -93,8 +97,8 @@ object MultilinePromptLoggerTests extends TestSuite { promptLogger.globalTicker("123/456") promptLogger.refreshPrompt() - check(baos)( - " 123/456 ============================ TITLE ================================= ", + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " ) promptLogger.ticker("[1]", "[1/456]", "my-task") @@ -102,39 +106,72 @@ object MultilinePromptLoggerTests extends TestSuite { prefixLogger.outputStream.println("HELLO") - promptLogger.refreshPrompt() - check(baos)( + promptLogger.refreshPrompt() // Need to call `refreshPrompt()` for prompt to change + // First time we log with the prefix `[1]`, make sure we print out the title line + // `[1/456] my-task` so the viewer knows what `[1]` refers to + check(promptLogger, baos)( "[1/456] my-task", "[1] HELLO", " 123/456 ============================ TITLE ============================== 10s", "[1] my-task 10s" ) + prefixLogger.outputStream.println("WORLD") + // Prompt doesn't change, no need to call `refreshPrompt()` for it to be + // re-rendered below the latest prefixed output. Subsequent log line with `[1]` + // prefix does not re-render title line `[1/456] ...` + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + " 123/456 ============================ TITLE ============================== 10s", + "[1] my-task 10s" + ) + val t = new Thread(() => { + val newPrefixLogger = new PrefixLogger(promptLogger, "[2]") + newPrefixLogger.ticker("[2]", "[2/456]", "my-task-new") + newPrefixLogger.errorStream.println("I AM COW") + newPrefixLogger.errorStream.println("HEAR ME MOO") + }) + t.start() + t.join() promptLogger.refreshPrompt() - check(baos)( + check(promptLogger, baos)( "[1/456] my-task", "[1] HELLO", "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", " 123/456 ============================ TITLE ============================== 10s", - "[1] my-task 10s" + "[1] my-task 10s", + "[2] my-task-new " ) + promptLogger.endTicker() now += 10000 promptLogger.refreshPrompt() - check(baos)( + check(promptLogger, baos)( "[1/456] my-task", "[1] HELLO", "[1] WORLD", - " 123/456 ============================ TITLE ============================== 20s" + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + " 123/456 ============================ TITLE ============================== 20s", + "[2] my-task-new 10s" ) now += 10000 promptLogger.close() - check(baos)( + check(promptLogger, baos)( "[1/456] my-task", "[1] HELLO", "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", "123/456 ============================== TITLE ============================== 30s" ) } From 9989a08ea4dfffe4b8255f5986c2981ae440bad9 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 15:28:31 +0800 Subject: [PATCH 093/116] . --- main/api/src/mill/api/Logger.scala | 7 ++++++- main/src/mill/main/MainModule.scala | 6 ++++-- main/util/src/mill/util/MultiLogger.scala | 2 ++ main/util/src/mill/util/MultilinePromptLogger.scala | 9 +++++++-- main/util/src/mill/util/PrefixLogger.scala | 2 ++ main/util/src/mill/util/ProxyLogger.scala | 1 + scalalib/src/mill/scalalib/ScalaModule.scala | 2 +- 7 files changed, 23 insertions(+), 6 deletions(-) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 76346e8ca86..fd7d1be764e 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -60,5 +60,10 @@ trait Logger { def debugEnabled: Boolean = false def close(): Unit = () - def withPaused[T](t: => T) = t + + /** + * Used to disable the terminal UI prompt without a certain block of code so you + * can run stuff like REPLs or other output-sensitive code in a clean terminal + */ + def withPromptPaused[T](t: => T) = t } diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index 54421527574..048d1056486 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -6,7 +6,7 @@ import mill.api.{Ctx, Logger, PathRef, Result} import mill.eval.{Evaluator, EvaluatorPaths, Terminal} import mill.resolve.{Resolve, SelectMode} import mill.resolve.SelectMode.Separated -import mill.util.Watchable +import mill.util.{AnsiNav, Watchable} import pprint.{Renderer, Tree, Truncated} import scala.collection.mutable @@ -49,7 +49,9 @@ object MainModule { case Right((watched, Right(res))) => val output = f(res) watched.foreach(watch0) - log.rawOutputStream.println(output.render(indent = 2)) + log.withPromptPaused{ + log.rawOutputStream.println(output.render(indent = 2)) + } Result.Success(output) } } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index e3dfb112e2d..f5a1e997edd 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -64,6 +64,8 @@ class MultiLogger( logger1.globalTicker(s) logger2.globalTicker(s) } + + override def withPromptPaused[T](t: => T): T = logger1.withPromptPaused(logger2.withPromptPaused(t)) } class MultiStream(stream1: OutputStream, stream2: OutputStream) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 6f4a9deaba5..ae480dff76e 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -71,9 +71,14 @@ private[mill] class MultilinePromptLogger( def refreshPrompt(): Unit = state.refreshPrompt() if (enableTicker && autoUpdate) promptUpdaterThread.start() - override def withPaused[T](t: => T): T = { + override def withPromptPaused[T](t: => T): T = { paused = true - try t + + try { + // Clear the prompt so the code in `t` has a blank terminal to work with + rawOutputStream.write(AnsiNav.clearScreen(0).getBytes) + t + } finally paused = false } diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 893a651ed92..ea337f22883 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -76,6 +76,8 @@ class PrefixLogger( } private[mill] override def endTicker(): Unit = logger0.endTicker() private[mill] override def globalTicker(s: String): Unit = logger0.globalTicker(s) + + override def withPromptPaused[T](t: => T): T = logger0.withPromptPaused(t) } object PrefixLogger { diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 9ddf1caa9bb..e4bd67a6ab7 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -33,4 +33,5 @@ class ProxyLogger(logger: Logger) extends Logger { override def rawOutputStream: PrintStream = logger.rawOutputStream private[mill] override def endTicker(): Unit = logger.endTicker() private[mill] override def globalTicker(s: String): Unit = logger.globalTicker(s) + override def withPromptPaused[T](t: => T): T = logger.withPromptPaused(t) } diff --git a/scalalib/src/mill/scalalib/ScalaModule.scala b/scalalib/src/mill/scalalib/ScalaModule.scala index ed20f824ae5..7ec5170f716 100644 --- a/scalalib/src/mill/scalalib/ScalaModule.scala +++ b/scalalib/src/mill/scalalib/ScalaModule.scala @@ -437,7 +437,7 @@ trait ScalaModule extends JavaModule with TestModule.ScalaModuleBase { outer => Result.Failure("console needs to be run with the -i/--interactive flag") } else { val useJavaCp = "-usejavacp" - T.log.withPaused { + T.log.withPromptPaused { SystemStreams.withStreams(SystemStreams.original) { Jvm.runSubprocess( mainClass = From 35c06383627180c8d255cec8764aff8662b9915e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 15:35:17 +0800 Subject: [PATCH 094/116] . --- main/src/mill/main/MainModule.scala | 4 ++-- main/util/src/mill/util/MultiLogger.scala | 3 ++- main/util/src/mill/util/MultilinePromptLogger.scala | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index 048d1056486..965e6860e93 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -6,7 +6,7 @@ import mill.api.{Ctx, Logger, PathRef, Result} import mill.eval.{Evaluator, EvaluatorPaths, Terminal} import mill.resolve.{Resolve, SelectMode} import mill.resolve.SelectMode.Separated -import mill.util.{AnsiNav, Watchable} +import mill.util.Watchable import pprint.{Renderer, Tree, Truncated} import scala.collection.mutable @@ -49,7 +49,7 @@ object MainModule { case Right((watched, Right(res))) => val output = f(res) watched.foreach(watch0) - log.withPromptPaused{ + log.withPromptPaused { log.rawOutputStream.println(output.render(indent = 2)) } Result.Success(output) diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index f5a1e997edd..9e5c43a9910 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -65,7 +65,8 @@ class MultiLogger( logger2.globalTicker(s) } - override def withPromptPaused[T](t: => T): T = logger1.withPromptPaused(logger2.withPromptPaused(t)) + override def withPromptPaused[T](t: => T): T = + logger1.withPromptPaused(logger2.withPromptPaused(t)) } class MultiStream(stream1: OutputStream, stream2: OutputStream) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index ae480dff76e..39b64826c18 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -78,8 +78,7 @@ private[mill] class MultilinePromptLogger( // Clear the prompt so the code in `t` has a blank terminal to work with rawOutputStream.write(AnsiNav.clearScreen(0).getBytes) t - } - finally paused = false + } finally paused = false } def info(s: String): Unit = synchronized { systemStreams.err.println(s) } From 4fdfdbfffc8a421ace4b8f9fa2f8d224ca83acdd Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 21:21:31 +0800 Subject: [PATCH 095/116] use raw outputstreams in withPromptPaused --- main/src/mill/main/MainModule.scala | 27 +++++++++++++------ .../src/mill/util/MultilinePromptLogger.scala | 4 ++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index 965e6860e93..c80b31e73ec 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -50,7 +50,7 @@ object MainModule { val output = f(res) watched.foreach(watch0) log.withPromptPaused { - log.rawOutputStream.println(output.render(indent = 2)) + println(output.render(indent = 2)) } Result.Success(output) } @@ -71,7 +71,9 @@ trait MainModule extends BaseModule0 { */ def version(): Command[String] = Target.command { val res = BuildInfo.millVersion - println(res) + Task.log.withPromptPaused{ + println(res) + } res } @@ -89,7 +91,9 @@ trait MainModule extends BaseModule0 { case Left(err) => Result.Failure(err) case Right(resolvedSegmentsList) => val resolvedStrings = resolvedSegmentsList.map(_.render) - resolvedStrings.sorted.foreach(Target.log.outputStream.println) + Task.log.withPromptPaused{ + resolvedStrings.sorted.foreach(println) + } Result.Success(resolvedStrings) } } @@ -103,7 +107,9 @@ trait MainModule extends BaseModule0 { case Left(err) => Result.Failure(err) case Right(success) => val renderedTasks = success.map(_.segments.render) - renderedTasks.foreach(Target.log.outputStream.println) + Task.log.withPromptPaused { + renderedTasks.foreach(println) + } Result.Success(renderedTasks) } } @@ -166,8 +172,9 @@ trait MainModule extends BaseModule0 { val labels = list .collect { case n: NamedTask[_] => n.ctx.segments.render } - labels.foreach(Target.log.outputStream.println(_)) - + Task.log.withPromptPaused { + labels.foreach(println) + } Result.Success(labels) } } @@ -286,7 +293,9 @@ trait MainModule extends BaseModule0 { for { str <- truncated ++ Iterator("\n") } sb.append(str) sb.toString() }).mkString("\n") - Target.log.outputStream.println(output) + Task.log.withPromptPaused { + println(output) + } fansi.Str(output).plainText } } @@ -432,7 +441,9 @@ trait MainModule extends BaseModule0 { in.put((rs, allRs, ctx.dest)) val res = out.take() res.map { v => - println(upickle.default.write(v.map(_.path.toString()), indent = 2)) + ctx.log.withPromptPaused { + println(upickle.default.write(v.map(_.path.toString()), indent = 2)) + } v } } diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index 39b64826c18..d2a485d6d58 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -77,7 +77,9 @@ private[mill] class MultilinePromptLogger( try { // Clear the prompt so the code in `t` has a blank terminal to work with rawOutputStream.write(AnsiNav.clearScreen(0).getBytes) - t + SystemStreams.withStreams(SystemStreams.original){ + t + } } finally paused = false } From 4965555893b2dc6e214b4a504eb00b4fcfd873e1 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Tue, 24 Sep 2024 21:49:52 +0800 Subject: [PATCH 096/116] . --- main/api/src/mill/api/Logger.scala | 2 ++ main/eval/src/mill/eval/EvaluatorCore.scala | 2 +- main/src/mill/main/MainModule.scala | 4 ++-- main/util/src/mill/util/DummyLogger.scala | 1 + main/util/src/mill/util/FileLogger.scala | 1 + main/util/src/mill/util/MultiLogger.scala | 2 ++ main/util/src/mill/util/MultilinePromptLogger.scala | 4 ++-- main/util/src/mill/util/PrefixLogger.scala | 2 ++ main/util/src/mill/util/PrintLogger.scala | 2 +- main/util/src/mill/util/ProxyLogger.scala | 2 ++ 10 files changed, 16 insertions(+), 6 deletions(-) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index fd7d1be764e..7a4bf952bac 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -66,4 +66,6 @@ trait Logger { * can run stuff like REPLs or other output-sensitive code in a clean terminal */ def withPromptPaused[T](t: => T) = t + + def enableTicker: Boolean = false } diff --git a/main/eval/src/mill/eval/EvaluatorCore.scala b/main/eval/src/mill/eval/EvaluatorCore.scala index 9aeb68e470f..8e556bf06a0 100644 --- a/main/eval/src/mill/eval/EvaluatorCore.scala +++ b/main/eval/src/mill/eval/EvaluatorCore.scala @@ -122,7 +122,7 @@ private[mill] trait EvaluatorCore extends GroupEvaluator { val contextLogger = PrefixLogger( out = logger, - context = countMsg, + context = if (logger.enableTicker) countMsg else "", tickerContext = GroupEvaluator.dynamicTickerPrefix.value ) diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index c80b31e73ec..be400e81c5d 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -71,7 +71,7 @@ trait MainModule extends BaseModule0 { */ def version(): Command[String] = Target.command { val res = BuildInfo.millVersion - Task.log.withPromptPaused{ + Task.log.withPromptPaused { println(res) } res @@ -91,7 +91,7 @@ trait MainModule extends BaseModule0 { case Left(err) => Result.Failure(err) case Right(resolvedSegmentsList) => val resolvedStrings = resolvedSegmentsList.map(_.render) - Task.log.withPromptPaused{ + Task.log.withPromptPaused { resolvedStrings.sorted.foreach(println) } Result.Success(resolvedStrings) diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index 14685983ea5..082101ca5f2 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -19,4 +19,5 @@ object DummyLogger extends Logger { def ticker(s: String) = () def debug(s: String) = () override val debugEnabled: Boolean = false + } diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index 6c67d1fc1fd..e23d4ea03f2 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -54,4 +54,5 @@ class FileLogger( outputStream.close() } override def rawOutputStream: PrintStream = outputStream + } diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 9e5c43a9910..09b0e09139c 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -67,6 +67,8 @@ class MultiLogger( override def withPromptPaused[T](t: => T): T = logger1.withPromptPaused(logger2.withPromptPaused(t)) + + override def enableTicker: Boolean = logger1.enableTicker || logger2.enableTicker } class MultiStream(stream1: OutputStream, stream2: OutputStream) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultilinePromptLogger.scala index d2a485d6d58..1035b512d1a 100644 --- a/main/util/src/mill/util/MultilinePromptLogger.scala +++ b/main/util/src/mill/util/MultilinePromptLogger.scala @@ -16,7 +16,7 @@ import MultilinePromptLoggerUtil._ private[mill] class MultilinePromptLogger( override val colored: Boolean, - val enableTicker: Boolean, + override val enableTicker: Boolean, override val infoColor: fansi.Attrs, override val errorColor: fansi.Attrs, systemStreams0: SystemStreams, @@ -77,7 +77,7 @@ private[mill] class MultilinePromptLogger( try { // Clear the prompt so the code in `t` has a blank terminal to work with rawOutputStream.write(AnsiNav.clearScreen(0).getBytes) - SystemStreams.withStreams(SystemStreams.original){ + SystemStreams.withStreams(SystemStreams.original) { t } } finally paused = false diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index ea337f22883..38a4ca5da59 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -78,6 +78,8 @@ class PrefixLogger( private[mill] override def globalTicker(s: String): Unit = logger0.globalTicker(s) override def withPromptPaused[T](t: => T): T = logger0.withPromptPaused(t) + + override def enableTicker = logger0.enableTicker } object PrefixLogger { diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index f8b2f965fdd..de06c9c4ac2 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -5,7 +5,7 @@ import mill.api.SystemStreams class PrintLogger( override val colored: Boolean, - val enableTicker: Boolean, + override val enableTicker: Boolean, override val infoColor: fansi.Attrs, override val errorColor: fansi.Attrs, val systemStreams: SystemStreams, diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index e4bd67a6ab7..adc1d7dcd39 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -34,4 +34,6 @@ class ProxyLogger(logger: Logger) extends Logger { private[mill] override def endTicker(): Unit = logger.endTicker() private[mill] override def globalTicker(s: String): Unit = logger.globalTicker(s) override def withPromptPaused[T](t: => T): T = logger.withPromptPaused(t) + + override def enableTicker = logger.enableTicker } From 7f6c71303354eaecdcd2f4c1a37f66c1ff0700a6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 08:47:59 +0800 Subject: [PATCH 097/116] rename --- .../{MultilinePromptLogger.scala => MultiLinePromptLogger.scala} | 0 ...ePromptLoggerTests.scala => MultiLinePromptLoggerTests$.scala} | 0 ...oggerUtilTests.scala => MultiLinePromptLoggerUtilTests$.scala} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename main/util/src/mill/util/{MultilinePromptLogger.scala => MultiLinePromptLogger.scala} (100%) rename main/util/test/src/mill/util/{MultilinePromptLoggerTests.scala => MultiLinePromptLoggerTests$.scala} (100%) rename main/util/test/src/mill/util/{MultilinePromptLoggerUtilTests.scala => MultiLinePromptLoggerUtilTests$.scala} (100%) diff --git a/main/util/src/mill/util/MultilinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala similarity index 100% rename from main/util/src/mill/util/MultilinePromptLogger.scala rename to main/util/src/mill/util/MultiLinePromptLogger.scala diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala similarity index 100% rename from main/util/test/src/mill/util/MultilinePromptLoggerTests.scala rename to main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala diff --git a/main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala similarity index 100% rename from main/util/test/src/mill/util/MultilinePromptLoggerUtilTests.scala rename to main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala From b042c775113393286a7b88ee2b06190a7de5aa18 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 08:50:32 +0800 Subject: [PATCH 098/116] rename --- main/util/src/mill/util/MultiLinePromptLogger.scala | 6 +++--- .../src/mill/util/MultiLinePromptLoggerTests$.scala | 10 +++++----- .../mill/util/MultiLinePromptLoggerUtilTests$.scala | 4 ++-- runner/src/mill/runner/MillMain.scala | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index 1035b512d1a..b7cd9613950 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -14,7 +14,7 @@ import pprint.Util.literalize import java.io._ import MultilinePromptLoggerUtil._ -private[mill] class MultilinePromptLogger( +private[mill] class MultiLinePromptLogger( override val colored: Boolean, override val enableTicker: Boolean, override val infoColor: fansi.Attrs, @@ -27,7 +27,7 @@ private[mill] class MultilinePromptLogger( autoUpdate: Boolean = true ) extends ColorLogger with AutoCloseable { override def toString: String = s"MultilinePromptLogger(${literalize(titleText)}" - import MultilinePromptLogger._ + import MultiLinePromptLogger._ private var termDimensions: (Option[Int], Option[Int]) = (None, None) @@ -124,7 +124,7 @@ private[mill] class MultilinePromptLogger( def systemStreams = streams.systemStreams } -object MultilinePromptLogger { +object MultiLinePromptLogger { private class Streams( enableTicker: Boolean, diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala index 258304dd62e..a7af20fe506 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala @@ -5,13 +5,13 @@ import mill.main.client.ProxyStream import utest._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} -object MultilinePromptLoggerTests extends TestSuite { +object MultiLinePromptLoggerTests$ extends TestSuite { def setup(now: () => Long, terminfoPath: os.Path) = { val baos = new ByteArrayOutputStream() val baosOut = new PrintStream(new ProxyStream.Output(baos, ProxyStream.OUT)) val baosErr = new PrintStream(new ProxyStream.Output(baos, ProxyStream.ERR)) - val promptLogger = new MultilinePromptLogger( + val promptLogger = new MultiLinePromptLogger( colored = false, enableTicker = true, infoColor = fansi.Attrs.Empty, @@ -28,9 +28,9 @@ object MultilinePromptLoggerTests extends TestSuite { } def check( - promptLogger: MultilinePromptLogger, - baos: ByteArrayOutputStream, - width: Int = 80 + promptLogger: MultiLinePromptLogger, + baos: ByteArrayOutputStream, + width: Int = 80 )(expected: String*) = { promptLogger.streamsAwaitPumperEmpty() val finalBaos = new ByteArrayOutputStream() diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala index ee88f087c09..d5e9a517280 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala @@ -4,7 +4,7 @@ import utest._ import scala.collection.immutable.SortedMap import MultilinePromptLoggerUtil._ -object MultilinePromptLoggerUtilTests extends TestSuite { +object MultiLinePromptLoggerUtilTests$ extends TestSuite { val tests = Tests { test("lastIndexOfNewline") { @@ -108,7 +108,7 @@ object MultilinePromptLoggerUtilTests extends TestSuite { } test("renderPrompt") { - import MultilinePromptLogger._ + import MultiLinePromptLogger._ val now = System.currentTimeMillis() test("simple") { val rendered = renderPrompt( diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index e600cbcc261..739e5218d07 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -9,7 +9,7 @@ import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal} import mill.bsp.{BspContext, BspServerResult} import mill.main.BuildInfo import mill.main.client.ServerFiles -import mill.util.{MultilinePromptLogger, PrintLogger} +import mill.util.{MultiLinePromptLogger, PrintLogger} import java.lang.reflect.InvocationTargetException import scala.util.control.NonFatal @@ -331,7 +331,7 @@ object MillMain { printLoggerState ) } else { - new MultilinePromptLogger( + new MultiLinePromptLogger( colored = colored, enableTicker = enableTicker.getOrElse(true), infoColor = colors.info, From ae88d2b2489048838e0eafed4b7710a27dbf751e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 09:10:40 +0800 Subject: [PATCH 099/116] remove unnecessary sleep --- main/util/src/mill/util/MultiLinePromptLogger.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index b7cd9613950..8734b17d74e 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -124,7 +124,7 @@ private[mill] class MultiLinePromptLogger( def systemStreams = streams.systemStreams } -object MultiLinePromptLogger { +private[mill] object MultiLinePromptLogger { private class Streams( enableTicker: Boolean, @@ -180,7 +180,6 @@ object MultiLinePromptLogger { val pumperThread = new Thread(pumper) pumperThread.start() - Thread.sleep(100) def close(): Unit = { // Close the write side of the pipe first but do not close the read side, so // the `pumperThread` can continue reading remaining text in the pipe buffer From 93f6fae35d0aa05db53c8ce8b8b8c698580eed7d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 09:29:35 +0800 Subject: [PATCH 100/116] . --- main/util/src/mill/util/MultiLinePromptLogger.scala | 2 +- .../src/mill/util/MultiLinePromptLoggerUtil.scala | 12 +++++++++++- testkit/src/mill/testkit/ExampleTester.scala | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index 8734b17d74e..0849ae1e8ac 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -258,7 +258,7 @@ private[mill] object MultiLinePromptLogger { val now = currentTimeMillis() sOpt match { - case None => statuses.get(threadId).foreach(_.removedTimeMillis = now) + case None => statuses.updateWith(threadId)(_.map(_.copy(removedTimeMillis = now))) case Some(s) => statuses(threadId) = Status(now, s, Long.MaxValue) } } diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala index f07d016f365..037a3028ba6 100644 --- a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -38,7 +38,17 @@ private object MultilinePromptLoggerUtil { */ val statusRemovalRemoveDelayMillis = 2000 - private[mill] case class Status(startTimeMillis: Long, text: String, var removedTimeMillis: Long) + + private[mill] case class Status(text: String, startTimeMillis: Long, removedTimeMillis: Long){ + def shouldRender(now: Long): Option[String] = { + if (removedTimeMillis == Long.MaxValue) { + Option.when(now - startTimeMillis > statusRemovalHideDelayMillis)(text) + } else{ + if (now - removedTimeMillis > statusRemovalRemoveDelayMillis) None + else Option.when(now - startTimeMillis > statusRemovalHideDelayMillis)(text) + } + } + } private[mill] val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes diff --git a/testkit/src/mill/testkit/ExampleTester.scala b/testkit/src/mill/testkit/ExampleTester.scala index e4feeac583b..65fbafd3b1e 100644 --- a/testkit/src/mill/testkit/ExampleTester.scala +++ b/testkit/src/mill/testkit/ExampleTester.scala @@ -102,6 +102,7 @@ class ExampleTester( ): Unit = { val commandStr = commandStr0 match { case s"mill $rest" => s"./mill --disable-ticker $rest" + case s"./mill $rest" => s"./mill --disable-ticker $rest" case s"curl $rest" => s"curl --retry 5 --retry-all-errors $rest" case s => s } From 5f748647985a43a6931cc84980bc47c7590f11a2 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 11:53:07 +0800 Subject: [PATCH 101/116] wip --- .../src/mill/util/MultiLinePromptLogger.scala | 31 +- .../mill/util/MultiLinePromptLoggerUtil.scala | 40 ++- ...scala => MultiLinePromptLoggerTests.scala} | 116 ++++++- .../MultiLinePromptLoggerUtilTests$.scala | 320 ------------------ .../util/MultiLinePromptLoggerUtilTests.scala | 306 +++++++++++++++++ .../src/mill/util/TestTerminalTests.scala | 10 +- 6 files changed, 471 insertions(+), 352 deletions(-) rename main/util/test/src/mill/util/{MultiLinePromptLoggerTests$.scala => MultiLinePromptLoggerTests.scala} (63%) delete mode 100644 main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala create mode 100644 main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index 0849ae1e8ac..3a9abb77994 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -76,10 +76,15 @@ private[mill] class MultiLinePromptLogger( try { // Clear the prompt so the code in `t` has a blank terminal to work with + outputStream.flush() + errorStream.flush() rawOutputStream.write(AnsiNav.clearScreen(0).getBytes) - SystemStreams.withStreams(SystemStreams.original) { + val res = SystemStreams.withStreams(SystemStreams.original) { t } + SystemStreams.original.out.flush() + SystemStreams.original.err.flush() + res } finally paused = false } @@ -208,8 +213,8 @@ private[mill] object MultiLinePromptLogger { private def updatePromptBytes(ending: Boolean = false) = { val now = currentTimeMillis() for (k <- statuses.keySet) { - val removedTime = statuses(k).removedTimeMillis - if (now - removedTime > statusRemovalRemoveDelayMillis) { + val removedTime = statuses(k).beginTransitionTime + if (statuses(k).next.isEmpty && (now - removedTime > statusRemovalRemoveDelayMillis)) { statuses.remove(k) } } @@ -257,9 +262,22 @@ private[mill] object MultiLinePromptLogger { val threadId = Thread.currentThread().getId.toInt val now = currentTimeMillis() - sOpt match { - case None => statuses.updateWith(threadId)(_.map(_.copy(removedTimeMillis = now))) - case Some(s) => statuses(threadId) = Status(now, s, Long.MaxValue) + val sOptEntry = sOpt.map(StatusEntry(_, now)) + statuses.updateWith(threadId){ + case None => Some(Status(sOptEntry, now, None)) + case Some(existing) => + Some( + // If already performing a transition, do not update the `prevTransitionTime` + // since we do not want to delay the transition that is already in progress + if (existing.beginTransitionTime >= now) existing.copy(next = sOptEntry) + else { + existing.copy( + next = sOptEntry, + beginTransitionTime = now, + prev = existing.next + ) + } + ) } } @@ -268,6 +286,7 @@ private[mill] object MultiLinePromptLogger { // For non-interactive jobs, we only want to print the new prompt if the contents // differs from the previous prompt, since the prompts do not overwrite each other // in log files and printing large numbers of identical prompts is spammy and useless + lazy val statusesHashCode = statuses.hashCode if (consoleDims()._1.nonEmpty || statusesHashCode != lastRenderedPromptHash) { lastRenderedPromptHash = statusesHashCode diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala index 037a3028ba6..177e5f761af 100644 --- a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -39,16 +39,14 @@ private object MultilinePromptLoggerUtil { val statusRemovalRemoveDelayMillis = 2000 - private[mill] case class Status(text: String, startTimeMillis: Long, removedTimeMillis: Long){ - def shouldRender(now: Long): Option[String] = { - if (removedTimeMillis == Long.MaxValue) { - Option.when(now - startTimeMillis > statusRemovalHideDelayMillis)(text) - } else{ - if (now - removedTimeMillis > statusRemovalRemoveDelayMillis) None - else Option.when(now - startTimeMillis > statusRemovalHideDelayMillis)(text) - } - } - } + private[mill] case class StatusEntry(text: String, startTimeMillis: Long) + + /** + * Represents a line in the prompt. Stores up to two separate [[StatusEntry]]s, because + * we want to buffer up status transitions to debounce them. Which status entry is currently + * shown depends on the [[beginTransitionTime]] and other heuristics + */ + private[mill] case class Status(next: Option[StatusEntry], beginTransitionTime: Long, prev: Option[StatusEntry]) private[mill] val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes @@ -93,14 +91,24 @@ private object MultilinePromptLoggerUtil { val headerSuffix = renderSeconds(now - startTimeMillis) val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth, ending, interactive) + val body0 = statuses - .map { + .flatMap { case (threadId, status) => - if (now - status.removedTimeMillis > statusRemovalHideDelayMillis) "" - else splitShorten( - status.text + " " + renderSeconds(now - status.startTimeMillis), - maxWidth - ) + // For statuses that have completed transitioning from Some to None, continue + // rendering them as an empty line for `statusRemovalRemoveDelayMillis` to try + // and maintain prompt height and stop it from bouncing up and down + if ( + status.prev.nonEmpty && + status.next.isEmpty && + status.beginTransitionTime + statusRemovalHideDelayMillis < now && + status.beginTransitionTime > now - statusRemovalRemoveDelayMillis + ){ + Some("") + } else { + val textOpt = if (status.beginTransitionTime + statusRemovalHideDelayMillis < now) status.next else status.prev + textOpt.map(t => splitShorten(t.text + " " + renderSeconds(now - t.startTimeMillis), maxWidth)) + } } // For non-interactive jobs, we do not need to preserve the height of the prompt // between renderings, since consecutive prompts do not appear at the same place diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala similarity index 63% rename from main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala rename to main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index a7af20fe506..c8fcefbdb84 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests$.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -5,7 +5,7 @@ import mill.main.client.ProxyStream import utest._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} -object MultiLinePromptLoggerTests$ extends TestSuite { +object MultiLinePromptLoggerTests extends TestSuite { def setup(now: () => Long, terminfoPath: os.Path) = { val baos = new ByteArrayOutputStream() @@ -40,6 +40,7 @@ object MultiLinePromptLoggerTests$ extends TestSuite { val term = new TestTerminal(width) term.writeAll(finalBaos.toString) val lines = term.grid + assert(lines == expected) } @@ -87,7 +88,8 @@ object MultiLinePromptLoggerTests$ extends TestSuite { "======================================================================================================================", // Closing the prompt prints the prompt one last time with an updated time elapsed "123/456 ================================================== TITLE ================================================= 30s", - "======================================================================================================================" + "======================================================================================================================", + "" ) } @@ -127,15 +129,37 @@ object MultiLinePromptLoggerTests$ extends TestSuite { " 123/456 ============================ TITLE ============================== 10s", "[1] my-task 10s" ) - val t = new Thread(() => { + + // Adding new ticker entries doesn't appear immediately, + // Only after some time has passed do we start displaying the new ticker entry, + // to ensure it is meaningful to read and not just something that will flash and disappear + val newTaskThread = new Thread(() => { val newPrefixLogger = new PrefixLogger(promptLogger, "[2]") newPrefixLogger.ticker("[2]", "[2/456]", "my-task-new") newPrefixLogger.errorStream.println("I AM COW") newPrefixLogger.errorStream.println("HEAR ME MOO") }) - t.start() - t.join() + newTaskThread.start() + newTaskThread.join() + // For short-lived ticker entries that are removed quickly, they never + // appear in the prompt at all even though they can run and generate logs + val shortLivedSemaphore = new Object() + val shortLivedThread = new Thread(() => { + val newPrefixLogger = new PrefixLogger(promptLogger, "[3]") + newPrefixLogger.ticker("[3]", "[3/456]", "my-task-short-lived") + newPrefixLogger.errorStream.println("hello short lived") + shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) + + newPrefixLogger.errorStream.println("goodbye short lived") + + shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) + newPrefixLogger.endTicker() + }) + shortLivedThread.start() + shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) + + // my-task-new does not appear yet because it is too new promptLogger.refreshPrompt() check(promptLogger, baos)( "[1/456] my-task", @@ -144,14 +168,81 @@ object MultiLinePromptLoggerTests$ extends TestSuite { "[2/456] my-task-new", "[2] I AM COW", "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", " 123/456 ============================ TITLE ============================== 10s", "[1] my-task 10s", - "[2] my-task-new " + ) + + shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) + shortLivedThread.join() + + now += 1000 + + + println("X" * 100) + // my-task-new appears by now, but my-task-short-lived has already ended and never appears + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 11s", + "[1] my-task 11s", + "[2] my-task-new 1s", ) promptLogger.endTicker() + now += 100 + + // Even after ending my-task, it remains on the ticker for a moment before being removed + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 11s", + "[1] my-task 11s", + "[2] my-task-new 1s", + ) + + now += 1000 + + // When my-task disappears from the ticker, it leaves a blank line for a + // moment to preserve the height of the prompt + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 12s", + "[2] my-task-new 2s", + "" + ) + now += 10000 + + // Only after more time does the prompt shrink back promptLogger.refreshPrompt() check(promptLogger, baos)( "[1/456] my-task", @@ -160,8 +251,11 @@ object MultiLinePromptLoggerTests$ extends TestSuite { "[2/456] my-task-new", "[2] I AM COW", "[2] HEAR ME MOO", - " 123/456 ============================ TITLE ============================== 20s", - "[2] my-task-new 10s" + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 22s", + "[2] my-task-new 12s", ) now += 10000 promptLogger.close() @@ -172,7 +266,11 @@ object MultiLinePromptLoggerTests$ extends TestSuite { "[2/456] my-task-new", "[2] I AM COW", "[2] HEAR ME MOO", - "123/456 ============================== TITLE ============================== 30s" + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + "123/456 ============================== TITLE ============================== 32s", + "" ) } } diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala deleted file mode 100644 index d5e9a517280..00000000000 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests$.scala +++ /dev/null @@ -1,320 +0,0 @@ -package mill.util - -import utest._ - -import scala.collection.immutable.SortedMap -import MultilinePromptLoggerUtil._ -object MultiLinePromptLoggerUtilTests$ extends TestSuite { - - val tests = Tests { - test("lastIndexOfNewline") { - // Fuzz test to make sure our custom fast `lastIndexOfNewline` logic behaves - // the same as a slower generic implementation using `.slice.lastIndexOf` - val allSampleByteArrays = Seq[Array[Byte]]( - Array(1), - Array('\n'), - Array(1, 1), - Array(1, '\n'), - Array('\n', 1), - Array('\n', '\n'), - Array(1, 1, 1), - Array(1, 1, '\n'), - Array(1, '\n', 1), - Array('\n', 1, 1), - Array(1, '\n', '\n'), - Array('\n', 1, '\n'), - Array('\n', '\n', 1), - Array('\n', '\n', '\n'), - Array(1, 1, 1, 1), - Array(1, 1, 1, '\n'), - Array(1, 1, '\n', 1), - Array(1, '\n', 1, 1), - Array('\n', 1, 1, 1), - Array(1, 1, '\n', '\n'), - Array(1, '\n', '\n', 1), - Array('\n', '\n', 1, 1), - Array(1, '\n', 1, '\n'), - Array('\n', 1, '\n', 1), - Array('\n', 1, 1, '\n'), - Array('\n', '\n', '\n', 1), - Array('\n', '\n', 1, '\n'), - Array('\n', 1, '\n', '\n'), - Array(1, '\n', '\n', '\n'), - Array('\n', '\n', '\n', '\n') - ) - - for (sample <- allSampleByteArrays) { - for (start <- Range(0, sample.length)) { - for (len <- Range(0, sample.length - start)) { - val found = lastIndexOfNewline(sample, start, len) - val expected0 = sample.slice(start, start + len).lastIndexOf('\n') - val expected = expected0 + start - def assertMsg = - s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" - if (expected0 == -1) Predef.assert(found == -1, assertMsg) - else Predef.assert(found == expected, assertMsg) - } - } - } - } - test("renderHeader") { - - def check(prefix: String, title: String, suffix: String, maxWidth: Int, expected: String) = { - val rendered = renderHeader(prefix, title, suffix, maxWidth) - // leave two spaces open on the left so there's somewhere to park the cursor - assert(expected == rendered) - assert(rendered.length == maxWidth) - rendered - } - test("simple") - check( - "PREFIX", - "TITLE", - "SUFFIX", - 60, - expected = " PREFIX ==================== TITLE ================= SUFFIX" - ) - - test("short") - check( - "PREFIX", - "TITLE", - "SUFFIX", - 40, - expected = " PREFIX ========== TITLE ======= SUFFIX" - ) - - test("shorter") - check( - "PREFIX", - "TITLE", - "SUFFIX", - 25, - expected = " PREFIX ==...==== SUFFIX" - ) - - test("truncateTitle") - check( - "PREFIX", - "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "SUFFIX", - 60, - expected = " PREFIX ====== TITLE_ABCDEF...OPQRSTUVWXYZ ========= SUFFIX" - ) - - test("asymmetricTruncateTitle") - check( - "PREFIX_LONG", - "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "SUFFIX", - 60, - expected = " PREFIX_LONG = TITLE_A...TUVWXYZ =================== SUFFIX" - ) - } - - test("renderPrompt") { - import MultiLinePromptLogger._ - val now = System.currentTimeMillis() - test("simple") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile", - statuses = SortedMap( - 0 -> Status(now - 1000, "hello", Long.MaxValue), - 1 -> Status(now - 2000, "world", Long.MaxValue) - ), - interactive = true - ) - val expected = List( - " 123/456 =============== __.compile ================ 1337s", - "hello 1s", - "world 2s" - ) - assert(rendered == expected) - } - - test("maxWithoutTruncation") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmn", - statuses = SortedMap( - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123", - Long.MaxValue - ), - 1 -> Status(now - 2000, "#2 world", Long.MaxValue), - 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue) - ), - interactive = true - ) - - val expected = List( - " 123/456 ======== __.compile.abcdefghijklmn ======== 1337s", - "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123 1s", - "#2 world 2s", - "#3 i am cow 3s", - "#4 hear me moo 4s", - "#5 i weigh twice as much as you 5s" - ) - assert(rendered == expected) - } - test("minAfterTruncation") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmno", - statuses = SortedMap( - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz12345678901234", - Long.MaxValue - ), - 1 -> Status(now - 2000, "#2 world", Long.MaxValue), - 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), - 5 -> Status(now - 6000, "#6 and I look good on the barbecue", Long.MaxValue) - ), - interactive = true - ) - - val expected = List( - " 123/456 ======= __.compile....efghijklmno ========= 1337s", - "#1 hello1234567890abcefghijk...pqrstuvwxyz12345678901234 1s", - "#2 world 2s", - "#3 i am cow 3s", - "#4 hear me moo 4s", - "... and 2 more threads" - ) - assert(rendered == expected) - } - - test("truncated") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 20, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", - statuses = SortedMap( - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, - Long.MaxValue - ), - 1 -> Status(now - 2000, "#2 world", Long.MaxValue), - 2 -> Status(now - 3000, "#3 i am cow", Long.MaxValue), - 3 -> Status(now - 4000, "#4 hear me moo", Long.MaxValue), - 4 -> Status(now - 5000, "#5 i weigh twice as much as you", Long.MaxValue), - 5 -> Status(now - 6000, "#6 and i look good on the barbecue", Long.MaxValue), - 6 -> Status(now - 7000, "#7 yoghurt curds cream cheese and butter", Long.MaxValue) - ), - interactive = true - ) - val expected = List( - " 123/456 __.compile....z1234567890 ================ 1337s", - "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", - "#2 world 2s", - "#3 i am cow 3s", - "#4 hear me moo 4s", - "... and 3 more threads" - ) - assert(rendered == expected) - } - - test("removalDelay") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 23, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", - statuses = SortedMap( - // Not yet removed, should be shown - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, - Long.MaxValue - ), - // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), - 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), - // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not - // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), - 4 -> Status(now - 5000, "#5i weigh twice", now - statusRemovalRemoveDelayMillis + 1), - 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1), - 6 -> Status( - now - 7000, - "#7 and I look good on the barbecue", - now - statusRemovalRemoveDelayMillis + 1 - ) - ), - interactive = true - ) - - val expected = List( - " 123/456 __.compile....z1234567890 ================ 1337s", - "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", - "#2 world 2s", - "#3 i am cow 3s", - "", - "", - "" - ) - - assert(rendered == expected) - } - - test("nonInteractive") { - val rendered = renderPrompt( - consoleWidth = 60, - consoleHeight = 23, - now = now, - startTimeMillis = now - 1337000, - headerPrefix = "123/456", - titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890", - statuses = SortedMap( - // Not yet removed, should be shown - 0 -> Status( - now - 1000, - "#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, - Long.MaxValue - ), - // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(now - 2000, "#2 world", now - statusRemovalHideDelayMillis + 1), - 2 -> Status(now - 3000, "#3 i am cow", now - statusRemovalHideDelayMillis + 1), - // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not - // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(now - 4000, "#4 hear me moo", now - statusRemovalRemoveDelayMillis + 1), - 4 -> Status(now - 5000, "#5 i weigh twice", now - statusRemovalRemoveDelayMillis + 1), - 5 -> Status(now - 6000, "#6 as much as you", now - statusRemovalRemoveDelayMillis + 1) - ), - interactive = false - ) - - // Make sure the non-interactive prompt does not show the blank lines, - // and it contains a footer line to mark the end of the prompt in logs - val expected = List( - "123/456 __.compile.ab...xyz1234567890 ============== 1337s", - "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", - "#2 world 2s", - "#3 i am cow 3s", - "===========================================================" - ) - assert(rendered == expected) - } - } - } -} diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala new file mode 100644 index 00000000000..cf116d2c2d8 --- /dev/null +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala @@ -0,0 +1,306 @@ +package mill.util + +import utest._ + +import scala.collection.immutable.SortedMap +import MultilinePromptLoggerUtil._ +object MultiLinePromptLoggerUtilTests extends TestSuite { + + val tests = Tests { + test("lastIndexOfNewline") { + // Fuzz test to make sure our custom fast `lastIndexOfNewline` logic behaves + // the same as a slower generic implementation using `.slice.lastIndexOf` + val allSampleByteArrays = Seq[Array[Byte]]( + Array(1), + Array('\n'), + Array(1, 1), + Array(1, '\n'), + Array('\n', 1), + Array('\n', '\n'), + Array(1, 1, 1), + Array(1, 1, '\n'), + Array(1, '\n', 1), + Array('\n', 1, 1), + Array(1, '\n', '\n'), + Array('\n', 1, '\n'), + Array('\n', '\n', 1), + Array('\n', '\n', '\n'), + Array(1, 1, 1, 1), + Array(1, 1, 1, '\n'), + Array(1, 1, '\n', 1), + Array(1, '\n', 1, 1), + Array('\n', 1, 1, 1), + Array(1, 1, '\n', '\n'), + Array(1, '\n', '\n', 1), + Array('\n', '\n', 1, 1), + Array(1, '\n', 1, '\n'), + Array('\n', 1, '\n', 1), + Array('\n', 1, 1, '\n'), + Array('\n', '\n', '\n', 1), + Array('\n', '\n', 1, '\n'), + Array('\n', 1, '\n', '\n'), + Array(1, '\n', '\n', '\n'), + Array('\n', '\n', '\n', '\n') + ) + + for (sample <- allSampleByteArrays) { + for (start <- Range(0, sample.length)) { + for (len <- Range(0, sample.length - start)) { + val found = lastIndexOfNewline(sample, start, len) + val expected0 = sample.slice(start, start + len).lastIndexOf('\n') + val expected = expected0 + start + def assertMsg = + s"found:$found, expected$expected, sample:${sample.toSeq}, start:$start, len:$len" + if (expected0 == -1) Predef.assert(found == -1, assertMsg) + else Predef.assert(found == expected, assertMsg) + } + } + } + } + test("renderHeader") { + + def check(prefix: String, title: String, suffix: String, maxWidth: Int, expected: String) = { + val rendered = renderHeader(prefix, title, suffix, maxWidth) + // leave two spaces open on the left so there's somewhere to park the cursor + assert(expected == rendered) + assert(rendered.length == maxWidth) + rendered + } + test("simple") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 60, + expected = " PREFIX ==================== TITLE ================= SUFFIX" + ) + + test("short") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 40, + expected = " PREFIX ========== TITLE ======= SUFFIX" + ) + + test("shorter") - check( + "PREFIX", + "TITLE", + "SUFFIX", + 25, + expected = " PREFIX ==...==== SUFFIX" + ) + + test("truncateTitle") - check( + "PREFIX", + "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "SUFFIX", + 60, + expected = " PREFIX ====== TITLE_ABCDEF...OPQRSTUVWXYZ ========= SUFFIX" + ) + + test("asymmetricTruncateTitle") - check( + "PREFIX_LONG", + "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "SUFFIX", + 60, + expected = " PREFIX_LONG = TITLE_A...TUVWXYZ =================== SUFFIX" + ) + } + + test("renderPrompt") { + import MultiLinePromptLogger._ + val now = System.currentTimeMillis() + def renderPromptTest(interactive: Boolean, titleText: String = "__.compile")(statuses: (Int, Status)*) = { + renderPrompt( + consoleWidth = 60, + consoleHeight = 20, + now = now, + startTimeMillis = now - 1337000, + headerPrefix = "123/456", + titleText = titleText, + statuses = SortedMap(statuses: _*), + interactive = interactive + ) + + } + test("simple") { + val rendered = renderPromptTest(interactive = true)( + 0 -> Status(Some(StatusEntry("hello", now - 1000)), 0, None), + 1 -> Status(Some(StatusEntry("world", now - 2000)), 0, None) + ) + val expected = List( + " 123/456 =============== __.compile ================ 1337s", + "hello 1s", + "world 2s" + ) + assert(rendered == expected) + } + + test("maxWithoutTruncation") { + val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmn")( + 0 -> Status( + Some(StatusEntry( + "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123", + now - 1000, + )) + + , + 0, + None + ), + 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), + 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), + 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), + 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None) + ) + + val expected = List( + " 123/456 ======== __.compile.abcdefghijklmn ======== 1337s", + "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", + "#5 i weigh twice as much as you 5s" + ) + assert(rendered == expected) + } + test("minAfterTruncation") { + val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmno")( + 0 -> Status( + Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz12345678901234", now - 1000)), + 0, + None + ), + 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), + 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), + 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), + 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None), + 5 -> Status(Some(StatusEntry("#6 and I look good on the barbecue", now - 6000)), 0, None) + ) + + val expected = List( + " 123/456 ======= __.compile....efghijklmno ========= 1337s", + "#1 hello1234567890abcefghijk...pqrstuvwxyz12345678901234 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", + "... and 2 more threads" + ) + assert(rendered == expected) + } + + test("truncated") { + val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + 0 -> Status( + Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), + 0, + None + ), + 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), + 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), + 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), + 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None), + 5 -> Status(Some(StatusEntry("#6 and i look good on the barbecue", now - 6000)), 0, None), + 6 -> Status(Some(StatusEntry("#7 yoghurt curds cream cheese and butter", now - 7000)), 0, None) + ) + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "#4 hear me moo 4s", + "... and 3 more threads" + ) + assert(rendered == expected) + } + + test("removalDelay") { + val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + // Not yet removed, should be shown + 0 -> Status( + Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), + 0, + None + ), + // This is removed but are still within the transition window, so still shown + 2 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#3 i am cow", now - 3000))), + // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not + // yet removed, so rendered as blank lines to prevent terminal jumping around too much + 3 -> Status(None, now - statusRemovalRemoveDelayMillis + 1, Some(StatusEntry("#4 hear me moo", now - 4000))), + 4 -> Status(None, now - statusRemovalRemoveDelayMillis + 1, Some(StatusEntry("#5 i weigh twice", now - 5000))), + 5 -> Status(None, now - statusRemovalRemoveDelayMillis + 1, Some(StatusEntry("#6 as much as you", now - 6000))), + // This one would be rendered as a blank line, but because of the max prompt height + // controlled by the `consoleHeight` it ends up being silently truncated + 6 -> Status( + None, + now - statusRemovalRemoveDelayMillis + 1 , + Some(StatusEntry("#7 and I look good on the barbecue", now - 7000)) + ) + ) + + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#3 i am cow 3s", + "", + "", + "" + ) + + assert(rendered == expected) + } + test("removalFinal") { + val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + // Not yet removed, should be shown + 0 -> Status( + Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), + 0, + None + ), + // This is removed a long time ago, so it is totally removed + 1 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#2 world", now - 2000))), + // This is removed but are still within the transition window, so still shown + 2 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#3 i am cow", now - 3000))), + ) + + val expected = List( + " 123/456 __.compile....z1234567890 ================ 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#3 i am cow 3s", + ) + + assert(rendered == expected) + } + + test("nonInteractive") { + val rendered = renderPromptTest(interactive = false, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + // Not yet removed, should be shown + 0 -> Status( + Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), + 0, + None + ), + // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown + 1 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#2 world", now - 2000))), + 2 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#3 i am cow", now - 3000))), + // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not + // yet removed, so rendered as blank lines to prevent terminal jumping around too much + 3 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#4 hear me moo", now - 4000))), + 4 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#5 i weigh twice", now - 5000))), + 5 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#6 as much as you", now - 6000))) + ) + + // Make sure the non-interactive prompt does not show the blank lines, + // and it contains a footer line to mark the end of the prompt in logs + val expected = List( + "123/456 __.compile.ab...xyz1234567890 ============== 1337s", + "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", + "#2 world 2s", + "#3 i am cow 3s", + "===========================================================" + ) + assert(rendered == expected) + } + } + } +} diff --git a/main/util/test/src/mill/util/TestTerminalTests.scala b/main/util/test/src/mill/util/TestTerminalTests.scala index d57cbcdbd45..667ac1096d2 100644 --- a/main/util/test/src/mill/util/TestTerminalTests.scala +++ b/main/util/test/src/mill/util/TestTerminalTests.scala @@ -52,8 +52,8 @@ class TestTerminal(width: Int) { case normal => // normal text if (normal.head == '\n') { cursorX = 0 - if (cursorY >= grid.length) grid.append("") cursorY += 1 + if (cursorY >= grid.length) grid.append("") } else { if (cursorX == width) { cursorX = 0 @@ -94,6 +94,14 @@ object TestTerminalTests extends TestSuite { "67890" ) } + test("trailingNewline") { + val t = new TestTerminal(width = 10) + t.writeAll("12345\n") + t.grid ==> Seq( + "12345", + "" + ) + } test("wrapNewline") { val t = new TestTerminal(width = 10) t.writeAll("1234567890\nabcdef") From 1ca0c71a3947d264aefeb78dcf80076e4c6a84a9 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 13:02:41 +0800 Subject: [PATCH 102/116] wip --- main/util/src/mill/util/MultiLinePromptLogger.scala | 5 +++-- .../util/test/src/mill/util/MultiLinePromptLoggerTests.scala | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index 3a9abb77994..391bbffa395 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -269,8 +269,9 @@ private[mill] object MultiLinePromptLogger { Some( // If already performing a transition, do not update the `prevTransitionTime` // since we do not want to delay the transition that is already in progress - if (existing.beginTransitionTime >= now) existing.copy(next = sOptEntry) - else { + if (existing.beginTransitionTime + statusRemovalHideDelayMillis > now) { + existing.copy(next = sOptEntry) + } else { existing.copy( next = sOptEntry, beginTransitionTime = now, diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index c8fcefbdb84..a39d97266ce 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -181,7 +181,7 @@ object MultiLinePromptLoggerTests extends TestSuite { now += 1000 - println("X" * 100) + // my-task-new appears by now, but my-task-short-lived has already ended and never appears promptLogger.refreshPrompt() check(promptLogger, baos)( @@ -201,7 +201,7 @@ object MultiLinePromptLoggerTests extends TestSuite { promptLogger.endTicker() - now += 100 + now += 10 // Even after ending my-task, it remains on the ticker for a moment before being removed promptLogger.refreshPrompt() From b3a69f767e79bda86f69495a02676db6b644b9b4 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 13:41:50 +0800 Subject: [PATCH 103/116] wip --- main/eval/src/mill/eval/GroupEvaluator.scala | 453 +++++++++--------- .../src/mill/util/MultiLinePromptLogger.scala | 19 +- .../mill/util/MultiLinePromptLoggerUtil.scala | 2 +- .../util/MultiLinePromptLoggerTests.scala | 51 ++ 4 files changed, 296 insertions(+), 229 deletions(-) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index b9d13554857..985f885e9fa 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -58,161 +58,187 @@ private[mill] trait GroupEvaluator { allTransitiveClassMethods: Map[Class[_], Map[String, Method]] ): GroupEvaluator.Results = { - val externalInputsHash = MurmurHash3.orderedHash( - group.items.flatMap(_.inputs).filter(!group.contains(_)) - .flatMap(results(_).result.asSuccess.map(_.value._2)) - ) + val targetLabel = terminal match { + case Terminal.Task(task) => None + case t: Terminal.Labelled[_] => Some(Terminal.printTerm(t)) + } + // should we log progress? + val logRun = targetLabel.isDefined && { + val inputResults = for { + target <- group.indexed.filterNot(results.contains) + item <- target.inputs.filterNot(group.contains) + } yield results(item).map(_._1) + inputResults.forall(_.result.isInstanceOf[Result.Success[_]]) + } - val sideHashes = MurmurHash3.orderedHash(group.iterator.map(_.sideHash)) - - val scriptsHash = - if (disableCallgraph) 0 - else group - .iterator - .collect { - case namedTask: NamedTask[_] => - val encodedTaskName = encode(namedTask.ctx.segment.pathSegments.head) - val methodOpt = for { - parentCls <- classToTransitiveClasses(namedTask.ctx.enclosingCls).iterator - m <- allTransitiveClassMethods(parentCls).get(encodedTaskName) - } yield m - - val methodClass = methodOpt - .nextOption() - .getOrElse(throw new MillException( - s"Could not detect the parent class of target ${namedTask}. " + - s"Please report this at ${BuildInfo.millReportNewIssueUrl} . " - )) - .getDeclaringClass.getName - - val name = namedTask.ctx.segment.pathSegments.last - val expectedName = methodClass + "#" + name + "()mill.define.Target" - - // We not only need to look up the code hash of the Target method being called, - // but also the code hash of the constructors required to instantiate the Module - // that the Target is being called on. This can be done by walking up the nested - // modules and looking at their constructors (they're `object`s and should each - // have only one) - val allEnclosingModules = Vector.unfold(namedTask.ctx) { - case null => None - case ctx => - ctx.enclosingModule match { - case null => None - case m: mill.define.Module => Some((m, m.millOuterCtx)) - case unknown => - throw new MillException(s"Unknown ctx of target ${namedTask}: $unknown") - } - } + val tickerPrefix = terminal.render.collect { + case targetLabel if logRun && logger.enableTicker => targetLabel + } - val constructorHashes = allEnclosingModules - .map(m => - constructorHashSignatures.get(m.getClass.getName) match { - case Some(Seq((singleMethod, hash))) => hash - case Some(multiple) => throw new MillException( + def withTicker[T](s: Option[String])(t: => T): T = s match { + case None => t + case Some(s) => + logger.ticker(counterMsg, identSuffix, s) + try t + finally logger.endTicker() + } + + withTicker(Some(tickerPrefix)) { + val externalInputsHash = MurmurHash3.orderedHash( + group.items.flatMap(_.inputs).filter(!group.contains(_)) + .flatMap(results(_).result.asSuccess.map(_.value._2)) + ) + + val sideHashes = MurmurHash3.orderedHash(group.iterator.map(_.sideHash)) + + val scriptsHash = + if (disableCallgraph) 0 + else group + .iterator + .collect { + case namedTask: NamedTask[_] => + val encodedTaskName = encode(namedTask.ctx.segment.pathSegments.head) + val methodOpt = for { + parentCls <- classToTransitiveClasses(namedTask.ctx.enclosingCls).iterator + m <- allTransitiveClassMethods(parentCls).get(encodedTaskName) + } yield m + + val methodClass = methodOpt + .nextOption() + .getOrElse(throw new MillException( + s"Could not detect the parent class of target ${namedTask}. " + + s"Please report this at ${BuildInfo.millReportNewIssueUrl} . " + )) + .getDeclaringClass.getName + + val name = namedTask.ctx.segment.pathSegments.last + val expectedName = methodClass + "#" + name + "()mill.define.Target" + + // We not only need to look up the code hash of the Target method being called, + // but also the code hash of the constructors required to instantiate the Module + // that the Target is being called on. This can be done by walking up the nested + // modules and looking at their constructors (they're `object`s and should each + // have only one) + val allEnclosingModules = Vector.unfold(namedTask.ctx) { + case null => None + case ctx => + ctx.enclosingModule match { + case null => None + case m: mill.define.Module => Some((m, m.millOuterCtx)) + case unknown => + throw new MillException(s"Unknown ctx of target ${namedTask}: $unknown") + } + } + + val constructorHashes = allEnclosingModules + .map(m => + constructorHashSignatures.get(m.getClass.getName) match { + case Some(Seq((singleMethod, hash))) => hash + case Some(multiple) => throw new MillException( s"Multiple constructors found for module $m: ${multiple.mkString(",")}" ) - case None => 0 - } - ) + case None => 0 + } + ) - methodCodeHashSignatures.get(expectedName) ++ constructorHashes - } - .flatten - .sum - - val inputsHash = externalInputsHash + sideHashes + classLoaderSigHash + scriptsHash - - terminal match { - case Terminal.Task(task) => - val (newResults, newEvaluated) = evaluateGroup( - group, - results, - inputsHash, - paths = None, - maybeTargetLabel = None, - counterMsg = counterMsg, - identSuffix = identSuffix, - zincProblemReporter, - testReporter, - logger - ) - GroupEvaluator.Results(newResults, newEvaluated.toSeq, null, inputsHash, -1) + methodCodeHashSignatures.get(expectedName) ++ constructorHashes + } + .flatten + .sum + + val inputsHash = externalInputsHash + sideHashes + classLoaderSigHash + scriptsHash + + terminal match { + case Terminal.Task(task) => + val (newResults, newEvaluated) = evaluateGroup( + group, + results, + inputsHash, + paths = None, + maybeTargetLabel = None, + counterMsg = counterMsg, + identSuffix = identSuffix, + zincProblemReporter, + testReporter, + logger + ) + GroupEvaluator.Results(newResults, newEvaluated.toSeq, null, inputsHash, -1) - case labelled: Terminal.Labelled[_] => - val out = - if (!labelled.task.ctx.external) outPath - else externalOutPath + case labelled: Terminal.Labelled[_] => + val out = + if (!labelled.task.ctx.external) outPath + else externalOutPath - val paths = EvaluatorPaths.resolveDestPaths( - out, - Terminal.destSegments(labelled) - ) + val paths = EvaluatorPaths.resolveDestPaths( + out, + Terminal.destSegments(labelled) + ) - val cached = loadCachedJson(logger, inputsHash, labelled, paths) - - val upToDateWorker = loadUpToDateWorker(logger, inputsHash, labelled) - - upToDateWorker.map((_, inputsHash)) orElse cached.flatMap(_._2) match { - case Some((v, hashCode)) => - val res = Result.Success((v, hashCode)) - val newResults: Map[Task[_], TaskResult[(Val, Int)]] = - Map(labelled.task -> TaskResult(res, () => res)) - - GroupEvaluator.Results( - newResults, - Nil, - cached = true, - inputsHash, - -1 - ) - - case _ => - // uncached - if (labelled.task.flushDest) os.remove.all(paths.dest) - - val targetLabel = Terminal.printTerm(terminal) - - val (newResults, newEvaluated) = - GroupEvaluator.dynamicTickerPrefix.withValue(s"[$counterMsg] $targetLabel > ") { - evaluateGroup( - group, - results, - inputsHash, - paths = Some(paths), - maybeTargetLabel = Some(targetLabel), - counterMsg = counterMsg, - identSuffix = identSuffix, - zincProblemReporter, - testReporter, - logger - ) - } + val cached = loadCachedJson(logger, inputsHash, labelled, paths) - newResults(labelled.task) match { - case TaskResult(Result.Failure(_, Some((v, _))), _) => - handleTaskResult(v, v.##, paths.meta, inputsHash, labelled) + val upToDateWorker = loadUpToDateWorker(logger, inputsHash, labelled) - case TaskResult(Result.Success((v, _)), _) => - handleTaskResult(v, v.##, paths.meta, inputsHash, labelled) + upToDateWorker.map((_, inputsHash)) orElse cached.flatMap(_._2) match { + case Some((v, hashCode)) => + val res = Result.Success((v, hashCode)) + val newResults: Map[Task[_], TaskResult[(Val, Int)]] = + Map(labelled.task -> TaskResult(res, () => res)) - case _ => - // Wipe out any cached meta.json file that exists, so - // a following run won't look at the cached metadata file and - // assume it's associated with the possibly-borked state of the - // destPath after an evaluation failure. - os.remove.all(paths.meta) - } + GroupEvaluator.Results( + newResults, + Nil, + cached = true, + inputsHash, + -1 + ) - GroupEvaluator.Results( - newResults, - newEvaluated.toSeq, - cached = if (labelled.task.isInstanceOf[InputImpl[_]]) null else false, - inputsHash, - cached.map(_._1).getOrElse(-1) - ) - } - } + case _ => + // uncached + if (labelled.task.flushDest) os.remove.all(paths.dest) + + + + val (newResults, newEvaluated) = + GroupEvaluator.dynamicTickerPrefix.withValue(s"[$counterMsg] $targetLabel > ") { + evaluateGroup( + group, + results, + inputsHash, + paths = Some(paths), + maybeTargetLabel = targetLabel, + counterMsg = counterMsg, + identSuffix = identSuffix, + zincProblemReporter, + testReporter, + logger + ) + } + + newResults(labelled.task) match { + case TaskResult(Result.Failure(_, Some((v, _))), _) => + handleTaskResult(v, v.##, paths.meta, inputsHash, labelled) + case TaskResult(Result.Success((v, _)), _) => + handleTaskResult(v, v.##, paths.meta, inputsHash, labelled) + + case _ => + // Wipe out any cached meta.json file that exists, so + // a following run won't look at the cached metadata file and + // assume it's associated with the possibly-borked state of the + // destPath after an evaluation failure. + os.remove.all(paths.meta) + } + + GroupEvaluator.Results( + newResults, + newEvaluated.toSeq, + cached = if (labelled.task.isInstanceOf[InputImpl[_]]) null else false, + inputsHash, + cached.map(_._1).getOrElse(-1) + ) + } + } + } } private def evaluateGroup( @@ -228,107 +254,88 @@ private[mill] trait GroupEvaluator { logger: mill.api.Logger ): (Map[Task[_], TaskResult[(Val, Int)]], mutable.Buffer[Task[_]]) = { - def computeAll(enableTicker: Boolean) = { + def computeAll() = { val newEvaluated = mutable.Buffer.empty[Task[_]] val newResults = mutable.Map.empty[Task[_], Result[(Val, Int)]] val nonEvaluatedTargets = group.indexed.filterNot(results.contains) - // should we log progress? - val logRun = maybeTargetLabel.isDefined && { - val inputResults = for { - target <- nonEvaluatedTargets - item <- target.inputs.filterNot(group.contains) - } yield results(item).map(_._1) - inputResults.forall(_.result.isInstanceOf[Result.Success[_]]) - } - val tickerPrefix = maybeTargetLabel.collect { - case targetLabel if logRun && enableTicker => targetLabel - } - def withTicker[T](s: Option[String])(t: => T): T = s match { - case None => t - case Some(s) => - logger.ticker(counterMsg, identSuffix, s) - try t - finally logger.endTicker() + val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { + override def ticker(s: String): Unit = { + if (enableTicker) super.ticker(s) + else () // do nothing + } + + override def rawOutputStream: PrintStream = logger.rawOutputStream } - withTicker(tickerPrefix) { - val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { - override def ticker(s: String): Unit = { - if (enableTicker) super.ticker(s) - else () // do nothing - } - override def rawOutputStream: PrintStream = logger.rawOutputStream + var usedDest = Option.empty[os.Path] + for (task <- nonEvaluatedTargets) { + newEvaluated.append(task) + val targetInputValues = task.inputs + .map { x => newResults.getOrElse(x, results(x).result) } + .collect { case Result.Success((v, _)) => v } + + def makeDest() = this.synchronized { + paths match { + case Some(dest) => + if (usedDest.isEmpty) os.makeDir.all(dest.dest) + usedDest = Some(dest.dest) + dest.dest + + case None => throw new Exception("No `dest` folder available here") + } } - var usedDest = Option.empty[os.Path] - for (task <- nonEvaluatedTargets) { - newEvaluated.append(task) - val targetInputValues = task.inputs - .map { x => newResults.getOrElse(x, results(x).result) } - .collect { case Result.Success((v, _)) => v } - - def makeDest() = this.synchronized { - paths match { - case Some(dest) => - if (usedDest.isEmpty) os.makeDir.all(dest.dest) - usedDest = Some(dest.dest) - dest.dest - - case None => throw new Exception("No `dest` folder available here") + val res = { + if (targetInputValues.length != task.inputs.length) Result.Skipped + else { + val args = new mill.api.Ctx( + args = targetInputValues.map(_.value).toIndexedSeq, + dest0 = () => makeDest(), + log = multiLogger, + home = home, + env = env, + reporter = reporter, + testReporter = testReporter, + workspace = workspace, + systemExit = systemExit + ) with mill.api.Ctx.Jobs { + override def jobs: Int = effectiveThreadCount } - } - val res = { - if (targetInputValues.length != task.inputs.length) Result.Skipped - else { - val args = new mill.api.Ctx( - args = targetInputValues.map(_.value).toIndexedSeq, - dest0 = () => makeDest(), - log = multiLogger, - home = home, - env = env, - reporter = reporter, - testReporter = testReporter, - workspace = workspace, - systemExit = systemExit - ) with mill.api.Ctx.Jobs { - override def jobs: Int = effectiveThreadCount - } - - os.dynamicPwdFunction.withValue(() => makeDest()) { - mill.api.SystemStreams.withStreams(multiLogger.systemStreams) { - try task.evaluate(args).map(Val(_)) - catch { - case f: Result.Failing[Val] => f - case NonFatal(e) => - Result.Exception( - e, - new OuterStack(new Exception().getStackTrace.toIndexedSeq) - ) - } + os.dynamicPwdFunction.withValue(() => makeDest()) { + mill.api.SystemStreams.withStreams(multiLogger.systemStreams) { + try task.evaluate(args).map(Val(_)) + catch { + case f: Result.Failing[Val] => f + case NonFatal(e) => + Result.Exception( + e, + new OuterStack(new Exception().getStackTrace.toIndexedSeq) + ) } } } } - - newResults(task) = for (v <- res) yield { - ( - v, - if (task.isInstanceOf[Worker[_]]) inputsHash - else v.## - ) - } } - multiLogger.close() - (newResults, newEvaluated) + newResults(task) = for (v <- res) yield { + ( + v, + if (task.isInstanceOf[Worker[_]]) inputsHash + else v.## + ) + } } + + multiLogger.close() + (newResults, newEvaluated) } - val (newResults, newEvaluated) = computeAll(enableTicker = true) + + val (newResults, newEvaluated) = computeAll() if (!failFast) maybeTargetLabel.foreach { targetLabel => val taskFailed = newResults.exists(task => !task._2.isInstanceOf[Success[_]]) @@ -340,7 +347,7 @@ private[mill] trait GroupEvaluator { ( newResults .map { case (k, v) => - val recalc = () => computeAll(enableTicker = false)._1.apply(k) + val recalc = () => computeAll()._1.apply(k) val taskResult = TaskResult(v, recalc) (k, taskResult) } diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index 391bbffa395..d73f2de469a 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -262,16 +262,25 @@ private[mill] object MultiLinePromptLogger { val threadId = Thread.currentThread().getId.toInt val now = currentTimeMillis() + def stillTransitioning(status: Status) = { + status.beginTransitionTime + statusRemovalHideDelayMillis > now + } val sOptEntry = sOpt.map(StatusEntry(_, now)) statuses.updateWith(threadId){ - case None => Some(Status(sOptEntry, now, None)) + case None => + statuses.find{case (k, v) => v.next.isEmpty && stillTransitioning(v)} match{ + case Some((reusableKey, reusableValue)) => + statuses.remove(reusableKey) + Some(reusableValue.copy(next = sOptEntry)) + case None => Some(Status(sOptEntry, now, None)) + } + case Some(existing) => Some( - // If already performing a transition, do not update the `prevTransitionTime` + // If still performing a transition, do not update the `prevTransitionTime` // since we do not want to delay the transition that is already in progress - if (existing.beginTransitionTime + statusRemovalHideDelayMillis > now) { - existing.copy(next = sOptEntry) - } else { + if (stillTransitioning(existing)) existing.copy(next = sOptEntry) + else { existing.copy( next = sOptEntry, beginTransitionTime = now, diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala index 177e5f761af..4dd72b24dd1 100644 --- a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -28,7 +28,7 @@ private object MultilinePromptLoggerUtil { * is even more distracting than changing the contents of a line, so we want to minimize * those occurrences even further. */ - val statusRemovalHideDelayMillis = 500 + val statusRemovalHideDelayMillis = 250 /** * How long to wait before actually removing the blank line left by a removed status diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index a39d97266ce..5d63c7105ef 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -273,5 +273,56 @@ object MultiLinePromptLoggerTests extends TestSuite { "" ) } + + test("sequentialShortLived") { + // Make sure that when we have multiple sequential tasks being run on different threads, + // we still end up showing some kind of task in progress in the ticker, even though the + // tasks on each thread are short-lived enough they would not normally get shown if run + // alone. + @volatile var now = 0L + val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) + + promptLogger.globalTicker("123/456") + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " + ) + promptLogger.ticker("[1]", "[1/456]", "my-task") + + now += 100 + + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " + ) + + promptLogger.endTicker() + + val newTaskThread = new Thread(() => { + promptLogger.ticker("[2]", "[2/456]", "my-task-new") + now += 100 + promptLogger.endTicker() + }) + newTaskThread.start() + newTaskThread.join() + + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " + ) + + + val newTaskThread2 = new Thread(() => { + promptLogger.ticker("[2]", "[2/456]", "my-task-new") + now += 100 + }) + newTaskThread2.start() + newTaskThread2.join() + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= ", + "[2] my-task-new " + ) + } } } From 705757003972e00e9cf7fa89230fdd77eedd1908 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 13:48:35 +0800 Subject: [PATCH 104/116] wip --- main/eval/src/mill/eval/GroupEvaluator.scala | 10 +- .../src/mill/util/MultiLinePromptLogger.scala | 4 +- .../mill/util/MultiLinePromptLoggerUtil.scala | 25 ++- .../util/MultiLinePromptLoggerTests.scala | 17 +- .../util/MultiLinePromptLoggerUtilTests.scala | 172 ++++++++++++------ 5 files changed, 149 insertions(+), 79 deletions(-) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 985f885e9fa..91b21fef524 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -62,7 +62,7 @@ private[mill] trait GroupEvaluator { case Terminal.Task(task) => None case t: Terminal.Labelled[_] => Some(Terminal.printTerm(t)) } - // should we log progress? + // should we log progress? val logRun = targetLabel.isDefined && { val inputResults = for { target <- group.indexed.filterNot(results.contains) @@ -135,8 +135,8 @@ private[mill] trait GroupEvaluator { constructorHashSignatures.get(m.getClass.getName) match { case Some(Seq((singleMethod, hash))) => hash case Some(multiple) => throw new MillException( - s"Multiple constructors found for module $m: ${multiple.mkString(",")}" - ) + s"Multiple constructors found for module $m: ${multiple.mkString(",")}" + ) case None => 0 } ) @@ -196,8 +196,6 @@ private[mill] trait GroupEvaluator { // uncached if (labelled.task.flushDest) os.remove.all(paths.dest) - - val (newResults, newEvaluated) = GroupEvaluator.dynamicTickerPrefix.withValue(s"[$counterMsg] $targetLabel > ") { evaluateGroup( @@ -260,7 +258,6 @@ private[mill] trait GroupEvaluator { val nonEvaluatedTargets = group.indexed.filterNot(results.contains) - val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { override def ticker(s: String): Unit = { if (enableTicker) super.ticker(s) @@ -334,7 +331,6 @@ private[mill] trait GroupEvaluator { (newResults, newEvaluated) } - val (newResults, newEvaluated) = computeAll() if (!failFast) maybeTargetLabel.foreach { targetLabel => diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index d73f2de469a..5ea762ed307 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -266,9 +266,9 @@ private[mill] object MultiLinePromptLogger { status.beginTransitionTime + statusRemovalHideDelayMillis > now } val sOptEntry = sOpt.map(StatusEntry(_, now)) - statuses.updateWith(threadId){ + statuses.updateWith(threadId) { case None => - statuses.find{case (k, v) => v.next.isEmpty && stillTransitioning(v)} match{ + statuses.find { case (k, v) => v.next.isEmpty && stillTransitioning(v) } match { case Some((reusableKey, reusableValue)) => statuses.remove(reusableKey) Some(reusableValue.copy(next = sOptEntry)) diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala index 4dd72b24dd1..ff7ff38ad0f 100644 --- a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -38,7 +38,6 @@ private object MultilinePromptLoggerUtil { */ val statusRemovalRemoveDelayMillis = 2000 - private[mill] case class StatusEntry(text: String, startTimeMillis: Long) /** @@ -46,7 +45,11 @@ private object MultilinePromptLoggerUtil { * we want to buffer up status transitions to debounce them. Which status entry is currently * shown depends on the [[beginTransitionTime]] and other heuristics */ - private[mill] case class Status(next: Option[StatusEntry], beginTransitionTime: Long, prev: Option[StatusEntry]) + private[mill] case class Status( + next: Option[StatusEntry], + beginTransitionTime: Long, + prev: Option[StatusEntry] + ) private[mill] val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes @@ -99,15 +102,19 @@ private object MultilinePromptLoggerUtil { // rendering them as an empty line for `statusRemovalRemoveDelayMillis` to try // and maintain prompt height and stop it from bouncing up and down if ( - status.prev.nonEmpty && - status.next.isEmpty && - status.beginTransitionTime + statusRemovalHideDelayMillis < now && - status.beginTransitionTime > now - statusRemovalRemoveDelayMillis - ){ + status.prev.nonEmpty && + status.next.isEmpty && + status.beginTransitionTime + statusRemovalHideDelayMillis < now && + status.beginTransitionTime > now - statusRemovalRemoveDelayMillis + ) { Some("") } else { - val textOpt = if (status.beginTransitionTime + statusRemovalHideDelayMillis < now) status.next else status.prev - textOpt.map(t => splitShorten(t.text + " " + renderSeconds(now - t.startTimeMillis), maxWidth)) + val textOpt = if (status.beginTransitionTime + statusRemovalHideDelayMillis < now) + status.next + else status.prev + textOpt.map(t => + splitShorten(t.text + " " + renderSeconds(now - t.startTimeMillis), maxWidth) + ) } } // For non-interactive jobs, we do not need to preserve the height of the prompt diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index 5d63c7105ef..ed626600039 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -28,9 +28,9 @@ object MultiLinePromptLoggerTests extends TestSuite { } def check( - promptLogger: MultiLinePromptLogger, - baos: ByteArrayOutputStream, - width: Int = 80 + promptLogger: MultiLinePromptLogger, + baos: ByteArrayOutputStream, + width: Int = 80 )(expected: String*) = { promptLogger.streamsAwaitPumperEmpty() val finalBaos = new ByteArrayOutputStream() @@ -172,7 +172,7 @@ object MultiLinePromptLoggerTests extends TestSuite { "[3] hello short lived", "[3] goodbye short lived", " 123/456 ============================ TITLE ============================== 10s", - "[1] my-task 10s", + "[1] my-task 10s" ) shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) @@ -180,8 +180,6 @@ object MultiLinePromptLoggerTests extends TestSuite { now += 1000 - - // my-task-new appears by now, but my-task-short-lived has already ended and never appears promptLogger.refreshPrompt() check(promptLogger, baos)( @@ -196,7 +194,7 @@ object MultiLinePromptLoggerTests extends TestSuite { "[3] goodbye short lived", " 123/456 ============================ TITLE ============================== 11s", "[1] my-task 11s", - "[2] my-task-new 1s", + "[2] my-task-new 1s" ) promptLogger.endTicker() @@ -217,7 +215,7 @@ object MultiLinePromptLoggerTests extends TestSuite { "[3] goodbye short lived", " 123/456 ============================ TITLE ============================== 11s", "[1] my-task 11s", - "[2] my-task-new 1s", + "[2] my-task-new 1s" ) now += 1000 @@ -255,7 +253,7 @@ object MultiLinePromptLoggerTests extends TestSuite { "[3] hello short lived", "[3] goodbye short lived", " 123/456 ============================ TITLE ============================== 22s", - "[2] my-task-new 12s", + "[2] my-task-new 12s" ) now += 10000 promptLogger.close() @@ -311,7 +309,6 @@ object MultiLinePromptLoggerTests extends TestSuite { " 123/456 ============================ TITLE ================================= " ) - val newTaskThread2 = new Thread(() => { promptLogger.ticker("[2]", "[2/456]", "my-task-new") now += 100 diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala index cf116d2c2d8..ea5103b5cb8 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala @@ -110,7 +110,10 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { test("renderPrompt") { import MultiLinePromptLogger._ val now = System.currentTimeMillis() - def renderPromptTest(interactive: Boolean, titleText: String = "__.compile")(statuses: (Int, Status)*) = { + def renderPromptTest( + interactive: Boolean, + titleText: String = "__.compile" + )(statuses: (Int, Status)*) = { renderPrompt( consoleWidth = 60, consoleHeight = 20, @@ -125,8 +128,8 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { } test("simple") { val rendered = renderPromptTest(interactive = true)( - 0 -> Status(Some(StatusEntry("hello", now - 1000)), 0, None), - 1 -> Status(Some(StatusEntry("world", now - 2000)), 0, None) + 0 -> Status(Some(StatusEntry("hello", now - 1000)), 0, None), + 1 -> Status(Some(StatusEntry("world", now - 2000)), 0, None) ) val expected = List( " 123/456 =============== __.compile ================ 1337s", @@ -137,22 +140,21 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { } test("maxWithoutTruncation") { - val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmn")( - 0 -> Status( - Some(StatusEntry( - "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123", - now - 1000, - )) - - , - 0, - None - ), - 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), - 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), - 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), - 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None) - ) + val rendered = + renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmn")( + 0 -> Status( + Some(StatusEntry( + "#1 hello1234567890abcefghijklmnopqrstuvwxyz1234567890123", + now - 1000 + )), + 0, + None + ), + 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), + 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), + 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), + 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None) + ) val expected = List( " 123/456 ======== __.compile.abcdefghijklmn ======== 1337s", @@ -165,18 +167,26 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { assert(rendered == expected) } test("minAfterTruncation") { - val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmno")( - 0 -> Status( - Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz12345678901234", now - 1000)), - 0, - None - ), - 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), - 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), - 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), - 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None), - 5 -> Status(Some(StatusEntry("#6 and I look good on the barbecue", now - 6000)), 0, None) - ) + val rendered = + renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmno")( + 0 -> Status( + Some(StatusEntry( + "#1 hello1234567890abcefghijklmnopqrstuvwxyz12345678901234", + now - 1000 + )), + 0, + None + ), + 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), + 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), + 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), + 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None), + 5 -> Status( + Some(StatusEntry("#6 and I look good on the barbecue", now - 6000)), + 0, + None + ) + ) val expected = List( " 123/456 ======= __.compile....efghijklmno ========= 1337s", @@ -190,18 +200,25 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { } test("truncated") { - val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + val rendered = renderPromptTest( + interactive = true, + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890" + )( 0 -> Status( Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), - 0, - None + 0, + None ), 1 -> Status(Some(StatusEntry("#2 world", now - 2000)), 0, None), 2 -> Status(Some(StatusEntry("#3 i am cow", now - 3000)), 0, None), 3 -> Status(Some(StatusEntry("#4 hear me moo", now - 4000)), 0, None), 4 -> Status(Some(StatusEntry("#5 i weigh twice as much as you", now - 5000)), 0, None), 5 -> Status(Some(StatusEntry("#6 and i look good on the barbecue", now - 6000)), 0, None), - 6 -> Status(Some(StatusEntry("#7 yoghurt curds cream cheese and butter", now - 7000)), 0, None) + 6 -> Status( + Some(StatusEntry("#7 yoghurt curds cream cheese and butter", now - 7000)), + 0, + None + ) ) val expected = List( " 123/456 __.compile....z1234567890 ================ 1337s", @@ -215,7 +232,10 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { } test("removalDelay") { - val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + val rendered = renderPromptTest( + interactive = true, + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890" + )( // Not yet removed, should be shown 0 -> Status( Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), @@ -223,17 +243,33 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { None ), // This is removed but are still within the transition window, so still shown - 2 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#3 i am cow", now - 3000))), + 2 -> Status( + None, + now - statusRemovalHideDelayMillis + 1, + Some(StatusEntry("#3 i am cow", now - 3000)) + ), // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(None, now - statusRemovalRemoveDelayMillis + 1, Some(StatusEntry("#4 hear me moo", now - 4000))), - 4 -> Status(None, now - statusRemovalRemoveDelayMillis + 1, Some(StatusEntry("#5 i weigh twice", now - 5000))), - 5 -> Status(None, now - statusRemovalRemoveDelayMillis + 1, Some(StatusEntry("#6 as much as you", now - 6000))), + 3 -> Status( + None, + now - statusRemovalRemoveDelayMillis + 1, + Some(StatusEntry("#4 hear me moo", now - 4000)) + ), + 4 -> Status( + None, + now - statusRemovalRemoveDelayMillis + 1, + Some(StatusEntry("#5 i weigh twice", now - 5000)) + ), + 5 -> Status( + None, + now - statusRemovalRemoveDelayMillis + 1, + Some(StatusEntry("#6 as much as you", now - 6000)) + ), // This one would be rendered as a blank line, but because of the max prompt height // controlled by the `consoleHeight` it ends up being silently truncated 6 -> Status( None, - now - statusRemovalRemoveDelayMillis + 1 , + now - statusRemovalRemoveDelayMillis + 1, Some(StatusEntry("#7 and I look good on the barbecue", now - 7000)) ) ) @@ -250,7 +286,10 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { assert(rendered == expected) } test("removalFinal") { - val rendered = renderPromptTest(interactive = true, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + val rendered = renderPromptTest( + interactive = true, + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890" + )( // Not yet removed, should be shown 0 -> Status( Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), @@ -258,22 +297,33 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { None ), // This is removed a long time ago, so it is totally removed - 1 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#2 world", now - 2000))), + 1 -> Status( + None, + now - statusRemovalRemoveDelayMillis - 1, + Some(StatusEntry("#2 world", now - 2000)) + ), // This is removed but are still within the transition window, so still shown - 2 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#3 i am cow", now - 3000))), + 2 -> Status( + None, + now - statusRemovalHideDelayMillis + 1, + Some(StatusEntry("#3 i am cow", now - 3000)) + ) ) val expected = List( " 123/456 __.compile....z1234567890 ================ 1337s", "#1 hello1234567890abcefghijk...abcefghijklmnopqrstuvwxyz 1s", - "#3 i am cow 3s", + "#3 i am cow 3s" ) assert(rendered == expected) } test("nonInteractive") { - val rendered = renderPromptTest(interactive = false, titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890")( + val rendered = renderPromptTest( + interactive = false, + titleText = "__.compile.abcdefghijklmnopqrstuvwxyz1234567890" + )( // Not yet removed, should be shown 0 -> Status( Some(StatusEntry("#1 hello1234567890abcefghijklmnopqrstuvwxyz" * 3, now - 1000)), @@ -281,13 +331,33 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { None ), // These are removed but are still within the `statusRemovalDelayMillis` window, so still shown - 1 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#2 world", now - 2000))), - 2 -> Status(None, now - statusRemovalHideDelayMillis + 1, Some(StatusEntry("#3 i am cow", now - 3000))), + 1 -> Status( + None, + now - statusRemovalHideDelayMillis + 1, + Some(StatusEntry("#2 world", now - 2000)) + ), + 2 -> Status( + None, + now - statusRemovalHideDelayMillis + 1, + Some(StatusEntry("#3 i am cow", now - 3000)) + ), // Removed but already outside the `statusRemovalDelayMillis` window, not shown, but not // yet removed, so rendered as blank lines to prevent terminal jumping around too much - 3 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#4 hear me moo", now - 4000))), - 4 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#5 i weigh twice", now - 5000))), - 5 -> Status(None, now - statusRemovalRemoveDelayMillis - 1, Some(StatusEntry("#6 as much as you", now - 6000))) + 3 -> Status( + None, + now - statusRemovalRemoveDelayMillis - 1, + Some(StatusEntry("#4 hear me moo", now - 4000)) + ), + 4 -> Status( + None, + now - statusRemovalRemoveDelayMillis - 1, + Some(StatusEntry("#5 i weigh twice", now - 5000)) + ), + 5 -> Status( + None, + now - statusRemovalRemoveDelayMillis - 1, + Some(StatusEntry("#6 as much as you", now - 6000)) + ) ) // Make sure the non-interactive prompt does not show the blank lines, From 5482bfb607cead37deb41a931a7e898e5db9bd41 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 15:41:55 +0800 Subject: [PATCH 105/116] wip --- main/util/src/mill/util/LinePrefixOutputStream.scala | 4 ++-- main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/main/util/src/mill/util/LinePrefixOutputStream.scala b/main/util/src/mill/util/LinePrefixOutputStream.scala index 837dfb1a54a..3145b9ba28e 100644 --- a/main/util/src/mill/util/LinePrefixOutputStream.scala +++ b/main/util/src/mill/util/LinePrefixOutputStream.scala @@ -1,6 +1,6 @@ package mill.util -import java.io.{ByteArrayOutputStream, OutputStream} +import java.io.{ByteArrayOutputStream, FilterOutputStream, OutputStream} /** * Prefixes the first and each new line with a dynamically provided prefix, @@ -14,7 +14,7 @@ class LinePrefixOutputStream( linePrefix: String, out: OutputStream, reportPrefix: () => Unit -) extends OutputStream { +) extends FilterOutputStream(out) { def this(linePrefix: String, out: OutputStream) = this(linePrefix, out, () => ()) private[this] val linePrefixBytes = linePrefix.getBytes("UTF-8") private[this] val linePrefixNonEmpty = linePrefixBytes.length != 0 diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index ed626600039..d57911559f3 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -38,7 +38,8 @@ object MultiLinePromptLoggerTests extends TestSuite { new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) pumper.run() val term = new TestTerminal(width) - term.writeAll(finalBaos.toString) + // Try to normalize behavior between windows and unix + term.writeAll(finalBaos.toString.replace("\r", "")) val lines = term.grid assert(lines == expected) From b7579d1873d71a0a10f4e8b048419d29e38fe7ce Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 15:49:15 +0800 Subject: [PATCH 106/116] avoid using SystemStreams.original due to it's uncapturability in server mode --- main/util/src/mill/util/MultiLinePromptLogger.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index 5ea762ed307..4e7108b695e 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -79,12 +79,9 @@ private[mill] class MultiLinePromptLogger( outputStream.flush() errorStream.flush() rawOutputStream.write(AnsiNav.clearScreen(0).getBytes) - val res = SystemStreams.withStreams(SystemStreams.original) { + SystemStreams.withStreams(systemStreams0) { t } - SystemStreams.original.out.flush() - SystemStreams.original.err.flush() - res } finally paused = false } From c127015ac8867235cfda520e59b5c68fb53b1982 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 16:31:03 +0800 Subject: [PATCH 107/116] disable prompt integration tests on windows due to flakiness --- .../util/MultiLinePromptLoggerTests.scala | 533 +++++++++--------- 1 file changed, 269 insertions(+), 264 deletions(-) diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index d57911559f3..e3362cde03a 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -38,8 +38,6 @@ object MultiLinePromptLoggerTests extends TestSuite { new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) pumper.run() val term = new TestTerminal(width) - // Try to normalize behavior between windows and unix - term.writeAll(finalBaos.toString.replace("\r", "")) val lines = term.grid assert(lines == expected) @@ -47,280 +45,287 @@ object MultiLinePromptLoggerTests extends TestSuite { val tests = Tests { test("nonInteractive") { - var now = 0L - - val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp()) - - promptLogger.globalTicker("123/456") - promptLogger.ticker("[1]", "[1/456]", "my-task") - - now += 10000 - - prefixLogger.outputStream.println("HELLO") - - promptLogger.refreshPrompt() - - prefixLogger.outputStream.println("WORLD") - - promptLogger.endTicker() - - now += 10000 - promptLogger.refreshPrompt() - now += 10000 - promptLogger.close() - - check(promptLogger, baos, width = 999 /*log file has no line wrapping*/ )( - // Make sure that the first time a prefix is reported, - // we print the verbose prefix along with the ticker string - "[1/456] my-task", - // Further `println`s come with the prefix - "[1] HELLO", - // Calling `refreshPrompt()` prints the header with the given `globalTicker` without - // the double space prefix (since it's non-interactive and we don't need space for a cursor), - // the time elapsed, the reported title and ticker, the list of active tickers, followed by the - // footer - "123/456 ================================================== TITLE ================================================= 10s", - "[1] my-task 10s", - "======================================================================================================================", - "[1] WORLD", - // Calling `refreshPrompt()` after closing the ticker shows the prompt without - // the ticker in the list, with an updated time elapsed - "123/456 ================================================== TITLE ================================================= 20s", - "======================================================================================================================", - // Closing the prompt prints the prompt one last time with an updated time elapsed - "123/456 ================================================== TITLE ================================================= 30s", - "======================================================================================================================", - "" - ) + // These tests seem flaky on windows but not sure why + if (!Util.windowsPlatform) { + var now = 0L + + val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp()) + + promptLogger.globalTicker("123/456") + promptLogger.ticker("[1]", "[1/456]", "my-task") + + now += 10000 + + prefixLogger.outputStream.println("HELLO") + + promptLogger.refreshPrompt() + + prefixLogger.outputStream.println("WORLD") + + promptLogger.endTicker() + + now += 10000 + promptLogger.refreshPrompt() + now += 10000 + promptLogger.close() + + check(promptLogger, baos, width = 999 /*log file has no line wrapping*/ )( + // Make sure that the first time a prefix is reported, + // we print the verbose prefix along with the ticker string + "[1/456] my-task", + // Further `println`s come with the prefix + "[1] HELLO", + // Calling `refreshPrompt()` prints the header with the given `globalTicker` without + // the double space prefix (since it's non-interactive and we don't need space for a cursor), + // the time elapsed, the reported title and ticker, the list of active tickers, followed by the + // footer + "123/456 ================================================== TITLE ================================================= 10s", + "[1] my-task 10s", + "======================================================================================================================", + "[1] WORLD", + // Calling `refreshPrompt()` after closing the ticker shows the prompt without + // the ticker in the list, with an updated time elapsed + "123/456 ================================================== TITLE ================================================= 20s", + "======================================================================================================================", + // Closing the prompt prints the prompt one last time with an updated time elapsed + "123/456 ================================================== TITLE ================================================= 30s", + "======================================================================================================================", + "" + ) + } } test("interactive") { - var now = 0L - val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) - - promptLogger.globalTicker("123/456") - promptLogger.refreshPrompt() - check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " - ) - promptLogger.ticker("[1]", "[1/456]", "my-task") - - now += 10000 - - prefixLogger.outputStream.println("HELLO") - - promptLogger.refreshPrompt() // Need to call `refreshPrompt()` for prompt to change - // First time we log with the prefix `[1]`, make sure we print out the title line - // `[1/456] my-task` so the viewer knows what `[1]` refers to - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - " 123/456 ============================ TITLE ============================== 10s", - "[1] my-task 10s" - ) - - prefixLogger.outputStream.println("WORLD") - // Prompt doesn't change, no need to call `refreshPrompt()` for it to be - // re-rendered below the latest prefixed output. Subsequent log line with `[1]` - // prefix does not re-render title line `[1/456] ...` - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - "[1] WORLD", - " 123/456 ============================ TITLE ============================== 10s", - "[1] my-task 10s" - ) - - // Adding new ticker entries doesn't appear immediately, - // Only after some time has passed do we start displaying the new ticker entry, - // to ensure it is meaningful to read and not just something that will flash and disappear - val newTaskThread = new Thread(() => { - val newPrefixLogger = new PrefixLogger(promptLogger, "[2]") - newPrefixLogger.ticker("[2]", "[2/456]", "my-task-new") - newPrefixLogger.errorStream.println("I AM COW") - newPrefixLogger.errorStream.println("HEAR ME MOO") - }) - newTaskThread.start() - newTaskThread.join() - - // For short-lived ticker entries that are removed quickly, they never - // appear in the prompt at all even though they can run and generate logs - val shortLivedSemaphore = new Object() - val shortLivedThread = new Thread(() => { - val newPrefixLogger = new PrefixLogger(promptLogger, "[3]") - newPrefixLogger.ticker("[3]", "[3/456]", "my-task-short-lived") - newPrefixLogger.errorStream.println("hello short lived") + if (!Util.windowsPlatform) { + var now = 0L + val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) + + promptLogger.globalTicker("123/456") + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " + ) + promptLogger.ticker("[1]", "[1/456]", "my-task") + + now += 10000 + + prefixLogger.outputStream.println("HELLO") + + promptLogger.refreshPrompt() // Need to call `refreshPrompt()` for prompt to change + // First time we log with the prefix `[1]`, make sure we print out the title line + // `[1/456] my-task` so the viewer knows what `[1]` refers to + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + " 123/456 ============================ TITLE ============================== 10s", + "[1] my-task 10s" + ) + + prefixLogger.outputStream.println("WORLD") + // Prompt doesn't change, no need to call `refreshPrompt()` for it to be + // re-rendered below the latest prefixed output. Subsequent log line with `[1]` + // prefix does not re-render title line `[1/456] ...` + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + " 123/456 ============================ TITLE ============================== 10s", + "[1] my-task 10s" + ) + + // Adding new ticker entries doesn't appear immediately, + // Only after some time has passed do we start displaying the new ticker entry, + // to ensure it is meaningful to read and not just something that will flash and disappear + val newTaskThread = new Thread(() => { + val newPrefixLogger = new PrefixLogger(promptLogger, "[2]") + newPrefixLogger.ticker("[2]", "[2/456]", "my-task-new") + newPrefixLogger.errorStream.println("I AM COW") + newPrefixLogger.errorStream.println("HEAR ME MOO") + }) + newTaskThread.start() + newTaskThread.join() + + // For short-lived ticker entries that are removed quickly, they never + // appear in the prompt at all even though they can run and generate logs + val shortLivedSemaphore = new Object() + val shortLivedThread = new Thread(() => { + val newPrefixLogger = new PrefixLogger(promptLogger, "[3]") + newPrefixLogger.ticker("[3]", "[3/456]", "my-task-short-lived") + newPrefixLogger.errorStream.println("hello short lived") + shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) + + newPrefixLogger.errorStream.println("goodbye short lived") + + shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) + newPrefixLogger.endTicker() + }) + shortLivedThread.start() + shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) + + // my-task-new does not appear yet because it is too new + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 10s", + "[1] my-task 10s" + ) + shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) + shortLivedThread.join() + + now += 1000 + + // my-task-new appears by now, but my-task-short-lived has already ended and never appears + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 11s", + "[1] my-task 11s", + "[2] my-task-new 1s" + ) - newPrefixLogger.errorStream.println("goodbye short lived") + promptLogger.endTicker() - shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) - newPrefixLogger.endTicker() - }) - shortLivedThread.start() - shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) - - // my-task-new does not appear yet because it is too new - promptLogger.refreshPrompt() - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - "[1] WORLD", - "[2/456] my-task-new", - "[2] I AM COW", - "[2] HEAR ME MOO", - "[3/456] my-task-short-lived", - "[3] hello short lived", - "[3] goodbye short lived", - " 123/456 ============================ TITLE ============================== 10s", - "[1] my-task 10s" - ) - - shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) - shortLivedThread.join() - - now += 1000 - - // my-task-new appears by now, but my-task-short-lived has already ended and never appears - promptLogger.refreshPrompt() - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - "[1] WORLD", - "[2/456] my-task-new", - "[2] I AM COW", - "[2] HEAR ME MOO", - "[3/456] my-task-short-lived", - "[3] hello short lived", - "[3] goodbye short lived", - " 123/456 ============================ TITLE ============================== 11s", - "[1] my-task 11s", - "[2] my-task-new 1s" - ) - - promptLogger.endTicker() - - now += 10 - - // Even after ending my-task, it remains on the ticker for a moment before being removed - promptLogger.refreshPrompt() - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - "[1] WORLD", - "[2/456] my-task-new", - "[2] I AM COW", - "[2] HEAR ME MOO", - "[3/456] my-task-short-lived", - "[3] hello short lived", - "[3] goodbye short lived", - " 123/456 ============================ TITLE ============================== 11s", - "[1] my-task 11s", - "[2] my-task-new 1s" - ) - - now += 1000 - - // When my-task disappears from the ticker, it leaves a blank line for a - // moment to preserve the height of the prompt - promptLogger.refreshPrompt() - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - "[1] WORLD", - "[2/456] my-task-new", - "[2] I AM COW", - "[2] HEAR ME MOO", - "[3/456] my-task-short-lived", - "[3] hello short lived", - "[3] goodbye short lived", - " 123/456 ============================ TITLE ============================== 12s", - "[2] my-task-new 2s", - "" - ) - - now += 10000 - - // Only after more time does the prompt shrink back - promptLogger.refreshPrompt() - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - "[1] WORLD", - "[2/456] my-task-new", - "[2] I AM COW", - "[2] HEAR ME MOO", - "[3/456] my-task-short-lived", - "[3] hello short lived", - "[3] goodbye short lived", - " 123/456 ============================ TITLE ============================== 22s", - "[2] my-task-new 12s" - ) - now += 10000 - promptLogger.close() - check(promptLogger, baos)( - "[1/456] my-task", - "[1] HELLO", - "[1] WORLD", - "[2/456] my-task-new", - "[2] I AM COW", - "[2] HEAR ME MOO", - "[3/456] my-task-short-lived", - "[3] hello short lived", - "[3] goodbye short lived", - "123/456 ============================== TITLE ============================== 32s", - "" - ) + now += 10 + + // Even after ending my-task, it remains on the ticker for a moment before being removed + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 11s", + "[1] my-task 11s", + "[2] my-task-new 1s" + ) + + now += 1000 + + // When my-task disappears from the ticker, it leaves a blank line for a + // moment to preserve the height of the prompt + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 12s", + "[2] my-task-new 2s", + "" + ) + + now += 10000 + + // Only after more time does the prompt shrink back + promptLogger.refreshPrompt() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + " 123/456 ============================ TITLE ============================== 22s", + "[2] my-task-new 12s" + ) + now += 10000 + promptLogger.close() + check(promptLogger, baos)( + "[1/456] my-task", + "[1] HELLO", + "[1] WORLD", + "[2/456] my-task-new", + "[2] I AM COW", + "[2] HEAR ME MOO", + "[3/456] my-task-short-lived", + "[3] hello short lived", + "[3] goodbye short lived", + "123/456 ============================== TITLE ============================== 32s", + "" + ) + } } test("sequentialShortLived") { - // Make sure that when we have multiple sequential tasks being run on different threads, - // we still end up showing some kind of task in progress in the ticker, even though the - // tasks on each thread are short-lived enough they would not normally get shown if run - // alone. - @volatile var now = 0L - val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) - - promptLogger.globalTicker("123/456") - promptLogger.refreshPrompt() - check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " - ) - promptLogger.ticker("[1]", "[1/456]", "my-task") - - now += 100 - - promptLogger.refreshPrompt() - check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " - ) - - promptLogger.endTicker() - - val newTaskThread = new Thread(() => { - promptLogger.ticker("[2]", "[2/456]", "my-task-new") + if (!Util.windowsPlatform) { + // Make sure that when we have multiple sequential tasks being run on different threads, + // we still end up showing some kind of task in progress in the ticker, even though the + // tasks on each thread are short-lived enough they would not normally get shown if run + // alone. + @volatile var now = 0L + val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) + + promptLogger.globalTicker("123/456") + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " + ) + promptLogger.ticker("[1]", "[1/456]", "my-task") + now += 100 - promptLogger.endTicker() - }) - newTaskThread.start() - newTaskThread.join() - promptLogger.refreshPrompt() - check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " - ) + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " + ) - val newTaskThread2 = new Thread(() => { - promptLogger.ticker("[2]", "[2/456]", "my-task-new") - now += 100 - }) - newTaskThread2.start() - newTaskThread2.join() - promptLogger.refreshPrompt() - check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= ", - "[2] my-task-new " - ) + promptLogger.endTicker() + + val newTaskThread = new Thread(() => { + promptLogger.ticker("[2]", "[2/456]", "my-task-new") + now += 100 + promptLogger.endTicker() + }) + newTaskThread.start() + newTaskThread.join() + + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= " + ) + + val newTaskThread2 = new Thread(() => { + promptLogger.ticker("[2]", "[2/456]", "my-task-new") + now += 100 + }) + newTaskThread2.start() + newTaskThread2.join() + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ================================= ", + "[2] my-task-new " + ) + } } } } From 32cc9c4fe0d412ea1a331045ded640551bfcdf8e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Sep 2024 20:35:55 +0800 Subject: [PATCH 108/116] . --- main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index e3362cde03a..b51e593a687 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -38,6 +38,7 @@ object MultiLinePromptLoggerTests extends TestSuite { new ProxyStream.Pumper(new ByteArrayInputStream(baos.toByteArray), finalBaos, finalBaos) pumper.run() val term = new TestTerminal(width) + term.writeAll(finalBaos.toString) val lines = term.grid assert(lines == expected) From c4a957822166062b4aabc0ee7321205a4aee2fbe Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 08:15:20 +0800 Subject: [PATCH 109/116] make prompt based on prefix keys rather than thread IDs --- main/api/src/mill/api/Logger.scala | 6 +-- main/eval/src/mill/eval/GroupEvaluator.scala | 2 +- .../src/mill/util/MultiLinePromptLogger.scala | 18 ++++---- .../mill/util/MultiLinePromptLoggerUtil.scala | 2 +- main/util/src/mill/util/MultiLogger.scala | 6 +-- main/util/src/mill/util/PrefixLogger.scala | 10 ++--- main/util/src/mill/util/ProxyLogger.scala | 10 ++--- .../util/MultiLinePromptLoggerTests.scala | 41 ++++++------------- .../util/MultiLinePromptLoggerUtilTests.scala | 2 +- 9 files changed, 39 insertions(+), 58 deletions(-) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 7a4bf952bac..95e644773a6 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -45,11 +45,11 @@ trait Logger { def error(s: String): Unit def ticker(s: String): Unit private[mill] def reportPrefix(s: String): Unit = () - private[mill] def ticker(identifier: String, identSuffix: String, message: String): Unit = - ticker(s"$identifier $message") + private[mill] def ticker(key: String, identSuffix: String, message: String): Unit = + ticker(s"$key $message") private[mill] def globalTicker(s: String): Unit = () private[mill] def clearAllTickers(): Unit = () - private[mill] def endTicker(): Unit = () + private[mill] def endTicker(key: String): Unit = () def debug(s: String): Unit diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 91b21fef524..a7273b9a2d0 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -80,7 +80,7 @@ private[mill] trait GroupEvaluator { case Some(s) => logger.ticker(counterMsg, identSuffix, s) try t - finally logger.endTicker() + finally logger.endTicker(counterMsg) } withTicker(Some(tickerPrefix)) { diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/MultiLinePromptLogger.scala index 4e7108b695e..15f698e56d4 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/MultiLinePromptLogger.scala @@ -91,9 +91,9 @@ private[mill] class MultiLinePromptLogger( override def globalTicker(s: String): Unit = synchronized { state.updateGlobal(s) } override def clearAllTickers(): Unit = synchronized { state.clearStatuses() } - override def endTicker(): Unit = synchronized { state.updateCurrent(None) } + override def endTicker(key: String): Unit = synchronized { state.updateCurrent(key, None) } - def ticker(s: String): Unit = synchronized { state.updateCurrent(Some(s)) } + def ticker(s: String): Unit = () override def reportPrefix(s: String): Unit = synchronized { if (!reportedIdentifiers(s)) { @@ -107,10 +107,11 @@ private[mill] class MultiLinePromptLogger( def streamsAwaitPumperEmpty(): Unit = streams.awaitPumperEmpty() private val seenIdentifiers = collection.mutable.Map.empty[String, (String, String)] private val reportedIdentifiers = collection.mutable.Set.empty[String] - override def ticker(identifier: String, identSuffix: String, message: String): Unit = + override def ticker(key: String, identSuffix: String, message: String): Unit = synchronized { - seenIdentifiers(identifier) = (identSuffix, message) - super.ticker(infoColor(identifier).toString(), identSuffix, message) + state.updateCurrent(key, Some(s"$key $message")) + seenIdentifiers(key) = (identSuffix, message) + super.ticker(infoColor(key).toString(), identSuffix, message) } def debug(s: String): Unit = synchronized { if (debugEnabled) systemStreams.err.println(s) } @@ -199,7 +200,7 @@ private[mill] object MultiLinePromptLogger { currentTimeMillis: () => Long ) { private var lastRenderedPromptHash = 0 - private val statuses = collection.mutable.SortedMap.empty[Int, Status] + private val statuses = collection.mutable.SortedMap.empty[String, Status] private var headerPrefix = "" // Pre-compute the prelude and current prompt as byte arrays so that @@ -255,15 +256,14 @@ private[mill] object MultiLinePromptLogger { def clearStatuses(): Unit = synchronized { statuses.clear() } def updateGlobal(s: String): Unit = synchronized { headerPrefix = s } - def updateCurrent(sOpt: Option[String]): Unit = synchronized { - val threadId = Thread.currentThread().getId.toInt + def updateCurrent(key: String, sOpt: Option[String]): Unit = synchronized { val now = currentTimeMillis() def stillTransitioning(status: Status) = { status.beginTransitionTime + statusRemovalHideDelayMillis > now } val sOptEntry = sOpt.map(StatusEntry(_, now)) - statuses.updateWith(threadId) { + statuses.updateWith(key) { case None => statuses.find { case (k, v) => v.next.isEmpty && stillTransitioning(v) } match { case Some((reusableKey, reusableValue)) => diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala index ff7ff38ad0f..1ad942c416a 100644 --- a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala +++ b/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala @@ -83,7 +83,7 @@ private object MultilinePromptLoggerUtil { startTimeMillis: Long, headerPrefix: String, titleText: String, - statuses: collection.SortedMap[Int, Status], + statuses: collection.SortedMap[String, Status], interactive: Boolean, ending: Boolean = false ): List[String] = { diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 09b0e09139c..1687edcf781 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -56,9 +56,9 @@ class MultiLogger( override def rawOutputStream: PrintStream = systemStreams.out - private[mill] override def endTicker(): Unit = { - logger1.endTicker() - logger2.endTicker() + private[mill] override def endTicker(key: String): Unit = { + logger1.endTicker(key) + logger2.endTicker(key) } private[mill] override def globalTicker(s: String): Unit = { logger1.globalTicker(s) diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 38a4ca5da59..1f26a418b65 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -53,11 +53,11 @@ class PrefixLogger( } override def ticker(s: String): Unit = logger0.ticker(context + tickerContext + s) private[mill] override def ticker( - identifier: String, - identSuffix: String, - message: String + key: String, + identSuffix: String, + message: String ): Unit = - logger0.ticker(identifier, identSuffix, message) + logger0.ticker(key, identSuffix, message) override def debug(s: String): Unit = { if (debugEnabled) reportPrefix(context0) logger0.debug(infoColor(context) + s) @@ -74,7 +74,7 @@ class PrefixLogger( private[mill] override def reportPrefix(s: String): Unit = { logger0.reportPrefix(s) } - private[mill] override def endTicker(): Unit = logger0.endTicker() + private[mill] override def endTicker(key: String): Unit = logger0.endTicker(key) private[mill] override def globalTicker(s: String): Unit = logger0.globalTicker(s) override def withPromptPaused[T](t: => T): T = logger0.withPromptPaused(t) diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index adc1d7dcd39..deb13f72bbb 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -17,12 +17,8 @@ class ProxyLogger(logger: Logger) extends Logger { def info(s: String): Unit = logger.info(s) def error(s: String): Unit = logger.error(s) def ticker(s: String): Unit = logger.ticker(s) - private[mill] override def ticker( - identifier: String, - identSuffix: String, - message: String - ): Unit = - logger.ticker(identifier, identSuffix, message) + private[mill] override def ticker(key: String, identSuffix: String, message: String): Unit = + logger.ticker(key, identSuffix, message) def debug(s: String): Unit = logger.debug(s) override def debugEnabled: Boolean = logger.debugEnabled @@ -31,7 +27,7 @@ class ProxyLogger(logger: Logger) extends Logger { private[mill] override def reportPrefix(s: String): Unit = logger.reportPrefix(s) override def rawOutputStream: PrintStream = logger.rawOutputStream - private[mill] override def endTicker(): Unit = logger.endTicker() + private[mill] override def endTicker(key: String): Unit = logger.endTicker(key) private[mill] override def globalTicker(s: String): Unit = logger.globalTicker(s) override def withPromptPaused[T](t: => T): T = logger.withPromptPaused(t) diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala index b51e593a687..9715b689f4f 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala @@ -63,7 +63,7 @@ object MultiLinePromptLoggerTests extends TestSuite { prefixLogger.outputStream.println("WORLD") - promptLogger.endTicker() + promptLogger.endTicker("[1]") now += 10000 promptLogger.refreshPrompt() @@ -137,31 +137,17 @@ object MultiLinePromptLoggerTests extends TestSuite { // Adding new ticker entries doesn't appear immediately, // Only after some time has passed do we start displaying the new ticker entry, // to ensure it is meaningful to read and not just something that will flash and disappear - val newTaskThread = new Thread(() => { - val newPrefixLogger = new PrefixLogger(promptLogger, "[2]") - newPrefixLogger.ticker("[2]", "[2/456]", "my-task-new") - newPrefixLogger.errorStream.println("I AM COW") - newPrefixLogger.errorStream.println("HEAR ME MOO") - }) - newTaskThread.start() - newTaskThread.join() + val newPrefixLogger2 = new PrefixLogger(promptLogger, "[2]") + newPrefixLogger2.ticker("[2]", "[2/456]", "my-task-new") + newPrefixLogger2.errorStream.println("I AM COW") + newPrefixLogger2.errorStream.println("HEAR ME MOO") // For short-lived ticker entries that are removed quickly, they never // appear in the prompt at all even though they can run and generate logs - val shortLivedSemaphore = new Object() - val shortLivedThread = new Thread(() => { - val newPrefixLogger = new PrefixLogger(promptLogger, "[3]") - newPrefixLogger.ticker("[3]", "[3/456]", "my-task-short-lived") - newPrefixLogger.errorStream.println("hello short lived") - shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) - - newPrefixLogger.errorStream.println("goodbye short lived") - - shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) - newPrefixLogger.endTicker() - }) - shortLivedThread.start() - shortLivedSemaphore.synchronized(shortLivedSemaphore.wait()) + val newPrefixLogger3 = new PrefixLogger(promptLogger, "[3]") + newPrefixLogger3.ticker("[3]", "[3/456]", "my-task-short-lived") + newPrefixLogger3.errorStream.println("hello short lived") + newPrefixLogger3.errorStream.println("goodbye short lived") // my-task-new does not appear yet because it is too new promptLogger.refreshPrompt() @@ -179,8 +165,7 @@ object MultiLinePromptLoggerTests extends TestSuite { "[1] my-task 10s" ) - shortLivedSemaphore.synchronized(shortLivedSemaphore.notify()) - shortLivedThread.join() + newPrefixLogger3.endTicker("[3]") now += 1000 @@ -201,7 +186,7 @@ object MultiLinePromptLoggerTests extends TestSuite { "[2] my-task-new 1s" ) - promptLogger.endTicker() + promptLogger.endTicker("[1]") now += 10 @@ -300,12 +285,12 @@ object MultiLinePromptLoggerTests extends TestSuite { " 123/456 ============================ TITLE ================================= " ) - promptLogger.endTicker() + promptLogger.endTicker("[1]") val newTaskThread = new Thread(() => { promptLogger.ticker("[2]", "[2/456]", "my-task-new") now += 100 - promptLogger.endTicker() + promptLogger.endTicker("[2]") }) newTaskThread.start() newTaskThread.join() diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala index ea5103b5cb8..f923d1e9eab 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala +++ b/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala @@ -121,7 +121,7 @@ object MultiLinePromptLoggerUtilTests extends TestSuite { startTimeMillis = now - 1337000, headerPrefix = "123/456", titleText = titleText, - statuses = SortedMap(statuses: _*), + statuses = SortedMap(statuses.map{case (k, v) => (k.toString, v)}: _*), interactive = interactive ) From 38386bc9ac0851a11f27149980281fb0c649a75f Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 09:27:30 +0800 Subject: [PATCH 110/116] renames --- main/api/src/mill/api/Logger.scala | 2 +- main/eval/src/mill/eval/GroupEvaluator.scala | 11 ++--------- main/util/src/mill/util/MultiLogger.scala | 6 +++--- main/util/src/mill/util/PrefixLogger.scala | 10 ++++------ ...LinePromptLogger.scala => PromptLogger.scala} | 4 ++-- ...ptLoggerUtil.scala => PromptLoggerUtil.scala} | 0 main/util/src/mill/util/ProxyLogger.scala | 4 ++-- ...LoggerTests.scala => PromptLoggerTests.scala} | 16 ++++++++-------- ...ilTests.scala => PromptLoggerUtilTests.scala} | 2 +- 9 files changed, 23 insertions(+), 32 deletions(-) rename main/util/src/mill/util/{MultiLinePromptLogger.scala => PromptLogger.scala} (98%) rename main/util/src/mill/util/{MultiLinePromptLoggerUtil.scala => PromptLoggerUtil.scala} (100%) rename main/util/test/src/mill/util/{MultiLinePromptLoggerTests.scala => PromptLoggerTests.scala} (95%) rename main/util/test/src/mill/util/{MultiLinePromptLoggerUtilTests.scala => PromptLoggerUtilTests.scala} (99%) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 95e644773a6..67769c34c30 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -45,7 +45,7 @@ trait Logger { def error(s: String): Unit def ticker(s: String): Unit private[mill] def reportPrefix(s: String): Unit = () - private[mill] def ticker(key: String, identSuffix: String, message: String): Unit = + private[mill] def promptLine(key: String, identSuffix: String, message: String): Unit = ticker(s"$key $message") private[mill] def globalTicker(s: String): Unit = () private[mill] def clearAllTickers(): Unit = () diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index a7273b9a2d0..d8e5060c177 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -78,7 +78,7 @@ private[mill] trait GroupEvaluator { def withTicker[T](s: Option[String])(t: => T): T = s match { case None => t case Some(s) => - logger.ticker(counterMsg, identSuffix, s) + logger.promptLine(counterMsg, identSuffix, s) try t finally logger.endTicker(counterMsg) } @@ -258,14 +258,7 @@ private[mill] trait GroupEvaluator { val nonEvaluatedTargets = group.indexed.filterNot(results.contains) - val multiLogger = new ProxyLogger(resolveLogger(paths.map(_.log), logger)) { - override def ticker(s: String): Unit = { - if (enableTicker) super.ticker(s) - else () // do nothing - } - - override def rawOutputStream: PrintStream = logger.rawOutputStream - } + val multiLogger = resolveLogger(paths.map(_.log), logger) var usedDest = Option.empty[os.Path] for (task <- nonEvaluatedTargets) { diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 1687edcf781..a24e22b829c 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -31,13 +31,13 @@ class MultiLogger( logger2.ticker(s) } - private[mill] override def ticker( + private[mill] override def promptLine( identifier: String, identSuffix: String, message: String ): Unit = { - logger1.ticker(identifier, identSuffix, message) - logger2.ticker(identifier, identSuffix, message) + logger1.promptLine(identifier, identSuffix, message) + logger2.promptLine(identifier, identSuffix, message) } def debug(s: String): Unit = { diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 1f26a418b65..6de1c396494 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -52,12 +52,10 @@ class PrefixLogger( logger0.error(infoColor(context) + s) } override def ticker(s: String): Unit = logger0.ticker(context + tickerContext + s) - private[mill] override def ticker( - key: String, - identSuffix: String, - message: String - ): Unit = - logger0.ticker(key, identSuffix, message) + + private[mill] override def promptLine(key: String, identSuffix: String, message: String): Unit = + logger0.promptLine(key, identSuffix, message) + override def debug(s: String): Unit = { if (debugEnabled) reportPrefix(context0) logger0.debug(infoColor(context) + s) diff --git a/main/util/src/mill/util/MultiLinePromptLogger.scala b/main/util/src/mill/util/PromptLogger.scala similarity index 98% rename from main/util/src/mill/util/MultiLinePromptLogger.scala rename to main/util/src/mill/util/PromptLogger.scala index 15f698e56d4..1b554ed14dd 100644 --- a/main/util/src/mill/util/MultiLinePromptLogger.scala +++ b/main/util/src/mill/util/PromptLogger.scala @@ -107,11 +107,11 @@ private[mill] class MultiLinePromptLogger( def streamsAwaitPumperEmpty(): Unit = streams.awaitPumperEmpty() private val seenIdentifiers = collection.mutable.Map.empty[String, (String, String)] private val reportedIdentifiers = collection.mutable.Set.empty[String] - override def ticker(key: String, identSuffix: String, message: String): Unit = + override def promptLine(key: String, identSuffix: String, message: String): Unit = synchronized { state.updateCurrent(key, Some(s"$key $message")) seenIdentifiers(key) = (identSuffix, message) - super.ticker(infoColor(key).toString(), identSuffix, message) + super.promptLine(infoColor(key).toString(), identSuffix, message) } def debug(s: String): Unit = synchronized { if (debugEnabled) systemStreams.err.println(s) } diff --git a/main/util/src/mill/util/MultiLinePromptLoggerUtil.scala b/main/util/src/mill/util/PromptLoggerUtil.scala similarity index 100% rename from main/util/src/mill/util/MultiLinePromptLoggerUtil.scala rename to main/util/src/mill/util/PromptLoggerUtil.scala diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index deb13f72bbb..64b6a2af9d9 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -17,8 +17,8 @@ class ProxyLogger(logger: Logger) extends Logger { def info(s: String): Unit = logger.info(s) def error(s: String): Unit = logger.error(s) def ticker(s: String): Unit = logger.ticker(s) - private[mill] override def ticker(key: String, identSuffix: String, message: String): Unit = - logger.ticker(key, identSuffix, message) + private[mill] override def promptLine(key: String, identSuffix: String, message: String): Unit = + logger.promptLine(key, identSuffix, message) def debug(s: String): Unit = logger.debug(s) override def debugEnabled: Boolean = logger.debugEnabled diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala b/main/util/test/src/mill/util/PromptLoggerTests.scala similarity index 95% rename from main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala rename to main/util/test/src/mill/util/PromptLoggerTests.scala index 9715b689f4f..312c0ac0844 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerTests.scala +++ b/main/util/test/src/mill/util/PromptLoggerTests.scala @@ -5,7 +5,7 @@ import mill.main.client.ProxyStream import utest._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} -object MultiLinePromptLoggerTests extends TestSuite { +object PromptLoggerTests extends TestSuite { def setup(now: () => Long, terminfoPath: os.Path) = { val baos = new ByteArrayOutputStream() @@ -53,7 +53,7 @@ object MultiLinePromptLoggerTests extends TestSuite { val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp()) promptLogger.globalTicker("123/456") - promptLogger.ticker("[1]", "[1/456]", "my-task") + promptLogger.promptLine("[1]", "[1/456]", "my-task") now += 10000 @@ -106,7 +106,7 @@ object MultiLinePromptLoggerTests extends TestSuite { check(promptLogger, baos)( " 123/456 ============================ TITLE ================================= " ) - promptLogger.ticker("[1]", "[1/456]", "my-task") + promptLogger.promptLine("[1]", "[1/456]", "my-task") now += 10000 @@ -138,14 +138,14 @@ object MultiLinePromptLoggerTests extends TestSuite { // Only after some time has passed do we start displaying the new ticker entry, // to ensure it is meaningful to read and not just something that will flash and disappear val newPrefixLogger2 = new PrefixLogger(promptLogger, "[2]") - newPrefixLogger2.ticker("[2]", "[2/456]", "my-task-new") + newPrefixLogger2.promptLine("[2]", "[2/456]", "my-task-new") newPrefixLogger2.errorStream.println("I AM COW") newPrefixLogger2.errorStream.println("HEAR ME MOO") // For short-lived ticker entries that are removed quickly, they never // appear in the prompt at all even though they can run and generate logs val newPrefixLogger3 = new PrefixLogger(promptLogger, "[3]") - newPrefixLogger3.ticker("[3]", "[3/456]", "my-task-short-lived") + newPrefixLogger3.promptLine("[3]", "[3/456]", "my-task-short-lived") newPrefixLogger3.errorStream.println("hello short lived") newPrefixLogger3.errorStream.println("goodbye short lived") @@ -276,7 +276,7 @@ object MultiLinePromptLoggerTests extends TestSuite { check(promptLogger, baos)( " 123/456 ============================ TITLE ================================= " ) - promptLogger.ticker("[1]", "[1/456]", "my-task") + promptLogger.promptLine("[1]", "[1/456]", "my-task") now += 100 @@ -288,7 +288,7 @@ object MultiLinePromptLoggerTests extends TestSuite { promptLogger.endTicker("[1]") val newTaskThread = new Thread(() => { - promptLogger.ticker("[2]", "[2/456]", "my-task-new") + promptLogger.promptLine("[2]", "[2/456]", "my-task-new") now += 100 promptLogger.endTicker("[2]") }) @@ -301,7 +301,7 @@ object MultiLinePromptLoggerTests extends TestSuite { ) val newTaskThread2 = new Thread(() => { - promptLogger.ticker("[2]", "[2/456]", "my-task-new") + promptLogger.promptLine("[2]", "[2/456]", "my-task-new") now += 100 }) newTaskThread2.start() diff --git a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala b/main/util/test/src/mill/util/PromptLoggerUtilTests.scala similarity index 99% rename from main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala rename to main/util/test/src/mill/util/PromptLoggerUtilTests.scala index f923d1e9eab..62bf2244059 100644 --- a/main/util/test/src/mill/util/MultiLinePromptLoggerUtilTests.scala +++ b/main/util/test/src/mill/util/PromptLoggerUtilTests.scala @@ -4,7 +4,7 @@ import utest._ import scala.collection.immutable.SortedMap import MultilinePromptLoggerUtil._ -object MultiLinePromptLoggerUtilTests extends TestSuite { +object PromptLoggerUtilTests extends TestSuite { val tests = Tests { test("lastIndexOfNewline") { From 0a475d67fbe2a83e824e637286509a1455046030 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 10:46:18 +0800 Subject: [PATCH 111/116] . --- bsp/src/mill/bsp/BspContext.scala | 1 + main/api/src/mill/api/Logger.scala | 1 + main/eval/src/mill/eval/GroupEvaluator.scala | 1 - main/util/src/mill/util/DummyLogger.scala | 1 + main/util/src/mill/util/FileLogger.scala | 1 + main/util/src/mill/util/MultiLogger.scala | 5 ++ main/util/src/mill/util/PrefixLogger.scala | 3 +- main/util/src/mill/util/PrintLogger.scala | 1 + main/util/src/mill/util/PromptLogger.scala | 20 ++++-- .../util/src/mill/util/PromptLoggerUtil.scala | 31 ++++++--- main/util/src/mill/util/ProxyLogger.scala | 1 + .../src/mill/util/PromptLoggerTests.scala | 50 +++++++++++--- .../src/mill/util/PromptLoggerUtilTests.scala | 68 ++++++++++++++++--- runner/src/mill/runner/MillMain.scala | 4 +- 14 files changed, 148 insertions(+), 40 deletions(-) diff --git a/bsp/src/mill/bsp/BspContext.scala b/bsp/src/mill/bsp/BspContext.scala index b93ce2d187c..4075be10e89 100644 --- a/bsp/src/mill/bsp/BspContext.scala +++ b/bsp/src/mill/bsp/BspContext.scala @@ -55,6 +55,7 @@ private[mill] class BspContext( override def info(s: String): Unit = streams.err.println(s) override def error(s: String): Unit = streams.err.println(s) override def ticker(s: String): Unit = streams.err.println(s) + override def ticker(key: String, s: String): Unit = streams.err.println(s) override def debug(s: String): Unit = streams.err.println(s) override def debugEnabled: Boolean = true diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index 67769c34c30..c3597d2ad84 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,6 +44,7 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit + def ticker(key: String, s: String): Unit private[mill] def reportPrefix(s: String): Unit = () private[mill] def promptLine(key: String, identSuffix: String, message: String): Unit = ticker(s"$key $message") diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index d8e5060c177..e83e40f9501 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -7,7 +7,6 @@ import mill.define._ import mill.eval.Evaluator.TaskResult import mill.util._ -import java.io.PrintStream import java.lang.reflect.Method import scala.collection.mutable import scala.reflect.NameTransformer.encode diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index 082101ca5f2..3fd40d16e7c 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -17,6 +17,7 @@ object DummyLogger extends Logger { def info(s: String) = () def error(s: String) = () def ticker(s: String) = () + def ticker(key: String, s: String) = () def debug(s: String) = () override val debugEnabled: Boolean = false diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index e23d4ea03f2..8e0f8f5127f 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -48,6 +48,7 @@ class FileLogger( def info(s: String): Unit = outputStream.println(s) def error(s: String): Unit = outputStream.println(s) def ticker(s: String): Unit = outputStream.println(s) + def ticker(key: String, s: String): Unit = outputStream.println(s) def debug(s: String): Unit = if (debugEnabled) outputStream.println(s) override def close(): Unit = { if (outputStreamUsed) diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index a24e22b829c..04c1b42a4a7 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -31,6 +31,11 @@ class MultiLogger( logger2.ticker(s) } + def ticker(key: String, s: String): Unit = { + logger1.ticker(key, s) + logger2.ticker(key, s) + } + private[mill] override def promptLine( identifier: String, identSuffix: String, diff --git a/main/util/src/mill/util/PrefixLogger.scala b/main/util/src/mill/util/PrefixLogger.scala index 6de1c396494..e5cf813075b 100644 --- a/main/util/src/mill/util/PrefixLogger.scala +++ b/main/util/src/mill/util/PrefixLogger.scala @@ -51,7 +51,8 @@ class PrefixLogger( reportPrefix(context0) logger0.error(infoColor(context) + s) } - override def ticker(s: String): Unit = logger0.ticker(context + tickerContext + s) + override def ticker(s: String): Unit = ticker(context0, s) + override def ticker(key: String, s: String): Unit = logger0.ticker(key, s) private[mill] override def promptLine(key: String, identSuffix: String, message: String): Unit = logger0.promptLine(key, identSuffix, message) diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index de06c9c4ac2..6e511861af3 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -24,6 +24,7 @@ class PrintLogger( systemStreams.err.println((infoColor(context) ++ errorColor(s)).render) } + def ticker(key: String, s: String): Unit = synchronized { ticker(s) } def ticker(s: String): Unit = synchronized { if (enableTicker) { printLoggerState.value match { diff --git a/main/util/src/mill/util/PromptLogger.scala b/main/util/src/mill/util/PromptLogger.scala index 1b554ed14dd..c6b9c9cea68 100644 --- a/main/util/src/mill/util/PromptLogger.scala +++ b/main/util/src/mill/util/PromptLogger.scala @@ -2,7 +2,7 @@ package mill.util import mill.api.SystemStreams import mill.main.client.ProxyStream -import mill.util.MultilinePromptLoggerUtil.{ +import mill.util.PromptLoggerUtil.{ Status, clearScreenToEndBytes, defaultTermHeight, @@ -12,9 +12,9 @@ import mill.util.MultilinePromptLoggerUtil.{ import pprint.Util.literalize import java.io._ -import MultilinePromptLoggerUtil._ +import PromptLoggerUtil._ -private[mill] class MultiLinePromptLogger( +private[mill] class PromptLogger( override val colored: Boolean, override val enableTicker: Boolean, override val infoColor: fansi.Attrs, @@ -27,7 +27,7 @@ private[mill] class MultiLinePromptLogger( autoUpdate: Boolean = true ) extends ColorLogger with AutoCloseable { override def toString: String = s"MultilinePromptLogger(${literalize(titleText)}" - import MultiLinePromptLogger._ + import PromptLogger._ private var termDimensions: (Option[Int], Option[Int]) = (None, None) @@ -94,6 +94,9 @@ private[mill] class MultiLinePromptLogger( override def endTicker(key: String): Unit = synchronized { state.updateCurrent(key, None) } def ticker(s: String): Unit = () + def ticker(key: String, s: String): Unit = { + state.updateDetail(key, s) + } override def reportPrefix(s: String): Unit = synchronized { if (!reportedIdentifiers(s)) { @@ -127,7 +130,7 @@ private[mill] class MultiLinePromptLogger( def systemStreams = streams.systemStreams } -private[mill] object MultiLinePromptLogger { +private[mill] object PromptLogger { private class Streams( enableTicker: Boolean, @@ -256,13 +259,18 @@ private[mill] object MultiLinePromptLogger { def clearStatuses(): Unit = synchronized { statuses.clear() } def updateGlobal(s: String): Unit = synchronized { headerPrefix = s } + + def updateDetail(key: String, detail: String): Unit = synchronized { + statuses.updateWith(key)(_.map(se => se.copy(next = se.next.map(_.copy(detail = detail))))) + } + def updateCurrent(key: String, sOpt: Option[String]): Unit = synchronized { val now = currentTimeMillis() def stillTransitioning(status: Status) = { status.beginTransitionTime + statusRemovalHideDelayMillis > now } - val sOptEntry = sOpt.map(StatusEntry(_, now)) + val sOptEntry = sOpt.map(StatusEntry(_, now, "")) statuses.updateWith(key) { case None => statuses.find { case (k, v) => v.next.isEmpty && stillTransitioning(v) } match { diff --git a/main/util/src/mill/util/PromptLoggerUtil.scala b/main/util/src/mill/util/PromptLoggerUtil.scala index 1ad942c416a..af648706826 100644 --- a/main/util/src/mill/util/PromptLoggerUtil.scala +++ b/main/util/src/mill/util/PromptLoggerUtil.scala @@ -1,6 +1,6 @@ package mill.util -private object MultilinePromptLoggerUtil { +private object PromptLoggerUtil { private[mill] val defaultTermWidth = 119 private[mill] val defaultTermHeight = 50 @@ -38,7 +38,7 @@ private object MultilinePromptLoggerUtil { */ val statusRemovalRemoveDelayMillis = 2000 - private[mill] case class StatusEntry(text: String, startTimeMillis: Long) + private[mill] case class StatusEntry(text: String, startTimeMillis: Long, detail: String = "") /** * Represents a line in the prompt. Stores up to two separate [[StatusEntry]]s, because @@ -53,9 +53,9 @@ private object MultilinePromptLoggerUtil { private[mill] val clearScreenToEndBytes: Array[Byte] = AnsiNav.clearScreen(0).getBytes - private def renderSeconds(millis: Long) = (millis / 1000).toInt match { + private def renderSecondsSuffix(millis: Long) = (millis / 1000).toInt match { case 0 => "" - case n => s"${n}s" + case n => s" ${n}s" } def readTerminalDims(terminfoPath: os.Path): Option[(Option[Int], Option[Int])] = { @@ -91,7 +91,7 @@ private object MultilinePromptLoggerUtil { val maxWidth = consoleWidth - 1 // -1 to account for header val maxHeight = math.max(1, consoleHeight / 3 - 1) - val headerSuffix = renderSeconds(now - startTimeMillis) + val headerSuffix = renderSecondsSuffix(now - startTimeMillis) val header = renderHeader(headerPrefix, titleText, headerSuffix, maxWidth, ending, interactive) @@ -112,9 +112,16 @@ private object MultilinePromptLoggerUtil { val textOpt = if (status.beginTransitionTime + statusRemovalHideDelayMillis < now) status.next else status.prev - textOpt.map(t => - splitShorten(t.text + " " + renderSeconds(now - t.startTimeMillis), maxWidth) - ) + textOpt.map { t => + val seconds = renderSecondsSuffix(now - t.startTimeMillis) + val mainText = splitShorten(t.text + seconds, maxWidth) + + val detail = + if (t.detail == "") "" + else splitShorten(" " + t.detail, maxWidth - mainText.length) + + mainText + detail + } } } // For non-interactive jobs, we do not need to preserve the height of the prompt @@ -150,7 +157,7 @@ private object MultilinePromptLoggerUtil { interactive: Boolean = true ): String = { val headerPrefixStr = if (!interactive || ending) s"$headerPrefix0 " else s" $headerPrefix0 " - val headerSuffixStr = s" $headerSuffix0" + val headerSuffixStr = headerSuffix0 val titleText = s" $titleText0 " // -12 just to ensure we always have some ==== divider on each side of the title val maxTitleLength = @@ -177,8 +184,10 @@ private object MultilinePromptLoggerUtil { if (s.length <= maxLength) s else { val ellipses = "..." - val halfWidth = (maxLength - ellipses.length) / 2 - s.take(halfWidth) + ellipses + s.takeRight(halfWidth) + val nonEllipsesLength = maxLength - ellipses.length + val halfWidth = nonEllipsesLength / 2 + val halfWidth2 = nonEllipsesLength - halfWidth + s.take(halfWidth2) + ellipses.take(maxLength) + s.takeRight(halfWidth) } } diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 64b6a2af9d9..438b754ba40 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -17,6 +17,7 @@ class ProxyLogger(logger: Logger) extends Logger { def info(s: String): Unit = logger.info(s) def error(s: String): Unit = logger.error(s) def ticker(s: String): Unit = logger.ticker(s) + def ticker(key: String, s: String): Unit = logger.ticker(key, s) private[mill] override def promptLine(key: String, identSuffix: String, message: String): Unit = logger.promptLine(key, identSuffix, message) def debug(s: String): Unit = logger.debug(s) diff --git a/main/util/test/src/mill/util/PromptLoggerTests.scala b/main/util/test/src/mill/util/PromptLoggerTests.scala index 312c0ac0844..03485c1f76a 100644 --- a/main/util/test/src/mill/util/PromptLoggerTests.scala +++ b/main/util/test/src/mill/util/PromptLoggerTests.scala @@ -11,7 +11,7 @@ object PromptLoggerTests extends TestSuite { val baos = new ByteArrayOutputStream() val baosOut = new PrintStream(new ProxyStream.Output(baos, ProxyStream.OUT)) val baosErr = new PrintStream(new ProxyStream.Output(baos, ProxyStream.ERR)) - val promptLogger = new MultiLinePromptLogger( + val promptLogger = new PromptLogger( colored = false, enableTicker = true, infoColor = fansi.Attrs.Empty, @@ -28,7 +28,7 @@ object PromptLoggerTests extends TestSuite { } def check( - promptLogger: MultiLinePromptLogger, + promptLogger: PromptLogger, baos: ByteArrayOutputStream, width: Int = 80 )(expected: String*) = { @@ -104,7 +104,7 @@ object PromptLoggerTests extends TestSuite { promptLogger.globalTicker("123/456") promptLogger.refreshPrompt() check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " + " 123/456 ============================ TITLE ==================================" ) promptLogger.promptLine("[1]", "[1/456]", "my-task") @@ -274,7 +274,7 @@ object PromptLoggerTests extends TestSuite { promptLogger.globalTicker("123/456") promptLogger.refreshPrompt() check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " + " 123/456 ============================ TITLE ==================================" ) promptLogger.promptLine("[1]", "[1/456]", "my-task") @@ -282,7 +282,7 @@ object PromptLoggerTests extends TestSuite { promptLogger.refreshPrompt() check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " + " 123/456 ============================ TITLE ==================================" ) promptLogger.endTicker("[1]") @@ -297,7 +297,7 @@ object PromptLoggerTests extends TestSuite { promptLogger.refreshPrompt() check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= " + " 123/456 ============================ TITLE ==================================" ) val newTaskThread2 = new Thread(() => { @@ -308,8 +308,42 @@ object PromptLoggerTests extends TestSuite { newTaskThread2.join() promptLogger.refreshPrompt() check(promptLogger, baos)( - " 123/456 ============================ TITLE ================================= ", - "[2] my-task-new " + " 123/456 ============================ TITLE ==================================", + "[2] my-task-new" + ) + } + } + test("detail") { + if (!Util.windowsPlatform) { + // Make sure that when we have multiple sequential tasks being run on different threads, + // we still end up showing some kind of task in progress in the ticker, even though the + // tasks on each thread are short-lived enough they would not normally get shown if run + // alone. + @volatile var now = 0L + val (baos, promptLogger, prefixLogger) = setup(() => now, os.temp("80 40")) + + promptLogger.globalTicker("123/456") + promptLogger.refreshPrompt() + + promptLogger.promptLine("[1]", "[1/456]", "my-task") + prefixLogger.ticker("detail") + now += 1000 + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE =============================== 1s", + "[1] my-task 1s detail" + ) + prefixLogger.ticker("detail-too-long-gets-truncated-abcdefghijklmnopqrstuvwxyz1234567890") + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE =============================== 1s", + "[1] my-task 1s detail-too-long-gets-truncated...fghijklmnopqrstuvwxyz1234567890" + ) + promptLogger.endTicker("[1]") + now += 10000 + promptLogger.refreshPrompt() + check(promptLogger, baos)( + " 123/456 ============================ TITLE ============================== 11s" ) } } diff --git a/main/util/test/src/mill/util/PromptLoggerUtilTests.scala b/main/util/test/src/mill/util/PromptLoggerUtilTests.scala index 62bf2244059..3a527ad4f19 100644 --- a/main/util/test/src/mill/util/PromptLoggerUtilTests.scala +++ b/main/util/test/src/mill/util/PromptLoggerUtilTests.scala @@ -3,10 +3,25 @@ package mill.util import utest._ import scala.collection.immutable.SortedMap -import MultilinePromptLoggerUtil._ +import PromptLoggerUtil._ object PromptLoggerUtilTests extends TestSuite { val tests = Tests { + test("splitShorten") { + splitShorten("hello world", 12) ==> "hello world" + splitShorten("hello world", 11) ==> "hello world" + splitShorten("hello world", 10) ==> "hell...rld" + splitShorten("hello world", 9) ==> "hel...rld" + splitShorten("hello world", 8) ==> "hel...ld" + splitShorten("hello world", 7) ==> "he...ld" + splitShorten("hello world", 6) ==> "he...d" + splitShorten("hello world", 5) ==> "h...d" + splitShorten("hello world", 4) ==> "h..." + splitShorten("hello world", 3) ==> "..." + splitShorten("hello world", 2) ==> ".." + splitShorten("hello world", 1) ==> "." + splitShorten("hello world", 0) ==> "" + } test("lastIndexOfNewline") { // Fuzz test to make sure our custom fast `lastIndexOfNewline` logic behaves // the same as a slower generic implementation using `.slice.lastIndexOf` @@ -69,7 +84,7 @@ object PromptLoggerUtilTests extends TestSuite { test("simple") - check( "PREFIX", "TITLE", - "SUFFIX", + " SUFFIX", 60, expected = " PREFIX ==================== TITLE ================= SUFFIX" ) @@ -77,7 +92,7 @@ object PromptLoggerUtilTests extends TestSuite { test("short") - check( "PREFIX", "TITLE", - "SUFFIX", + " SUFFIX", 40, expected = " PREFIX ========== TITLE ======= SUFFIX" ) @@ -85,30 +100,29 @@ object PromptLoggerUtilTests extends TestSuite { test("shorter") - check( "PREFIX", "TITLE", - "SUFFIX", + " SUFFIX", 25, - expected = " PREFIX ==...==== SUFFIX" + expected = " PREFIX ========= SUFFIX" ) test("truncateTitle") - check( "PREFIX", "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "SUFFIX", + " SUFFIX", 60, - expected = " PREFIX ====== TITLE_ABCDEF...OPQRSTUVWXYZ ========= SUFFIX" + expected = " PREFIX ====== TITLE_ABCDEFG...OPQRSTUVWXYZ ======== SUFFIX" ) test("asymmetricTruncateTitle") - check( "PREFIX_LONG", "TITLE_ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "SUFFIX", + " SUFFIX", 60, - expected = " PREFIX_LONG = TITLE_A...TUVWXYZ =================== SUFFIX" + expected = " PREFIX_LONG = TITLE_AB...TUVWXYZ ================== SUFFIX" ) } test("renderPrompt") { - import MultiLinePromptLogger._ val now = System.currentTimeMillis() def renderPromptTest( interactive: Boolean, @@ -121,7 +135,7 @@ object PromptLoggerUtilTests extends TestSuite { startTimeMillis = now - 1337000, headerPrefix = "123/456", titleText = titleText, - statuses = SortedMap(statuses.map{case (k, v) => (k.toString, v)}: _*), + statuses = SortedMap(statuses.map { case (k, v) => (k.toString, v) }: _*), interactive = interactive ) @@ -230,6 +244,38 @@ object PromptLoggerUtilTests extends TestSuite { ) assert(rendered == expected) } + test("detail") { + val rendered = renderPromptTest(interactive = true)( + 0 -> Status(Some(StatusEntry("1 hello", now - 1000, "")), 0, None), + 1 -> Status(Some(StatusEntry("2 world", now - 2000, "HELLO")), 0, None), + 2 -> Status( + Some(StatusEntry( + "3 truncated-detail", + now - 3000, + "HELLO WORLD abcdefghijklmnopqrstuvwxyz1234567890" + )), + 0, + None + ), + 3 -> Status( + Some(StatusEntry( + "4 long-status-eliminated-detail-abcdefghijklmnopqrstuvwxyz1234567890", + now - 4000, + "HELLO" + )), + 0, + None + ) + ) + val expected = List( + " 123/456 =============== __.compile ================ 1337s", + "1 hello 1s", // no detail + "2 world 2s HELLO", // simple detail + "3 truncated-detail 3s HELLO WORLD abcde...tuvwxyz1234567890", // truncated detail + "4 long-status-eliminated-det...lmnopqrstuvwxyz1234567890 4s" + ) + assert(rendered == expected) + } test("removalDelay") { val rendered = renderPromptTest( diff --git a/runner/src/mill/runner/MillMain.scala b/runner/src/mill/runner/MillMain.scala index 739e5218d07..e9b2097ec85 100644 --- a/runner/src/mill/runner/MillMain.scala +++ b/runner/src/mill/runner/MillMain.scala @@ -9,7 +9,7 @@ import mill.api.{MillException, SystemStreams, WorkspaceRoot, internal} import mill.bsp.{BspContext, BspServerResult} import mill.main.BuildInfo import mill.main.client.ServerFiles -import mill.util.{MultiLinePromptLogger, PrintLogger} +import mill.util.{PromptLogger, PrintLogger} import java.lang.reflect.InvocationTargetException import scala.util.control.NonFatal @@ -331,7 +331,7 @@ object MillMain { printLoggerState ) } else { - new MultiLinePromptLogger( + new PromptLogger( colored = colored, enableTicker = enableTicker.getOrElse(true), infoColor = colors.info, From b62f9e7b07b003450a64c7b94526ef13d47f5642 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 10:53:08 +0800 Subject: [PATCH 112/116] color prompt details --- main/util/src/mill/util/PromptLogger.scala | 7 +++++-- main/util/src/mill/util/PromptLoggerUtil.scala | 3 ++- main/util/test/src/mill/util/PromptLoggerUtilTests.scala | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/main/util/src/mill/util/PromptLogger.scala b/main/util/src/mill/util/PromptLogger.scala index c6b9c9cea68..8a1b51aa253 100644 --- a/main/util/src/mill/util/PromptLogger.scala +++ b/main/util/src/mill/util/PromptLogger.scala @@ -38,7 +38,8 @@ private[mill] class PromptLogger( systemStreams0, currentTimeMillis(), () => termDimensions, - currentTimeMillis + currentTimeMillis, + infoColor ) private val streams = new Streams( @@ -200,7 +201,8 @@ private[mill] object PromptLogger { systemStreams0: SystemStreams, startTimeMillis: Long, consoleDims: () => (Option[Int], Option[Int]), - currentTimeMillis: () => Long + currentTimeMillis: () => Long, + infoColor: fansi.Attrs ) { private var lastRenderedPromptHash = 0 private val statuses = collection.mutable.SortedMap.empty[String, Status] @@ -235,6 +237,7 @@ private[mill] object PromptLogger { titleText, statuses, interactive = consoleDims()._1.nonEmpty, + infoColor = infoColor, ending = ending ) diff --git a/main/util/src/mill/util/PromptLoggerUtil.scala b/main/util/src/mill/util/PromptLoggerUtil.scala index af648706826..319932a4086 100644 --- a/main/util/src/mill/util/PromptLoggerUtil.scala +++ b/main/util/src/mill/util/PromptLoggerUtil.scala @@ -85,6 +85,7 @@ private object PromptLoggerUtil { titleText: String, statuses: collection.SortedMap[String, Status], interactive: Boolean, + infoColor: fansi.Attrs, ending: Boolean = false ): List[String] = { // -1 to leave a bit of buffer @@ -120,7 +121,7 @@ private object PromptLoggerUtil { if (t.detail == "") "" else splitShorten(" " + t.detail, maxWidth - mainText.length) - mainText + detail + mainText + infoColor(detail) } } } diff --git a/main/util/test/src/mill/util/PromptLoggerUtilTests.scala b/main/util/test/src/mill/util/PromptLoggerUtilTests.scala index 3a527ad4f19..35c88e3b758 100644 --- a/main/util/test/src/mill/util/PromptLoggerUtilTests.scala +++ b/main/util/test/src/mill/util/PromptLoggerUtilTests.scala @@ -136,7 +136,8 @@ object PromptLoggerUtilTests extends TestSuite { headerPrefix = "123/456", titleText = titleText, statuses = SortedMap(statuses.map { case (k, v) => (k.toString, v) }: _*), - interactive = interactive + interactive = interactive, + infoColor = fansi.Attrs.Empty ) } From f95f5b6836a3ba1ca8c9f1e6550d8e8e6a4cc975 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 11:43:09 +0800 Subject: [PATCH 113/116] mima --- main/api/src/mill/api/Logger.scala | 2 +- main/util/src/mill/util/DummyLogger.scala | 1 - main/util/src/mill/util/FileLogger.scala | 1 - main/util/src/mill/util/MultiLogger.scala | 2 +- main/util/src/mill/util/PrintLogger.scala | 2 +- main/util/src/mill/util/PromptLogger.scala | 2 +- main/util/src/mill/util/ProxyLogger.scala | 2 +- 7 files changed, 5 insertions(+), 7 deletions(-) diff --git a/main/api/src/mill/api/Logger.scala b/main/api/src/mill/api/Logger.scala index c3597d2ad84..c0c11039244 100644 --- a/main/api/src/mill/api/Logger.scala +++ b/main/api/src/mill/api/Logger.scala @@ -44,7 +44,7 @@ trait Logger { def info(s: String): Unit def error(s: String): Unit def ticker(s: String): Unit - def ticker(key: String, s: String): Unit + def ticker(key: String, s: String): Unit = ticker(s) private[mill] def reportPrefix(s: String): Unit = () private[mill] def promptLine(key: String, identSuffix: String, message: String): Unit = ticker(s"$key $message") diff --git a/main/util/src/mill/util/DummyLogger.scala b/main/util/src/mill/util/DummyLogger.scala index 3fd40d16e7c..082101ca5f2 100644 --- a/main/util/src/mill/util/DummyLogger.scala +++ b/main/util/src/mill/util/DummyLogger.scala @@ -17,7 +17,6 @@ object DummyLogger extends Logger { def info(s: String) = () def error(s: String) = () def ticker(s: String) = () - def ticker(key: String, s: String) = () def debug(s: String) = () override val debugEnabled: Boolean = false diff --git a/main/util/src/mill/util/FileLogger.scala b/main/util/src/mill/util/FileLogger.scala index 8e0f8f5127f..e23d4ea03f2 100644 --- a/main/util/src/mill/util/FileLogger.scala +++ b/main/util/src/mill/util/FileLogger.scala @@ -48,7 +48,6 @@ class FileLogger( def info(s: String): Unit = outputStream.println(s) def error(s: String): Unit = outputStream.println(s) def ticker(s: String): Unit = outputStream.println(s) - def ticker(key: String, s: String): Unit = outputStream.println(s) def debug(s: String): Unit = if (debugEnabled) outputStream.println(s) override def close(): Unit = { if (outputStreamUsed) diff --git a/main/util/src/mill/util/MultiLogger.scala b/main/util/src/mill/util/MultiLogger.scala index 04c1b42a4a7..4295cdf7b11 100644 --- a/main/util/src/mill/util/MultiLogger.scala +++ b/main/util/src/mill/util/MultiLogger.scala @@ -31,7 +31,7 @@ class MultiLogger( logger2.ticker(s) } - def ticker(key: String, s: String): Unit = { + override def ticker(key: String, s: String): Unit = { logger1.ticker(key, s) logger2.ticker(key, s) } diff --git a/main/util/src/mill/util/PrintLogger.scala b/main/util/src/mill/util/PrintLogger.scala index 6e511861af3..510e605e9f9 100644 --- a/main/util/src/mill/util/PrintLogger.scala +++ b/main/util/src/mill/util/PrintLogger.scala @@ -24,7 +24,7 @@ class PrintLogger( systemStreams.err.println((infoColor(context) ++ errorColor(s)).render) } - def ticker(key: String, s: String): Unit = synchronized { ticker(s) } + override def ticker(key: String, s: String): Unit = synchronized { ticker(s) } def ticker(s: String): Unit = synchronized { if (enableTicker) { printLoggerState.value match { diff --git a/main/util/src/mill/util/PromptLogger.scala b/main/util/src/mill/util/PromptLogger.scala index 8a1b51aa253..39fa747bff4 100644 --- a/main/util/src/mill/util/PromptLogger.scala +++ b/main/util/src/mill/util/PromptLogger.scala @@ -95,7 +95,7 @@ private[mill] class PromptLogger( override def endTicker(key: String): Unit = synchronized { state.updateCurrent(key, None) } def ticker(s: String): Unit = () - def ticker(key: String, s: String): Unit = { + override def ticker(key: String, s: String): Unit = { state.updateDetail(key, s) } diff --git a/main/util/src/mill/util/ProxyLogger.scala b/main/util/src/mill/util/ProxyLogger.scala index 438b754ba40..e76b437e19a 100644 --- a/main/util/src/mill/util/ProxyLogger.scala +++ b/main/util/src/mill/util/ProxyLogger.scala @@ -17,7 +17,7 @@ class ProxyLogger(logger: Logger) extends Logger { def info(s: String): Unit = logger.info(s) def error(s: String): Unit = logger.error(s) def ticker(s: String): Unit = logger.ticker(s) - def ticker(key: String, s: String): Unit = logger.ticker(key, s) + override def ticker(key: String, s: String): Unit = logger.ticker(key, s) private[mill] override def promptLine(key: String, identSuffix: String, message: String): Unit = logger.promptLine(key, identSuffix, message) def debug(s: String): Unit = logger.debug(s) From 3a8a4e74e7fa83b4230d2d8435930358381fc7d3 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 12:02:02 +0800 Subject: [PATCH 114/116] merge --- main/eval/src/mill/eval/GroupEvaluator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 35fea4ce99d..156ab79f191 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -180,7 +180,7 @@ private[mill] trait GroupEvaluator { inputsHash, labelled, forceDiscard = - // worker metadata file removed by user, let's recompute the worker + // worker metadata file removed by user, let's recompute the worker cached.isEmpty ) From 7a029dad7ef2bd7cdfc7e0fc626f294c55a89798 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 12:37:18 +0800 Subject: [PATCH 115/116] . --- main/util/test/src/mill/util/PromptLoggerTests.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main/util/test/src/mill/util/PromptLoggerTests.scala b/main/util/test/src/mill/util/PromptLoggerTests.scala index 03485c1f76a..03f370d3faf 100644 --- a/main/util/test/src/mill/util/PromptLoggerTests.scala +++ b/main/util/test/src/mill/util/PromptLoggerTests.scala @@ -22,7 +22,14 @@ object PromptLoggerTests extends TestSuite { terminfoPath = terminfoPath, currentTimeMillis = now, autoUpdate = false - ) + ){ + // For testing purposes, wait till the system is quiescent before re-printing + // the prompt, to try and keep the test executions deterministics + override def refreshPrompt(): Unit = { + streamsAwaitPumperEmpty() + super.refreshPrompt() + } + } val prefixLogger = new PrefixLogger(promptLogger, "[1]") (baos, promptLogger, prefixLogger) } From bcf10563dca9c3dfd33dffc10d86dade1299b757 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 26 Sep 2024 12:44:36 +0800 Subject: [PATCH 116/116] . --- main/util/test/src/mill/util/PromptLoggerTests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/util/test/src/mill/util/PromptLoggerTests.scala b/main/util/test/src/mill/util/PromptLoggerTests.scala index 03f370d3faf..a1d9600ac09 100644 --- a/main/util/test/src/mill/util/PromptLoggerTests.scala +++ b/main/util/test/src/mill/util/PromptLoggerTests.scala @@ -22,7 +22,7 @@ object PromptLoggerTests extends TestSuite { terminfoPath = terminfoPath, currentTimeMillis = now, autoUpdate = false - ){ + ) { // For testing purposes, wait till the system is quiescent before re-printing // the prompt, to try and keep the test executions deterministics override def refreshPrompt(): Unit = {