Merge pull request #5319 from eatkins/terminal

Overhaul terminal io
This commit is contained in:
eugene yokota 2020-05-01 22:36:47 -04:00 committed by GitHub
commit 5a529bf10c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 794 additions and 392 deletions

View File

@ -7,36 +7,67 @@
package sbt.internal.util package sbt.internal.util
import java.io._
import jline.console.ConsoleReader import jline.console.ConsoleReader
import jline.console.history.{ FileHistory, MemoryHistory } import jline.console.history.{ FileHistory, MemoryHistory }
import java.io.{ File, FileDescriptor, FileInputStream, FilterInputStream, InputStream } import sbt.internal.util.complete.Parser
import complete.Parser
import jline.Terminal
import scala.concurrent.duration._
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent.duration._
trait LineReader {
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
def redraw(): Unit = ()
}
object LineReader {
val HandleCONT =
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
val MaxHistorySize = 500
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = {
val cr = Terminal.createReader(in)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
case None => new MemoryHistory
case Some(file) => new FileHistory(file): MemoryHistory
}
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr
}
def simple(
historyPath: Option[File],
handleCONT: Boolean = HandleCONT,
injectThreadSleep: Boolean = false
): LineReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep)
}
abstract class JLine extends LineReader { abstract class JLine extends LineReader {
protected[this] def handleCONT: Boolean protected[this] def handleCONT: Boolean
protected[this] def reader: ConsoleReader protected[this] def reader: ConsoleReader
protected[this] def injectThreadSleep: Boolean @deprecated("For binary compatibility only", "1.4.0")
protected[this] lazy val in: InputStream = { protected[this] def injectThreadSleep: Boolean = false
// On Windows InputStream#available doesn't seem to return positive number. @deprecated("For binary compatibility only", "1.4.0")
JLine.makeInputStream(injectThreadSleep && !Util.isNonCygwinWindows) protected[this] lazy val in: InputStream = Terminal.wrappedSystemIn
}
def readLine(prompt: String, mask: Option[Char] = None) = override def readLine(prompt: String, mask: Option[Char] = None): Option[String] =
try { try {
JLine.withJLine { Terminal.withRawSystemIn(unsynchronizedReadLine(prompt, mask))
unsynchronizedReadLine(prompt, mask)
}
} catch { } catch {
case _: InterruptedException => case _: InterruptedException =>
// println("readLine: InterruptedException") // println("readLine: InterruptedException")
Option("") Option("")
} }
override def redraw(): Unit = {
reader.drawLine()
reader.flush()
}
private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] = private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] =
readLineWithHistory(prompt, mask) map { x => readLineWithHistory(prompt, mask) map { x =>
x.trim x.trim
@ -85,101 +116,73 @@ abstract class JLine extends LineReader {
} }
private[this] def resume(): Unit = { private[this] def resume(): Unit = {
jline.TerminalFactory.reset Terminal.reset()
JLine.terminal.init
reader.drawLine() reader.drawLine()
reader.flush() reader.flush()
} }
} }
@deprecated("Use LineReader apis", "1.4.0")
private[sbt] object JLine { private[sbt] object JLine {
private[this] val TerminalProperty = "jline.terminal" @deprecated("For binary compatibility only", "1.4.0")
fixTerminalProperty()
// translate explicit class names to type in order to support
// older Scala, since it shaded classes but not the system property
private[sbt] def fixTerminalProperty(): Unit = {
val newValue = System.getProperty(TerminalProperty) match {
case "jline.UnixTerminal" => "unix"
case null if System.getProperty("sbt.cygwin") != null => "unix"
case "jline.WindowsTerminal" => "windows"
case "jline.AnsiWindowsTerminal" => "windows"
case "jline.UnsupportedTerminal" => "none"
case x => x
}
if (newValue != null) {
System.setProperty(TerminalProperty, newValue)
()
}
}
protected[this] val originalIn = new FileInputStream(FileDescriptor.in) protected[this] val originalIn = new FileInputStream(FileDescriptor.in)
@deprecated("Handled by Terminal.fixTerminalProperty", "1.4.0")
private[sbt] def fixTerminalProperty(): Unit = ()
@deprecated("For binary compatibility only", "1.4.0")
private[sbt] def makeInputStream(injectThreadSleep: Boolean): InputStream = private[sbt] def makeInputStream(injectThreadSleep: Boolean): InputStream =
if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds) if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds)
else originalIn else originalIn
// When calling this, ensure that enableEcho has been or will be called. // When calling this, ensure that enableEcho has been or will be called.
// TerminalFactory.get will initialize the terminal to disable echo. // TerminalFactory.get will initialize the terminal to disable echo.
private[sbt] def terminal: Terminal = jline.TerminalFactory.get @deprecated("Don't use jline.Terminal directly", "1.4.0")
private[sbt] def terminal: jline.Terminal = Terminal.deprecatedTeminal
private def withTerminal[T](f: jline.Terminal => T): T =
synchronized {
val t = terminal
t.synchronized { f(t) }
}
/** /**
* For accessing the JLine Terminal object. * For accessing the JLine Terminal object.
* This ensures synchronized access as well as re-enabling echo after getting the Terminal. * This ensures synchronized access as well as re-enabling echo after getting the Terminal.
*/ */
@deprecated("Don't use jline.Terminal directly. Use Terminal.withCanonicalIn instead.", "1.4.0")
def usingTerminal[T](f: jline.Terminal => T): T = def usingTerminal[T](f: jline.Terminal => T): T =
withTerminal { t => Terminal.withCanonicalIn(f(Terminal.deprecatedTeminal))
t.restore
val result = f(t) @deprecated("unused", "1.4.0")
t.restore def createReader(): ConsoleReader = createReader(None, Terminal.wrappedSystemIn)
result
@deprecated("Use LineReader.createReader", "1.4.0")
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = {
val cr = Terminal.createReader(in)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
case None => new MemoryHistory
case Some(file) => new FileHistory(file): MemoryHistory
} }
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr
}
def createReader(): ConsoleReader = createReader(None, JLine.makeInputStream(true)) @deprecated("Avoid referencing JLine directly. Use Terminal.withRawSystemIn instead.", "1.4.0")
def withJLine[T](action: => T): T = Terminal.withRawSystemIn(action)
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader =
usingTerminal { _ =>
val cr = new ConsoleReader(in, System.out)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
case None => new MemoryHistory
case Some(file) => (new FileHistory(file): MemoryHistory)
}
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr
}
def withJLine[T](action: => T): T =
withTerminal { t =>
t.init
try {
action
} finally {
t.restore
}
}
@deprecated("Use LineReader.simple instead", "1.4.0")
def simple( def simple(
historyPath: Option[File], historyPath: Option[File],
handleCONT: Boolean = HandleCONT, handleCONT: Boolean = LineReader.HandleCONT,
injectThreadSleep: Boolean = false injectThreadSleep: Boolean = false
): SimpleReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep) ): SimpleReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep)
val MaxHistorySize = 500 @deprecated("Use LineReader.MaxHistorySize", "1.4.0")
val MaxHistorySize = LineReader.MaxHistorySize
val HandleCONT = @deprecated("Use LineReader.HandleCONT", "1.4.0")
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT) val HandleCONT = LineReader.HandleCONT
} }
@deprecated("For binary compatibility only", "1.4.0")
private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration) private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration)
extends FilterInputStream(is) { extends FilterInputStream(is) {
@tailrec final override def read(): Int = @tailrec final override def read(): Int =
@ -204,18 +207,21 @@ private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration)
} }
} }
trait LineReader {
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
}
final class FullReader( final class FullReader(
historyPath: Option[File], historyPath: Option[File],
complete: Parser[_], complete: Parser[_],
val handleCONT: Boolean = JLine.HandleCONT, val handleCONT: Boolean,
val injectThreadSleep: Boolean = false inputStream: InputStream,
) extends JLine { ) extends JLine {
protected[this] val reader = { @deprecated("Use the constructor with no injectThreadSleep parameter", "1.4.0")
val cr = JLine.createReader(historyPath, in) def this(
historyPath: Option[File],
complete: Parser[_],
handleCONT: Boolean = LineReader.HandleCONT,
injectThreadSleep: Boolean = false
) = this(historyPath, complete, handleCONT, JLine.makeInputStream(injectThreadSleep))
protected[this] val reader: ConsoleReader = {
val cr = LineReader.createReader(historyPath, inputStream)
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete) sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
cr cr
} }
@ -224,9 +230,12 @@ final class FullReader(
class SimpleReader private[sbt] ( class SimpleReader private[sbt] (
historyPath: Option[File], historyPath: Option[File],
val handleCONT: Boolean, val handleCONT: Boolean,
val injectThreadSleep: Boolean inputStream: InputStream
) extends JLine { ) extends JLine {
protected[this] val reader = JLine.createReader(historyPath, in) def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
this(historyPath, handleCONT, Terminal.wrappedSystemIn)
protected[this] val reader: ConsoleReader =
LineReader.createReader(historyPath, inputStream)
} }
object SimpleReader extends SimpleReader(None, JLine.HandleCONT, false) object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false)

View File

@ -7,17 +7,18 @@
package sbt.internal.util package sbt.internal.util
import sbt.util._
import java.io.{ PrintStream, PrintWriter } import java.io.{ PrintStream, PrintWriter }
import java.lang.StringBuilder import java.lang.StringBuilder
import java.util.Locale
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, AtomicReference } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, AtomicReference }
import org.apache.logging.log4j.{ Level => XLevel }
import org.apache.logging.log4j.message.{ Message, ObjectMessage, ReusableObjectMessage }
import org.apache.logging.log4j.core.{ LogEvent => XLogEvent }
import org.apache.logging.log4j.core.appender.AbstractAppender import org.apache.logging.log4j.core.appender.AbstractAppender
import scala.util.control.NonFatal import org.apache.logging.log4j.core.{ LogEvent => XLogEvent }
import ConsoleAppender._ import org.apache.logging.log4j.message.{ Message, ObjectMessage, ReusableObjectMessage }
import org.apache.logging.log4j.{ Level => XLevel }
import sbt.internal.util.ConsoleAppender._
import sbt.util._
import scala.collection.mutable.ArrayBuffer
object ConsoleLogger { object ConsoleLogger {
// These are provided so other modules do not break immediately. // These are provided so other modules do not break immediately.
@ -104,15 +105,17 @@ class ConsoleLogger private[ConsoleLogger] (
} }
object ConsoleAppender { object ConsoleAppender {
private[sbt] def cursorLeft(n: Int): String = s"\u001B[${n}D"
private[sbt] def cursorRight(n: Int): String = s"\u001B[${n}C"
private[sbt] def cursorUp(n: Int): String = s"\u001B[${n}A" private[sbt] def cursorUp(n: Int): String = s"\u001B[${n}A"
private[sbt] def cursorDown(n: Int): String = s"\u001B[${n}B" private[sbt] def cursorDown(n: Int): String = s"\u001B[${n}B"
private[sbt] def scrollUp(n: Int): String = s"\u001B[${n}S" private[sbt] def scrollUp(n: Int): String = s"\u001B[${n}S"
private[sbt] def clearScreen(n: Int): String = s"\u001B[${n}J"
private[sbt] def clearLine(n: Int): String = s"\u001B[${n}K"
private[sbt] final val DeleteLine = "\u001B[2K" private[sbt] final val DeleteLine = "\u001B[2K"
private[sbt] final val CursorLeft1000 = "\u001B[1000D" private[sbt] final val ClearScreenAfterCursor = clearScreen(0)
private[sbt] final val CursorLeft1000 = cursorLeft(1000)
private[sbt] final val CursorDown1 = cursorDown(1) private[sbt] final val CursorDown1 = cursorDown(1)
private[sbt] lazy val terminalWidth = usingTerminal { t =>
t.getWidth
}
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false) private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b) def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
def showProgress: Boolean = showProgressHolder.get def showProgress: Boolean = showProgressHolder.get
@ -293,31 +296,7 @@ object ConsoleAppender {
private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
private[this] def jline1to2CompatMsg = "Found class jline.Terminal, but interface was expected" private[this] def ansiSupported: Boolean = Terminal.isAnsiSupported
private[this] def ansiSupported =
try {
usingTerminal { t =>
t.isAnsiSupported
}
} catch {
case NonFatal(_) => !isWindows
}
/**
* For accessing the JLine Terminal object.
* This ensures re-enabling echo after getting the Terminal.
*/
private[this] def usingTerminal[T](f: jline.Terminal => T): T = {
val t = jline.TerminalFactory.get
t.restore
val result = f(t)
t.restore
result
}
private[this] def os = System.getProperty("os.name")
private[this] def isWindows = os.toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
} }
// See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2 // See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2
@ -340,77 +319,6 @@ class ConsoleAppender private[ConsoleAppender] (
) extends AbstractAppender(name, null, LogExchange.dummyLayout, true, Array.empty) { ) extends AbstractAppender(name, null, LogExchange.dummyLayout, true, Array.empty) {
import scala.Console.{ BLUE, GREEN, RED, YELLOW } import scala.Console.{ BLUE, GREEN, RED, YELLOW }
private val progressState: AtomicReference[ProgressState] = new AtomicReference(null)
private[sbt] def setProgressState(state: ProgressState) = progressState.set(state)
/**
* Splits a log message into individual lines and interlaces each line with
* the task progress report to reduce the appearance of flickering. It is assumed
* that this method is only called while holding the out.lockObject.
*/
private def supershellInterlaceMsg(msg: String): Unit = {
val state = progressState.get
import state._
val progress = progressLines.get
msg.linesIterator.foreach { l =>
out.println(s"$DeleteLine$l")
if (progress.length > 0) {
val pad = if (padding.get > 0) padding.decrementAndGet() else 0
val width = ConsoleAppender.terminalWidth
val len: Int = progress.foldLeft(progress.length)(_ + terminalLines(width)(_))
deleteConsoleLines(blankZone + pad)
progress.foreach(printProgressLine)
out.print(cursorUp(blankZone + len + padding.get))
}
}
out.flush()
}
private def printProgressLine(line: String): Unit = {
out.print(DeleteLine)
out.println(line)
}
/**
* Receives a new task report and replaces the old one. In the event that the new
* report has fewer lines than the previous report, padding lines are added on top
* so that the console log lines remain contiguous. When a console line is printed
* at the info or greater level, we can decrement the padding because the console
* line will have filled in the blank line.
*/
private def updateProgressState(pe: ProgressEvent): Unit = {
val state = progressState.get
import state._
val sorted = pe.items.sortBy(x => x.elapsedMicros)
val info = sorted map { item =>
val elapsed = item.elapsedMicros / 1000000L
s" | => ${item.name} ${elapsed}s"
}
val width = ConsoleAppender.terminalWidth
val currentLength = info.foldLeft(info.length)(_ + terminalLines(width)(_))
val previousLines = progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(previousLines.length)(_ + terminalLines(width)(_))
val prevPadding = padding.get
val newPadding = math.max(0, prevLength + prevPadding - currentLength)
padding.set(newPadding)
deleteConsoleLines(newPadding)
deleteConsoleLines(blankZone)
info.foreach(printProgressLine)
out.print(cursorUp(blankZone + currentLength + newPadding))
out.flush()
}
private def terminalLines(width: Int): String => Int =
(progressLine: String) => if (width > 0) (progressLine.length - 1) / width else 0
private def deleteConsoleLines(n: Int): Unit = {
(1 to n) foreach { _ =>
out.println(DeleteLine)
}
}
private val reset: String = { private val reset: String = {
if (ansiCodesSupported && useFormat) scala.Console.RESET if (ansiCodesSupported && useFormat) scala.Console.RESET
else "" else ""
@ -541,11 +449,7 @@ class ConsoleAppender private[ConsoleAppender] (
private def write(msg: String): Unit = { private def write(msg: String): Unit = {
val toWrite = val toWrite =
if (!useFormat || !ansiCodesSupported) EscHelpers.removeEscapeSequences(msg) else msg if (!useFormat || !ansiCodesSupported) EscHelpers.removeEscapeSequences(msg) else msg
if (progressState.get != null) { out.println(toWrite)
supershellInterlaceMsg(toWrite)
} else {
out.println(toWrite)
}
} }
private def appendMessage(level: Level.Value, msg: Message): Unit = private def appendMessage(level: Level.Value, msg: Message): Unit =
@ -575,18 +479,16 @@ class ConsoleAppender private[ConsoleAppender] (
} }
} }
private def appendProgressEvent(pe: ProgressEvent): Unit =
if (progressState.get != null) {
out.lockObject.synchronized(updateProgressState(pe))
}
private def appendMessageContent(level: Level.Value, o: AnyRef): Unit = { private def appendMessageContent(level: Level.Value, o: AnyRef): Unit = {
def appendEvent(oe: ObjectEvent[_]): Unit = { def appendEvent(oe: ObjectEvent[_]): Unit = {
val contentType = oe.contentType val contentType = oe.contentType
contentType match { contentType match {
case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent]) case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent])
case "sbt.internal.util.ProgressEvent" => case "sbt.internal.util.ProgressEvent" =>
appendProgressEvent(oe.message.asInstanceOf[ProgressEvent]) oe.message match {
case pe: ProgressEvent => ProgressState.updateProgressState(pe)
case _ =>
}
case _ => case _ =>
LogExchange.stringCodec[AnyRef](contentType) match { LogExchange.stringCodec[AnyRef](contentType) match {
case Some(codec) if contentType == "sbt.internal.util.SuccessEvent" => case Some(codec) if contentType == "sbt.internal.util.SuccessEvent" =>
@ -613,11 +515,106 @@ final class SuppressedTraceContext(val traceLevel: Int, val useFormat: Boolean)
private[sbt] final class ProgressState( private[sbt] final class ProgressState(
val progressLines: AtomicReference[Seq[String]], val progressLines: AtomicReference[Seq[String]],
val padding: AtomicInteger, val padding: AtomicInteger,
val blankZone: Int val blankZone: Int,
val currentLineBytes: AtomicReference[ArrayBuffer[Byte]],
) { ) {
def this(blankZone: Int) = this(new AtomicReference(Nil), new AtomicInteger(0), blankZone) def this(blankZone: Int) =
this(
new AtomicReference(Nil),
new AtomicInteger(0),
blankZone,
new AtomicReference(new ArrayBuffer[Byte])
)
def reset(): Unit = { def reset(): Unit = {
progressLines.set(Nil) progressLines.set(Nil)
padding.set(0) padding.set(0)
} }
} }
private[sbt] object ProgressState {
private val progressState: AtomicReference[ProgressState] = new AtomicReference(null)
private[util] def clearBytes(): Unit = progressState.get match {
case null =>
case state =>
val pad = state.padding.get
if (state.currentLineBytes.get.isEmpty && pad > 0) state.padding.decrementAndGet()
state.currentLineBytes.set(new ArrayBuffer[Byte])
}
private[util] def addBytes(bytes: ArrayBuffer[Byte]): Unit = progressState.get match {
case null =>
case state =>
val previous = state.currentLineBytes.get
val padding = state.padding.get
val prevLineCount = if (padding > 0) Terminal.lineCount(new String(previous.toArray)) else 0
previous ++= bytes
if (padding > 0) {
val newLineCount = Terminal.lineCount(new String(previous.toArray))
val diff = newLineCount - prevLineCount
state.padding.set(math.max(padding - diff, 0))
}
}
private[util] def reprint(printStream: PrintStream): Unit = progressState.get match {
case null => printStream.write('\n')
case state =>
if (state.progressLines.get.nonEmpty) {
val lines = printProgress(0, 0)
printStream.print(ClearScreenAfterCursor + "\n" + lines)
} else printStream.write('\n')
}
/**
* Receives a new task report and replaces the old one. In the event that the new
* report has fewer lines than the previous report, padding lines are added on top
* so that the console log lines remain contiguous. When a console line is printed
* at the info or greater level, we can decrement the padding because the console
* line will have filled in the blank line.
*/
private[util] def updateProgressState(pe: ProgressEvent): Unit = Terminal.withPrintStream { ps =>
progressState.get match {
case null =>
case state =>
val info = pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L
s" | => ${item.name} ${elapsed}s"
}
val currentLength = info.foldLeft(0)(_ + Terminal.lineCount(_))
val previousLines = state.progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(0)(_ + Terminal.lineCount(_))
val (height, width) = Terminal.getLineHeightAndWidth
val prevSize = prevLength + state.padding.get
val newPadding = math.max(0, prevSize - currentLength)
state.padding.set(newPadding)
ps.print(printProgress(height, width))
ps.flush()
}
}
private[sbt] def set(state: ProgressState): Unit = progressState.set(state)
private[util] def printProgress(height: Int, width: Int): String = progressState.get match {
case null => ""
case state =>
val previousLines = state.progressLines.get
if (previousLines.nonEmpty) {
val currentLength = previousLines.foldLeft(0)(_ + Terminal.lineCount(_))
val left = cursorLeft(1000) // resets the position to the left
val offset = width > 0
val pad = math.max(state.padding.get - height, 0)
val start = ClearScreenAfterCursor + (if (offset) "\n" else "")
val totalSize = currentLength + state.blankZone + pad
val blank = left + s"\n$DeleteLine" * (totalSize - currentLength)
val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine")
val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0))
val resetCursorRight = left + (if (offset) cursorRight(width) else "")
val resetCursor = resetCursorUp + resetCursorRight
start + blank + lines + resetCursor
} else {
ClearScreenAfterCursor
}
}
}

View File

@ -0,0 +1,342 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import java.io.{ InputStream, OutputStream, PrintStream }
import java.nio.channels.ClosedChannelException
import java.util.Locale
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.locks.ReentrantLock
import jline.console.ConsoleReader
import scala.annotation.tailrec
import scala.collection.mutable.ArrayBuffer
import scala.util.control.NonFatal
object Terminal {
/**
* Gets the current width of the terminal. The implementation reads a property from the jline
* config which is updated if it has been more than a second since the last update. It is thus
* possible for this value to be stale.
*
* @return the terminal width.
*/
def getWidth: Int = terminal.getWidth
/**
* Gets the current height of the terminal. The implementation reads a property from the jline
* config which is updated if it has been more than a second since the last update. It is thus
* possible for this value to be stale.
*
* @return the terminal height.
*/
def getHeight: Int = terminal.getHeight
/**
* Returns the height and width of the current line that is displayed on the terminal. If the
* most recently flushed byte is a newline, this will be `(0, 0)`.
*
* @return the (height, width) pair
*/
def getLineHeightAndWidth: (Int, Int) = currentLine.get.toArray match {
case bytes if bytes.isEmpty => (0, 0)
case bytes =>
val width = getWidth
val line = EscHelpers.removeEscapeSequences(new String(bytes))
val count = lineCount(line)
(count, line.length - ((count - 1) * width))
}
/**
* Returns the number of lines that the input string will cover given the current width of the
* terminal.
*
* @param line the input line
* @return the number of lines that the line will cover on the terminal
*/
def lineCount(line: String): Int = {
val width = getWidth
val lines = EscHelpers.removeEscapeSequences(line).split('\n')
def count(l: String): Int = {
val len = l.length
if (width > 0 && len > 0) (len - 1 + width) / width else 0
}
lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_))
}
/**
* Returns true if the current terminal supports ansi characters.
*
* @return true if the current terminal supports ansi escape codes.
*/
def isAnsiSupported: Boolean =
try terminal.isAnsiSupported
catch { case NonFatal(_) => !isWindows }
/**
* Returns true if System.in is attached. When sbt is run as a subprocess, like in scripted or
* as a server, System.in will not be attached and this method will return false. Otherwise
* it will return true.
*
* @return true if System.in is attached.
*/
def systemInIsAttached: Boolean = attached.get
/**
* Returns an InputStream that will throw a [[ClosedChannelException]] if read returns -1.
* @return the wrapped InputStream.
*/
private[sbt] def throwOnClosedSystemIn: InputStream = new InputStream {
override def available(): Int = WrappedSystemIn.available()
override def read(): Int = WrappedSystemIn.read() match {
case -1 => throw new ClosedChannelException
case r => r
}
}
/**
* Provides a wrapper around System.in. The wrapped stream in will check if the terminal is attached
* in available and read. If a read returns -1, it will mark System.in as unattached so that
* it can be detected by [[systemInIsAttached]].
*
* @return the wrapped InputStream
*/
private[sbt] def wrappedSystemIn: InputStream = WrappedSystemIn
/**
* Restore the terminal to its initial state.
*/
private[sbt] def restore(): Unit = terminal.restore()
/**
* Runs a thunk ensuring that the terminal has echo enabled. Most of the time sbt should have
* echo mode on except when it is explicitly set to raw mode via [[withRawSystemIn]].
*
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withEcho[T](toggle: Boolean)(f: => T): T = {
val previous = terminal.isEchoEnabled
terminalLock.lockInterruptibly()
try {
terminal.setEchoEnabled(toggle)
f
} finally {
terminal.setEchoEnabled(previous)
terminalLock.unlock()
}
}
/**
*
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withStreams[T](f: => T): T =
if (System.getProperty("sbt.io.virtual", "true") == "true") {
withOut(withIn(f))
} else f
/**
* Runs a thunk ensuring that the terminal is in canonical mode:
* [[https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html Canonical or Not]].
* Most of the time sbt should be in canonical mode except when it is explicitly set to raw mode
* via [[withRawSystemIn]].
*
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withCanonicalIn[T](f: => T): T = withTerminal { t =>
t.restore()
f
}
/**
* Runs a thunk ensuring that the terminal is in in non-canonical mode:
* [[https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html Canonical or Not]].
* This should be used when sbt is reading user input, e.g. in `shell` or a continuous build.
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withRawSystemIn[T](f: => T): T = withTerminal { t =>
t.init()
f
}
private[this] def withTerminal[T](f: jline.Terminal => T): T = {
val t = terminal
terminalLock.lockInterruptibly()
try f(t)
finally {
t.restore()
terminalLock.unlock()
}
}
private[this] val originalOut = System.out
private[this] val originalIn = System.in
private[this] val currentLine = new AtomicReference(new ArrayBuffer[Byte])
private[this] val lineBuffer = new LinkedBlockingQueue[Byte]
private[this] val flushQueue = new LinkedBlockingQueue[Unit]
private[this] val writeLock = new AnyRef
private[this] final class WriteThread extends Thread("sbt-stdout-write-thread") {
setDaemon(true)
start()
private[this] val isStopped = new AtomicBoolean(false)
def close(): Unit = {
isStopped.set(true)
flushQueue.put(())
()
}
@tailrec override def run(): Unit = {
try {
flushQueue.take()
val bytes = new java.util.ArrayList[Byte]
writeLock.synchronized {
lineBuffer.drainTo(bytes)
import scala.collection.JavaConverters._
val remaining = bytes.asScala.foldLeft(new ArrayBuffer[Byte]) { (buf, i) =>
if (i == 10) {
ProgressState.addBytes(buf)
ProgressState.clearBytes()
buf.foreach(b => originalOut.write(b & 0xFF))
ProgressState.reprint(originalOut)
currentLine.set(new ArrayBuffer[Byte])
new ArrayBuffer[Byte]
} else buf += i
}
if (remaining.nonEmpty) {
currentLine.get ++= remaining
originalOut.write(remaining.toArray)
}
originalOut.flush()
}
} catch { case _: InterruptedException => isStopped.set(true) }
if (!isStopped.get) run()
}
}
private[this] def withOut[T](f: => T): T = {
val thread = new WriteThread
try {
System.setOut(SystemPrintStream)
scala.Console.withOut(SystemPrintStream)(f)
} finally {
thread.close()
System.setOut(originalOut)
}
}
private[this] def withIn[T](f: => T): T =
try {
System.setIn(Terminal.wrappedSystemIn)
scala.Console.withIn(Terminal.wrappedSystemIn)(f)
} finally System.setIn(originalIn)
private[sbt] def withPrintStream[T](f: PrintStream => T): T = writeLock.synchronized {
f(originalOut)
}
private object SystemOutputStream extends OutputStream {
override def write(b: Int): Unit = writeLock.synchronized(lineBuffer.put(b.toByte))
override def write(b: Array[Byte]): Unit = writeLock.synchronized(b.foreach(lineBuffer.put))
override def write(b: Array[Byte], off: Int, len: Int): Unit = writeLock.synchronized {
val lo = math.max(0, off)
val hi = math.min(math.max(off + len, 0), b.length)
(lo until hi).foreach(i => lineBuffer.put(b(i)))
}
def write(s: String): Unit = s.getBytes.foreach(lineBuffer.put)
override def flush(): Unit = writeLock.synchronized(flushQueue.put(()))
}
private object SystemPrintStream extends PrintStream(SystemOutputStream, true)
private[this] object WrappedSystemIn extends InputStream {
private[this] val in = terminal.wrapInIfNeeded(System.in)
override def available(): Int = if (attached.get) in.available else 0
override def read(): Int = synchronized {
if (attached.get) {
val res = in.read
if (res == -1) attached.set(false)
res
} else -1
}
}
private[this] val terminalLock = new ReentrantLock()
private[this] val attached = new AtomicBoolean(true)
private[this] val terminalHolder = new AtomicReference(wrap(jline.TerminalFactory.get))
private[this] lazy val isWindows =
System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
private[this] def wrap(terminal: jline.Terminal): jline.Terminal = {
val term: jline.Terminal = new jline.Terminal {
private[this] val hasConsole = System.console != null
private[this] def alive = hasConsole && attached.get
override def init(): Unit = if (alive) terminal.init()
override def restore(): Unit = if (alive) terminal.restore()
override def reset(): Unit = if (alive) terminal.reset()
override def isSupported: Boolean = terminal.isSupported
override def getWidth: Int = terminal.getWidth
override def getHeight: Int = terminal.getHeight
override def isAnsiSupported: Boolean = terminal.isAnsiSupported
override def wrapOutIfNeeded(out: OutputStream): OutputStream = terminal.wrapOutIfNeeded(out)
override def wrapInIfNeeded(in: InputStream): InputStream = terminal.wrapInIfNeeded(in)
override def hasWeirdWrap: Boolean = terminal.hasWeirdWrap
override def isEchoEnabled: Boolean = terminal.isEchoEnabled
override def setEchoEnabled(enabled: Boolean): Unit = if (alive) {
terminal.setEchoEnabled(enabled)
}
override def disableInterruptCharacter(): Unit =
if (alive) terminal.disableInterruptCharacter()
override def enableInterruptCharacter(): Unit =
if (alive) terminal.enableInterruptCharacter()
override def getOutputEncoding: String = terminal.getOutputEncoding
}
term.restore()
term.setEchoEnabled(true)
term
}
private[util] def reset(): Unit = {
jline.TerminalFactory.reset()
terminalHolder.set(wrap(jline.TerminalFactory.get))
}
// translate explicit class names to type in order to support
// older Scala, since it shaded classes but not the system property
private[this] def fixTerminalProperty(): Unit = {
val terminalProperty = "jline.terminal"
val newValue = System.getProperty(terminalProperty) match {
case "jline.UnixTerminal" => "unix"
case null if System.getProperty("sbt.cygwin") != null => "unix"
case "jline.WindowsTerminal" => "windows"
case "jline.AnsiWindowsTerminal" => "windows"
case "jline.UnsupportedTerminal" => "none"
case x => x
}
if (newValue != null) {
System.setProperty(terminalProperty, newValue)
()
}
}
fixTerminalProperty()
private[sbt] def createReader(in: InputStream): ConsoleReader =
new ConsoleReader(in, System.out, terminal)
private[this] def terminal: jline.Terminal = terminalHolder.get match {
case null => throw new IllegalStateException("Uninitialized terminal.")
case term => term
}
@deprecated("For compatibility only", "1.4.0")
private[sbt] def deprecatedTeminal: jline.Terminal = terminal
}

View File

@ -9,9 +9,10 @@ package sbt
import java.io.File import java.io.File
import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile } import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile }
import sbt.internal.util.JLine import sbt.internal.util.Terminal
import sbt.util.Logger import sbt.util.Logger
import xsbti.compile.{ Inputs, Compilers } import xsbti.compile.{ Compilers, Inputs }
import scala.util.Try import scala.util.Try
final class Console(compiler: AnalyzingCompiler) { final class Console(compiler: AnalyzingCompiler) {
@ -51,10 +52,7 @@ final class Console(compiler: AnalyzingCompiler) {
loader, loader,
bindings bindings
) )
JLine.usingTerminal { t => Terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
t.init
Run.executeTrapExit(console0, log)
}
} }
} }

View File

@ -9,15 +9,15 @@ package sbt
import java.nio.file.Paths import java.nio.file.Paths
import sbt.util.Level import sbt.util.Level
import sbt.internal.util.{ AttributeKey, FullReader } import sbt.internal.util.{ AttributeKey, FullReader, LineReader, Terminal }
import sbt.internal.util.complete.{ import sbt.internal.util.complete.{
Completion, Completion,
Completions, Completions,
DefaultParsers, DefaultParsers,
History => CHistory,
HistoryCommands, HistoryCommands,
Parser, Parser,
TokenCompletions TokenCompletions,
History => CHistory
} }
import sbt.internal.util.Types.{ const, idFun } import sbt.internal.util.Types.{ const, idFun }
import sbt.internal.util.Util.{ AnyOps, nil, nilSeq, none } import sbt.internal.util.Util.{ AnyOps, nil, nilSeq, none }
@ -25,15 +25,18 @@ import sbt.internal.inc.classpath.ClasspathUtil.toLoader
import sbt.internal.inc.ModuleUtilities import sbt.internal.inc.ModuleUtilities
import sbt.internal.client.NetworkClient import sbt.internal.client.NetworkClient
import DefaultParsers._ import DefaultParsers._
import Function.tupled import Function.tupled
import Command.applyEffect import Command.applyEffect
import BasicCommandStrings._ import BasicCommandStrings._
import CommandUtil._ import CommandUtil._
import BasicKeys._ import BasicKeys._
import java.io.File import java.io.File
import sbt.io.IO
import sbt.io.IO
import sbt.util.Level
import scala.Function.tupled
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.util.control.NonFatal import scala.util.control.NonFatal
@ -372,7 +375,8 @@ object BasicCommands {
def oldshell: Command = Command.command(OldShell, Help.more(Shell, OldShellDetailed)) { s => def oldshell: Command = Command.command(OldShell, Help.more(Shell, OldShellDetailed)) { s =>
val history = (s get historyPath) getOrElse (new File(s.baseDir, ".history")).some val history = (s get historyPath) getOrElse (new File(s.baseDir, ".history")).some
val prompt = (s get shellPrompt) match { case Some(pf) => pf(s); case None => "> " } val prompt = (s get shellPrompt) match { case Some(pf) => pf(s); case None => "> " }
val reader = new FullReader(history, s.combinedParser) val reader =
new FullReader(history, s.combinedParser, LineReader.HandleCONT, Terminal.wrappedSystemIn)
val line = reader.readLine(prompt) val line = reader.readLine(prompt)
line match { line match {
case Some(line) => case Some(line) =>

View File

@ -62,4 +62,5 @@ case class ConsolePromptEvent(state: State) extends EventMessage
/* /*
* This is a data passed specifically for unprompting local console. * This is a data passed specifically for unprompting local console.
*/ */
@deprecated("No longer used", "1.4.0")
case class ConsoleUnpromptEvent(lastSource: Option[CommandSource]) extends EventMessage case class ConsoleUnpromptEvent(lastSource: Option[CommandSource]) extends EventMessage

View File

@ -8,32 +8,55 @@
package sbt package sbt
package internal package internal
import sbt.internal.util._
import BasicKeys._
import java.io.File import java.io.File
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicReference
import sbt.BasicKeys._
import sbt.internal.util._
import sbt.protocol.EventMessage import sbt.protocol.EventMessage
import sjsonnew.JsonFormat import sjsonnew.JsonFormat
import Util.AnyOps
private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel { private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel {
private var askUserThread: Option[Thread] = None private[this] val askUserThread = new AtomicReference[AskUserThread]
def makeAskUserThread(s: State): Thread = new Thread("ask-user-thread") { private[this] def getPrompt(s: State): String = s.get(shellPrompt) match {
val history = (s get historyPath) getOrElse (new File(s.baseDir, ".history")).some case Some(pf) => pf(s)
val prompt = (s get shellPrompt) match { case None =>
case Some(pf) => pf(s) def ansi(s: String): String = if (ConsoleAppender.formatEnabledInEnv) s"$s" else ""
case None => "> " s"${ansi(ConsoleAppender.DeleteLine)}> ${ansi(ConsoleAppender.ClearScreenAfterCursor)}"
} }
val reader = new FullReader(history, s.combinedParser, JLine.HandleCONT, true) private[this] class AskUserThread(s: State) extends Thread("ask-user-thread") {
override def run(): Unit = { private val history = s.get(historyPath).getOrElse(Some(new File(s.baseDir, ".history")))
// This internally handles thread interruption and returns Some("") private val prompt = getPrompt(s)
val line = reader.readLine(prompt) private val reader =
line match { new FullReader(
case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name)))) history,
case None => append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name)))) s.combinedParser,
} LineReader.HandleCONT,
askUserThread = None Terminal.throwOnClosedSystemIn
)
setDaemon(true)
start()
override def run(): Unit =
try {
reader.readLine(prompt) match {
case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name))))
case None =>
println("") // Prevents server shutdown log lines from appearing on the prompt line
append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name))))
}
()
} catch {
case _: ClosedChannelException =>
} finally askUserThread.synchronized(askUserThread.set(null))
def redraw(): Unit = {
System.out.print(ConsoleAppender.clearLine(0))
reader.redraw()
System.out.print(ConsoleAppender.ClearScreenAfterCursor)
System.out.flush()
} }
} }
private[this] def makeAskUserThread(s: State): AskUserThread = new AskUserThread(s)
def run(s: State): State = s def run(s: State): State = s
@ -44,33 +67,24 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel
def publishEventMessage(event: EventMessage): Unit = def publishEventMessage(event: EventMessage): Unit =
event match { event match {
case e: ConsolePromptEvent => case e: ConsolePromptEvent =>
askUserThread match { if (Terminal.systemInIsAttached) {
case Some(_) => askUserThread.synchronized {
case _ => askUserThread.get match {
val x = makeAskUserThread(e.state) case null => askUserThread.set(makeAskUserThread(e.state))
askUserThread = Some(x) case t => t.redraw()
x.start()
}
case e: ConsoleUnpromptEvent =>
e.lastSource match {
case Some(src) if src.channelName != name =>
askUserThread match {
case Some(_) =>
// keep listening while network-origin command is running
// make sure to test Windows and Cygwin, if you uncomment
// shutdown()
case _ =>
} }
case _ => }
} }
case _ => // case _ => //
} }
def shutdown(): Unit = def shutdown(): Unit = askUserThread.synchronized {
askUserThread match { askUserThread.get match {
case Some(x) if x.isAlive => case null =>
x.interrupt() case t if t.isAlive =>
askUserThread = None t.interrupt()
askUserThread.set(null)
case _ => () case _ => ()
} }
}
} }

View File

@ -12,19 +12,21 @@ package client
import java.io.{ File, IOException } import java.io.{ File, IOException }
import java.util.UUID import java.util.UUID
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import scala.collection.mutable.ListBuffer
import scala.util.control.NonFatal
import scala.util.{ Success, Failure }
import scala.sys.process.{ BasicIO, Process, ProcessLogger }
import sbt.protocol._
import sbt.internal.protocol._
import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams } import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams }
import sbt.internal.util.{ JLine, ConsoleAppender } import sbt.internal.protocol._
import sbt.util.Level import sbt.internal.util.{ ConsoleAppender, LineReader }
import sbt.io.syntax._
import sbt.io.IO import sbt.io.IO
import sbt.io.syntax._
import sbt.protocol._
import sbt.util.Level
import sjsonnew.support.scalajson.unsafe.Converter import sjsonnew.support.scalajson.unsafe.Converter
import scala.collection.mutable.ListBuffer
import scala.sys.process.{ BasicIO, Process, ProcessLogger }
import scala.util.control.NonFatal
import scala.util.{ Failure, Success }
class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[String]) { self => class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[String]) { self =>
private val channelName = new AtomicReference("_") private val channelName = new AtomicReference("_")
private val status = new AtomicReference("Ready") private val status = new AtomicReference("Ready")
@ -214,7 +216,7 @@ class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[Strin
} }
def shell(): Unit = { def shell(): Unit = {
val reader = JLine.simple(None, JLine.HandleCONT, injectThreadSleep = true) val reader = LineReader.simple(None, LineReader.HandleCONT, injectThreadSleep = true)
while (running.get) { while (running.get) {
reader.readLine("> ", None) match { reader.readLine("> ", None) match {
case Some("shutdown") => case Some("shutdown") =>

View File

@ -7,7 +7,7 @@
package sbt package sbt
import sbt.internal.util.{ JLine, SimpleReader } import sbt.internal.util.{ SimpleReader, Terminal }
trait CommandLineUIService extends InteractionService { trait CommandLineUIService extends InteractionService {
override def readLine(prompt: String, mask: Boolean): Option[String] = { override def readLine(prompt: String, mask: Boolean): Option[String] = {
@ -27,9 +27,9 @@ trait CommandLineUIService extends InteractionService {
} }
} }
override def terminalWidth: Int = JLine.terminal.getWidth override def terminalWidth: Int = Terminal.getWidth
override def terminalHeight: Int = JLine.terminal.getHeight override def terminalHeight: Int = Terminal.getHeight
} }
object CommandLineUIService extends CommandLineUIService object CommandLineUIService extends CommandLineUIService

View File

@ -716,7 +716,15 @@ object Defaults extends BuildCommon {
forkOptions := forkOptionsTask.value, forkOptions := forkOptionsTask.value,
selectMainClass := mainClass.value orElse askForMainClass(discoveredMainClasses.value), selectMainClass := mainClass.value orElse askForMainClass(discoveredMainClasses.value),
mainClass in run := (selectMainClass in run).value, mainClass in run := (selectMainClass in run).value,
mainClass := pickMainClassOrWarn(discoveredMainClasses.value, streams.value.log), mainClass := {
val logWarning = state.value.currentCommand
.flatMap(_.commandLine.split(" ").headOption.map(_.trim))
.fold(true) {
case "run" | "runMain" => false
case _ => true
}
pickMainClassOrWarn(discoveredMainClasses.value, streams.value.log, logWarning)
},
runMain := foregroundRunMainTask.evaluated, runMain := foregroundRunMainTask.evaluated,
run := foregroundRunTask.evaluated, run := foregroundRunTask.evaluated,
fgRun := runTask(fullClasspath, mainClass in run, runner in run).evaluated, fgRun := runTask(fullClasspath, mainClass in run, runner in run).evaluated,
@ -1478,17 +1486,38 @@ object Defaults extends BuildCommon {
} }
def askForMainClass(classes: Seq[String]): Option[String] = def askForMainClass(classes: Seq[String]): Option[String] =
sbt.SelectMainClass(Some(SimpleReader readLine _), classes) sbt.SelectMainClass(
if (classes.length >= 10) Some(SimpleReader.readLine(_))
else
Some(s => {
def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() }
print(s)
Terminal.withRawSystemIn {
Terminal.wrappedSystemIn.read match {
case -1 => None
case b =>
val res = b.toChar.toString
println(res)
Some(res)
}
}
}),
classes
)
def pickMainClass(classes: Seq[String]): Option[String] = def pickMainClass(classes: Seq[String]): Option[String] =
sbt.SelectMainClass(None, classes) sbt.SelectMainClass(None, classes)
private def pickMainClassOrWarn(classes: Seq[String], logger: Logger): Option[String] = { private def pickMainClassOrWarn(
classes: Seq[String],
logger: Logger,
logWarning: Boolean
): Option[String] = {
classes match { classes match {
case multiple if multiple.size > 1 => case multiple if multiple.size > 1 && logWarning =>
logger.warn( val msg =
"Multiple main classes detected. Run 'show discoveredMainClasses' to see the list" "multiple main classes detected: run 'show discoveredMainClasses' to see the list"
) logger.warn(msg)
case _ => case _ =>
} }
pickMainClass(classes) pickMainClass(classes)

View File

@ -260,10 +260,7 @@ object EvaluateTask {
ps.reset() ps.reset()
ConsoleAppender.setShowProgress(true) ConsoleAppender.setShowProgress(true)
val appender = MainAppender.defaultScreen(StandardMain.console) val appender = MainAppender.defaultScreen(StandardMain.console)
appender match { ProgressState.set(ps)
case c: ConsoleAppender => c.setProgressState(ps)
case _ =>
}
val log = LogManager.progressLogger(appender) val log = LogManager.progressLogger(appender)
Some(new TaskProgress(log)) Some(new TaskProgress(log))
case _ => None case _ => None

View File

@ -9,10 +9,10 @@ package sbt
import java.io.{ File, IOException } import java.io.{ File, IOException }
import java.net.URI import java.net.URI
import java.nio.file.{ FileAlreadyExistsException, Files, FileSystems } import java.nio.file.{ FileAlreadyExistsException, FileSystems, Files }
import java.util.Properties
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.{ Locale, Properties }
import sbt.BasicCommandStrings.{ Shell, TemplateCommand } import sbt.BasicCommandStrings.{ Shell, TemplateCommand }
import sbt.Project.LoadAction import sbt.Project.LoadAction
@ -23,7 +23,7 @@ import sbt.internal._
import sbt.internal.inc.ScalaInstance import sbt.internal.inc.ScalaInstance
import sbt.internal.util.Types.{ const, idFun } import sbt.internal.util.Types.{ const, idFun }
import sbt.internal.util._ import sbt.internal.util._
import sbt.internal.util.complete.{ SizeParser, Parser } import sbt.internal.util.complete.{ Parser, SizeParser }
import sbt.io._ import sbt.io._
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.util.{ Level, Logger, Show } import sbt.util.{ Level, Logger, Show }
@ -50,21 +50,20 @@ private[sbt] object xMain {
// if we detect -Dsbt.client=true or -client, run thin client. // if we detect -Dsbt.client=true or -client, run thin client.
val clientModByEnv = SysProp.client val clientModByEnv = SysProp.client
val userCommands = configuration.arguments.map(_.trim) val userCommands = configuration.arguments.map(_.trim)
if (clientModByEnv || (userCommands.exists { cmd => val isClient: String => Boolean = cmd => (cmd == DashClient) || (cmd == DashDashClient)
(cmd == DashClient) || (cmd == DashDashClient) Terminal.withStreams {
})) { if (clientModByEnv || userCommands.exists(isClient)) {
val args = userCommands.toList filterNot { cmd => val args = userCommands.toList.filterNot(isClient)
(cmd == DashClient) || (cmd == DashDashClient) NetworkClient.run(configuration, args)
Exit(0)
} else {
val state = StandardMain.initialState(
configuration,
Seq(defaults, early),
runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil
)
StandardMain.runManaged(state)
} }
NetworkClient.run(configuration, args)
Exit(0)
} else {
val state = StandardMain.initialState(
configuration,
Seq(defaults, early),
runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil
)
StandardMain.runManaged(state)
} }
} finally { } finally {
ShutdownHooks.close() ShutdownHooks.close()
@ -764,22 +763,24 @@ object BuiltinCommands {
@tailrec @tailrec
private[this] def doLoadFailed(s: State, loadArg: String): State = { private[this] def doLoadFailed(s: State, loadArg: String): State = {
val result = (SimpleReader.readLine( s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)")
"Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)" val result = Terminal.withRawSystemIn {
) getOrElse Quit) Terminal.withEcho(toggle = true)(Terminal.wrappedSystemIn.read() match {
.toLowerCase(Locale.ENGLISH) case -1 => 'q'.toInt
def matches(s: String) = !result.isEmpty && (s startsWith result) case b => b
def retry = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog })
def ignoreMsg = }
def retry: State = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog
def ignoreMsg: String =
if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded" if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded"
result match { result.toChar match {
case "" => retry case '\n' | '\r' => retry
case _ if matches("retry") => retry case 'r' | 'R' => retry
case _ if matches(Quit) => s.exit(ok = false) case 'q' | 'Q' => s.exit(ok = false)
case _ if matches("ignore") => s.log.warn(s"Ignoring load failure: $ignoreMsg."); s case 'i' | 'I' => s.log.warn(s"Ignoring load failure: $ignoreMsg."); s
case _ if matches("last") => LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s case 'l' | 'L' => LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s
case _ => println("Invalid response."); doLoadFailed(s, loadArg) case c => println(s"Invalid response: '$c'"); doLoadFailed(s, loadArg)
} }
} }
@ -888,7 +889,7 @@ object BuiltinCommands {
} }
def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 => def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 =>
import sbt.internal.{ ConsolePromptEvent, ConsoleUnpromptEvent } import sbt.internal.ConsolePromptEvent
val exchange = StandardMain.exchange val exchange = StandardMain.exchange
val welcomeState = displayWelcomeBanner(s0) val welcomeState = displayWelcomeBanner(s0)
val s1 = exchange run welcomeState val s1 = exchange run welcomeState
@ -898,13 +899,15 @@ object BuiltinCommands {
.getOpt(Keys.minForcegcInterval) .getOpt(Keys.minForcegcInterval)
.getOrElse(GCUtil.defaultMinForcegcInterval) .getOrElse(GCUtil.defaultMinForcegcInterval)
val exec: Exec = exchange.blockUntilNextExec(minGCInterval, s1.globalLogging.full) val exec: Exec = exchange.blockUntilNextExec(minGCInterval, s1.globalLogging.full)
if (exec.source.fold(true)(_.channelName != "console0")) {
s1.log.info(s"received remote command: ${exec.commandLine}")
}
val newState = s1 val newState = s1
.copy( .copy(
onFailure = Some(Exec(Shell, None)), onFailure = Some(Exec(Shell, None)),
remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands
) )
.setInteractive(true) .setInteractive(true)
exchange publishEventMessage ConsoleUnpromptEvent(exec.source)
if (exec.commandLine.trim.isEmpty) newState if (exec.commandLine.trim.isEmpty) newState
else newState.clearGlobalLog else newState.clearGlobalLog
} }

View File

@ -10,12 +10,11 @@ package sbt
import java.io.PrintWriter import java.io.PrintWriter
import java.util.Properties import java.util.Properties
import jline.TerminalFactory
import sbt.internal.{ Aggregation, ShutdownHooks } import sbt.internal.{ Aggregation, ShutdownHooks }
import sbt.internal.langserver.ErrorCodes import sbt.internal.langserver.ErrorCodes
import sbt.internal.protocol.JsonRpcResponseError import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking } import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Terminal }
import sbt.io.{ IO, Using } import sbt.io.{ IO, Using }
import sbt.protocol._ import sbt.protocol._
import sbt.util.Logger import sbt.util.Logger
@ -31,7 +30,7 @@ object MainLoop {
// We've disabled jline shutdown hooks to prevent classloader leaks, and have been careful to always restore // We've disabled jline shutdown hooks to prevent classloader leaks, and have been careful to always restore
// the jline terminal in finally blocks, but hitting ctrl+c prevents finally blocks from being executed, in that // the jline terminal in finally blocks, but hitting ctrl+c prevents finally blocks from being executed, in that
// case the only way to restore the terminal is in a shutdown hook. // case the only way to restore the terminal is in a shutdown hook.
val shutdownHook = ShutdownHooks.add(() => TerminalFactory.get().restore()) val shutdownHook = ShutdownHooks.add(Terminal.restore)
try { try {
runLoggedLoop(state, state.globalLogging.backing) runLoggedLoop(state, state.globalLogging.backing)

View File

@ -19,7 +19,7 @@ import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.langserver.{ LogMessageParams, MessageType } import sbt.internal.langserver.{ LogMessageParams, MessageType }
import sbt.internal.server._ import sbt.internal.server._
import sbt.internal.util.codec.JValueFormats import sbt.internal.util.codec.JValueFormats
import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent } import sbt.internal.util.{ ConsoleOut, MainAppender, ObjectEvent, StringEvent, Terminal }
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.io.{ Hash, IO } import sbt.io.{ Hash, IO }
import sbt.protocol.{ EventMessage, ExecStatusEvent } import sbt.protocol.{ EventMessage, ExecStatusEvent }
@ -50,6 +50,7 @@ private[sbt] final class CommandExchange {
private val channelBufferLock = new AnyRef {} private val channelBufferLock = new AnyRef {}
private val commandChannelQueue = new LinkedBlockingQueue[CommandChannel] private val commandChannelQueue = new LinkedBlockingQueue[CommandChannel]
private val nextChannelId: AtomicInteger = new AtomicInteger(0) private val nextChannelId: AtomicInteger = new AtomicInteger(0)
private[this] val activePrompt = new AtomicBoolean(false)
private lazy val jsonFormat = new sjsonnew.BasicJsonProtocol with JValueFormats {} private lazy val jsonFormat = new sjsonnew.BasicJsonProtocol with JValueFormats {}
def channels: List[CommandChannel] = channelBuffer.toList def channels: List[CommandChannel] = channelBuffer.toList
@ -83,7 +84,11 @@ private[sbt] final class CommandExchange {
commandChannelQueue.poll(1, TimeUnit.SECONDS) commandChannelQueue.poll(1, TimeUnit.SECONDS)
slurpMessages() slurpMessages()
Option(commandQueue.poll) match { Option(commandQueue.poll) match {
case Some(x) => x case Some(exec) =>
val needFinish = needToFinishPromptLine()
if (exec.source.fold(needFinish)(s => needFinish && s.channelName != "console0"))
ConsoleOut.systemOut.println("")
exec
case None => case None =>
val newDeadline = if (deadline.fold(false)(_.isOverdue())) { val newDeadline = if (deadline.fold(false)(_.isOverdue())) {
GCUtil.forceGcWithInterval(interval, logger) GCUtil.forceGcWithInterval(interval, logger)
@ -129,6 +134,7 @@ private[sbt] final class CommandExchange {
def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = { def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = {
val name = newNetworkName val name = newNetworkName
if (needToFinishPromptLine()) ConsoleOut.systemOut.println("")
s.log.info(s"new client connected: $name") s.log.info(s"new client connected: $name")
val logger: Logger = { val logger: Logger = {
val log = LogExchange.logger(name, None, None) val log = LogExchange.logger(name, None, None)
@ -362,11 +368,9 @@ private[sbt] final class CommandExchange {
// Special treatment for ConsolePromptEvent since it's hand coded without codec. // Special treatment for ConsolePromptEvent since it's hand coded without codec.
case entry: ConsolePromptEvent => case entry: ConsolePromptEvent =>
channels collect { channels collect {
case c: ConsoleChannel => c.publishEventMessage(entry) case c: ConsoleChannel =>
} c.publishEventMessage(entry)
case entry: ConsoleUnpromptEvent => activePrompt.set(Terminal.systemInIsAttached)
channels collect {
case c: ConsoleChannel => c.publishEventMessage(entry)
} }
case entry: ExecStatusEvent => case entry: ExecStatusEvent =>
channels collect { channels collect {
@ -384,4 +388,5 @@ private[sbt] final class CommandExchange {
removeChannels(toDel.toList) removeChannels(toDel.toList)
} }
private[this] def needToFinishPromptLine(): Boolean = activePrompt.compareAndSet(true, false)
} }

View File

@ -10,7 +10,7 @@ package internal
import sbt.internal.classpath.AlternativeZincUtil import sbt.internal.classpath.AlternativeZincUtil
import sbt.internal.inc.{ ScalaInstance, ZincLmUtil } import sbt.internal.inc.{ ScalaInstance, ZincLmUtil }
import sbt.internal.util.JLine import sbt.internal.util.Terminal
import sbt.util.Logger import sbt.util.Logger
import xsbti.compile.ClasspathOptionsUtil import xsbti.compile.ClasspathOptionsUtil
@ -61,7 +61,7 @@ object ConsoleProject {
val importString = imports.mkString("", ";\n", ";\n\n") val importString = imports.mkString("", ";\n", ";\n\n")
val initCommands = importString + extra val initCommands = importString + extra
JLine.usingTerminal { _ => Terminal.withCanonicalIn {
// TODO - Hook up dsl classpath correctly... // TODO - Hook up dsl classpath correctly...
(new Console(compiler))( (new Console(compiler))(
unit.classpath, unit.classpath,

View File

@ -26,7 +26,7 @@ import sbt.internal.io.WatchState
import sbt.internal.nio._ import sbt.internal.nio._
import sbt.internal.util.complete.Parser._ import sbt.internal.util.complete.Parser._
import sbt.internal.util.complete.{ Parser, Parsers } import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.{ AttributeKey, JLine, Util } import sbt.internal.util.{ AttributeKey, Terminal, Util }
import sbt.nio.Keys.{ fileInputs, _ } import sbt.nio.Keys.{ fileInputs, _ }
import sbt.nio.Watch.{ Creation, Deletion, ShowOptions, Update } import sbt.nio.Watch.{ Creation, Deletion, ShowOptions, Update }
import sbt.nio.file.{ FileAttributes, Glob } import sbt.nio.file.{ FileAttributes, Glob }
@ -272,11 +272,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
f(s, valid, invalid) f(s, valid, invalid)
} }
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = { private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = Terminal.withRawSystemIn {
val terminal = JLine.terminal val wrapped = Terminal.wrappedSystemIn
terminal.init()
terminal.setEchoEnabled(true)
val wrapped = terminal.wrapInIfNeeded(System.in)
if (Util.isNonCygwinWindows) { if (Util.isNonCygwinWindows) {
val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable { val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable {
private[this] val buffer = new java.util.LinkedList[Int] private[this] val buffer = new java.util.LinkedList[Int]
@ -288,10 +285,13 @@ private[sbt] object Continuous extends DeprecatedContinuous {
override def run(): Unit = { override def run(): Unit = {
try { try {
if (!closed.get()) { if (!closed.get()) {
buffer.add(wrapped.read()) wrapped.read() match {
case -1 => closed.set(true)
case b => buffer.add(b)
}
} }
} catch { } catch {
case _: InterruptedException => case _: InterruptedException => closed.set(true)
} }
if (!closed.get()) run() if (!closed.get()) run()
} }

View File

@ -9,6 +9,7 @@ package sbt
package internal package internal
import java.io.PrintWriter import java.io.PrintWriter
import Def.ScopedKey import Def.ScopedKey
import Scope.GlobalScope import Scope.GlobalScope
import Keys.{ logLevel, logManager, persistLogLevel, persistTraceLevel, sLog, traceLevel } import Keys.{ logLevel, logManager, persistLogLevel, persistTraceLevel, sLog, traceLevel }
@ -16,13 +17,14 @@ import sbt.internal.util.{
AttributeKey, AttributeKey,
ConsoleAppender, ConsoleAppender,
ConsoleOut, ConsoleOut,
MainAppender,
ManagedLogger,
ProgressState,
Settings, Settings,
SuppressedTraceContext, SuppressedTraceContext
MainAppender
} }
import MainAppender._ import MainAppender._
import sbt.util.{ Level, Logger, LogExchange } import sbt.util.{ Level, LogExchange, Logger }
import sbt.internal.util.ManagedLogger
import org.apache.logging.log4j.core.Appender import org.apache.logging.log4j.core.Appender
sealed abstract class LogManager { sealed abstract class LogManager {
@ -142,10 +144,7 @@ object LogManager {
val extraBacked = state.globalLogging.backed :: relay :: Nil val extraBacked = state.globalLogging.backed :: relay :: Nil
val ps = Project.extract(state).get(sbt.Keys.progressState in ThisBuild) val ps = Project.extract(state).get(sbt.Keys.progressState in ThisBuild)
val consoleOpt = consoleLocally(state, console) val consoleOpt = consoleLocally(state, console)
consoleOpt foreach { ps.foreach(ProgressState.set)
case a: ConsoleAppender => ps.foreach(a.setProgressState)
case _ =>
}
val config = MainAppender.MainAppenderConfig( val config = MainAppender.MainAppenderConfig(
consoleOpt, consoleOpt,
backed, backed,

View File

@ -8,13 +8,13 @@
package sbt package sbt
package internal package internal
import sbt.internal.util.{ JLine }
import sbt.util.Show import sbt.util.Show
import java.io.File import java.io.File
import Def.{ compiled, flattenLocals, ScopedKey }
import Predef.{ any2stringadd => _, _ }
import Def.{ ScopedKey, compiled, flattenLocals }
import sbt.internal.util.Terminal
import Predef.{ any2stringadd => _, _ }
import sbt.io.IO import sbt.io.IO
object SettingGraph { object SettingGraph {
@ -82,7 +82,7 @@ object Graph {
// [info] | // [info] |
// [info] +-quux // [info] +-quux
def toAscii[A](top: A, children: A => Seq[A], display: A => String, defaultWidth: Int): String = { def toAscii[A](top: A, children: A => Seq[A], display: A => String, defaultWidth: Int): String = {
val maxColumn = math.max(JLine.terminal.getWidth, defaultWidth) - 8 val maxColumn = math.max(Terminal.getWidth, defaultWidth) - 8
val twoSpaces = " " + " " // prevent accidentally being converted into a tab val twoSpaces = " " + " " // prevent accidentally being converted into a tab
def limitLine(s: String): String = def limitLine(s: String): String =
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".." if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."

View File

@ -90,11 +90,11 @@ object SysProp {
def fileCacheSize: Long = def fileCacheSize: Long =
SizeParser(System.getProperty("sbt.file.cache.size", "128M")).getOrElse(128L * 1024 * 1024) SizeParser(System.getProperty("sbt.file.cache.size", "128M")).getOrElse(128L * 1024 * 1024)
def dumbTerm: Boolean = sys.env.get("TERM").filter(_ == "dumb").isDefined def dumbTerm: Boolean = sys.env.get("TERM").contains("dumb")
def supershell: Boolean = booleanOpt("sbt.supershell").getOrElse(!dumbTerm && color) def supershell: Boolean = booleanOpt("sbt.supershell").getOrElse(!dumbTerm && color)
def supershellSleep: Long = long("sbt.supershell.sleep", 100L) def supershellSleep: Long = long("sbt.supershell.sleep", 100L)
def supershellBlankZone: Int = int("sbt.supershell.blankzone", 5) def supershellBlankZone: Int = int("sbt.supershell.blankzone", 1)
def defaultUseCoursier: Boolean = { def defaultUseCoursier: Boolean = {
val coursierOpt = booleanOpt("sbt.coursier") val coursierOpt = booleanOpt("sbt.coursier")

View File

@ -14,6 +14,7 @@ import sbt.internal.util._
import sbt.util.Level import sbt.util.Level
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent.duration._
/** /**
* implements task progress display on the shell. * implements task progress display on the shell.
@ -23,22 +24,24 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
with ExecuteProgress[Task] { with ExecuteProgress[Task] {
private[this] val lastTaskCount = new AtomicInteger(0) private[this] val lastTaskCount = new AtomicInteger(0)
private[this] val currentProgressThread = new AtomicReference[Option[ProgressThread]](None) private[this] val currentProgressThread = new AtomicReference[Option[ProgressThread]](None)
private[this] val sleepDuration = SysProp.supershellSleep private[this] val sleepDuration = SysProp.supershellSleep.millis
private[this] val threshold = 10.millis
private[this] final class ProgressThread private[this] final class ProgressThread
extends Thread("task-progress-report-thread") extends Thread("task-progress-report-thread")
with AutoCloseable { with AutoCloseable {
private[this] val isClosed = new AtomicBoolean(false) private[this] val isClosed = new AtomicBoolean(false)
private[this] val firstTime = new AtomicBoolean(true)
setDaemon(true) setDaemon(true)
start() start()
@tailrec override def run(): Unit = { @tailrec override def run(): Unit = {
if (!isClosed.get()) { if (!isClosed.get()) {
try { try {
report() report()
Thread.sleep(sleepDuration) val duration =
if (active.isEmpty) TaskProgress.this.stop() if (firstTime.compareAndSet(true, activeExceedingThreshold.nonEmpty)) threshold
} catch { else sleepDuration
case _: InterruptedException => Thread.sleep(duration.toMillis)
} } catch { case _: InterruptedException => isClosed.set(true) }
run() run()
} }
} }
@ -65,9 +68,7 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
override def afterAllCompleted(results: RMap[Task, Result]): Unit = { override def afterAllCompleted(results: RMap[Task, Result]): Unit = {
// send an empty progress report to clear out the previous report // send an empty progress report to clear out the previous report
val event = ProgressEvent("Info", Vector(), Some(lastTaskCount.get), None, None) appendProgress(ProgressEvent("Info", Vector(), Some(lastTaskCount.get), None, None))
import sbt.internal.util.codec.JsonProtocol._
log.logEvent(Level.Info, event)
} }
private[this] val skipReportTasks = private[this] val skipReportTasks =
Set( Set(
@ -93,41 +94,40 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
case _ => case _ =>
} }
} }
private[this] def appendProgress(event: ProgressEvent): Unit = {
import sbt.internal.util.codec.JsonProtocol._
log.logEvent(Level.Info, event)
}
private[this] def active: Vector[Task[_]] = activeTasks.toVector.filterNot(Def.isDummy) private[this] def active: Vector[Task[_]] = activeTasks.toVector.filterNot(Def.isDummy)
private[this] def activeExceedingThreshold: Vector[(Task[_], Long)] = active.flatMap { task =>
val elapsed = timings.get(task).currentElapsedMicros
if (elapsed.micros > threshold) Some[(Task[_], Long)](task -> elapsed) else None
}
private[this] def report(): Unit = { private[this] def report(): Unit = {
val currentTasks = active val currentTasks = activeExceedingThreshold
val ltc = lastTaskCount.get val ltc = lastTaskCount.get
val currentTasksCount = currentTasks.size val currentTasksCount = currentTasks.size
def report0(tasks: Vector[Task[_]]): Unit = { def event(tasks: Vector[(Task[_], Long)]): ProgressEvent = ProgressEvent(
if (tasks.nonEmpty) maybeStartThread() "Info",
val event = ProgressEvent( tasks
"Info", .map { case (task, elapsed) => ProgressItem(taskName(task), elapsed) }
tasks .sortBy(_.elapsedMicros),
.map { task => Some(ltc),
val elapsed = timings.get(task).currentElapsedMicros None,
ProgressItem(taskName(task), elapsed) None
} )
.sortBy(_.name), if (active.nonEmpty) maybeStartThread()
Some(ltc), if (containsSkipTasks(active)) {
None,
None
)
import sbt.internal.util.codec.JsonProtocol._
log.logEvent(Level.Info, event)
}
if (containsSkipTasks(currentTasks)) {
if (ltc > 0) { if (ltc > 0) {
lastTaskCount.set(0) lastTaskCount.set(0)
report0(Vector.empty) appendProgress(event(Vector.empty))
} }
} else { } else {
lastTaskCount.set(currentTasksCount) lastTaskCount.set(currentTasksCount)
report0(currentTasks) appendProgress(event(currentTasks))
} }
} }
private[this] def containsSkipTasks(tasks: Vector[Task[_]]): Boolean = private[this] def containsSkipTasks(tasks: Vector[Task[_]]): Boolean =
tasks tasks.map(taskName).exists(n => skipReportTasks.exists(m => m == n || n.endsWith("/ " + m)))
.map(t => taskName(t))
.exists(n => skipReportTasks.exists(m => m == n || n.endsWith("/ " + m)))
} }

View File

@ -7,6 +7,7 @@
package sbt package sbt
import sbt.internal.util.ConsoleAppender
import sbt.internal.util.Util.{ AnyOps, none } import sbt.internal.util.Util.{ AnyOps, none }
object SelectMainClass { object SelectMainClass {
@ -19,12 +20,14 @@ object SelectMainClass {
case Nil => None case Nil => None
case head :: Nil => Some(head) case head :: Nil => Some(head)
case multiple => case multiple =>
promptIfMultipleChoices flatMap { prompt => promptIfMultipleChoices.flatMap { prompt =>
println("\nMultiple main classes detected, select one to run:\n") val header = "\nMultiple main classes detected. Select one to run:\n"
for ((className, index) <- multiple.zipWithIndex) val classes = multiple.zipWithIndex
println(" [" + (index + 1) + "] " + className) .map { case (className, index) => s" [${index + 1}] $className" }
.mkString("\n")
println(ConsoleAppender.ClearScreenAfterCursor + header + classes)
val line = trim(prompt("\nEnter number: ")) val line = trim(prompt("\nEnter number: "))
println("")
toInt(line, multiple.length) map multiple.apply toInt(line, multiple.length) map multiple.apply
} }
} }