mirror of https://github.com/sbt/sbt.git
commit
5a529bf10c
|
|
@ -7,36 +7,67 @@
|
|||
|
||||
package sbt.internal.util
|
||||
|
||||
import java.io._
|
||||
|
||||
import jline.console.ConsoleReader
|
||||
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.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 {
|
||||
protected[this] def handleCONT: Boolean
|
||||
protected[this] def reader: ConsoleReader
|
||||
protected[this] def injectThreadSleep: Boolean
|
||||
protected[this] lazy val in: InputStream = {
|
||||
// On Windows InputStream#available doesn't seem to return positive number.
|
||||
JLine.makeInputStream(injectThreadSleep && !Util.isNonCygwinWindows)
|
||||
}
|
||||
@deprecated("For binary compatibility only", "1.4.0")
|
||||
protected[this] def injectThreadSleep: Boolean = false
|
||||
@deprecated("For binary compatibility only", "1.4.0")
|
||||
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 {
|
||||
JLine.withJLine {
|
||||
unsynchronizedReadLine(prompt, mask)
|
||||
}
|
||||
Terminal.withRawSystemIn(unsynchronizedReadLine(prompt, mask))
|
||||
} catch {
|
||||
case _: InterruptedException =>
|
||||
// println("readLine: InterruptedException")
|
||||
Option("")
|
||||
}
|
||||
|
||||
override def redraw(): Unit = {
|
||||
reader.drawLine()
|
||||
reader.flush()
|
||||
}
|
||||
|
||||
private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] =
|
||||
readLineWithHistory(prompt, mask) map { x =>
|
||||
x.trim
|
||||
|
|
@ -85,101 +116,73 @@ abstract class JLine extends LineReader {
|
|||
}
|
||||
|
||||
private[this] def resume(): Unit = {
|
||||
jline.TerminalFactory.reset
|
||||
JLine.terminal.init
|
||||
Terminal.reset()
|
||||
reader.drawLine()
|
||||
reader.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@deprecated("Use LineReader apis", "1.4.0")
|
||||
private[sbt] object JLine {
|
||||
private[this] val TerminalProperty = "jline.terminal"
|
||||
|
||||
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)
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
@deprecated("For binary compatibility only", "1.4.0")
|
||||
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 =
|
||||
if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds)
|
||||
else originalIn
|
||||
|
||||
// When calling this, ensure that enableEcho has been or will be called.
|
||||
// TerminalFactory.get will initialize the terminal to disable echo.
|
||||
private[sbt] def terminal: Terminal = jline.TerminalFactory.get
|
||||
|
||||
private def withTerminal[T](f: jline.Terminal => T): T =
|
||||
synchronized {
|
||||
val t = terminal
|
||||
t.synchronized { f(t) }
|
||||
}
|
||||
@deprecated("Don't use jline.Terminal directly", "1.4.0")
|
||||
private[sbt] def terminal: jline.Terminal = Terminal.deprecatedTeminal
|
||||
|
||||
/**
|
||||
* For accessing the JLine Terminal object.
|
||||
* 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 =
|
||||
withTerminal { t =>
|
||||
t.restore
|
||||
val result = f(t)
|
||||
t.restore
|
||||
result
|
||||
Terminal.withCanonicalIn(f(Terminal.deprecatedTeminal))
|
||||
|
||||
@deprecated("unused", "1.4.0")
|
||||
def createReader(): ConsoleReader = createReader(None, Terminal.wrappedSystemIn)
|
||||
|
||||
@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))
|
||||
|
||||
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("Avoid referencing JLine directly. Use Terminal.withRawSystemIn instead.", "1.4.0")
|
||||
def withJLine[T](action: => T): T = Terminal.withRawSystemIn(action)
|
||||
|
||||
@deprecated("Use LineReader.simple instead", "1.4.0")
|
||||
def simple(
|
||||
historyPath: Option[File],
|
||||
handleCONT: Boolean = HandleCONT,
|
||||
handleCONT: Boolean = LineReader.HandleCONT,
|
||||
injectThreadSleep: Boolean = false
|
||||
): SimpleReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep)
|
||||
|
||||
val MaxHistorySize = 500
|
||||
@deprecated("Use LineReader.MaxHistorySize", "1.4.0")
|
||||
val MaxHistorySize = LineReader.MaxHistorySize
|
||||
|
||||
val HandleCONT =
|
||||
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
|
||||
@deprecated("Use LineReader.HandleCONT", "1.4.0")
|
||||
val HandleCONT = LineReader.HandleCONT
|
||||
}
|
||||
|
||||
@deprecated("For binary compatibility only", "1.4.0")
|
||||
private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration)
|
||||
extends FilterInputStream(is) {
|
||||
@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(
|
||||
historyPath: Option[File],
|
||||
complete: Parser[_],
|
||||
val handleCONT: Boolean = JLine.HandleCONT,
|
||||
val injectThreadSleep: Boolean = false
|
||||
val handleCONT: Boolean,
|
||||
inputStream: InputStream,
|
||||
) extends JLine {
|
||||
protected[this] val reader = {
|
||||
val cr = JLine.createReader(historyPath, in)
|
||||
@deprecated("Use the constructor with no injectThreadSleep parameter", "1.4.0")
|
||||
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)
|
||||
cr
|
||||
}
|
||||
|
|
@ -224,9 +230,12 @@ final class FullReader(
|
|||
class SimpleReader private[sbt] (
|
||||
historyPath: Option[File],
|
||||
val handleCONT: Boolean,
|
||||
val injectThreadSleep: Boolean
|
||||
inputStream: InputStream
|
||||
) 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)
|
||||
|
|
|
|||
|
|
@ -7,17 +7,18 @@
|
|||
|
||||
package sbt.internal.util
|
||||
|
||||
import sbt.util._
|
||||
import java.io.{ PrintStream, PrintWriter }
|
||||
import java.lang.StringBuilder
|
||||
import java.util.Locale
|
||||
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 scala.util.control.NonFatal
|
||||
import ConsoleAppender._
|
||||
import org.apache.logging.log4j.core.{ LogEvent => XLogEvent }
|
||||
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 {
|
||||
// These are provided so other modules do not break immediately.
|
||||
|
|
@ -104,15 +105,17 @@ class ConsoleLogger private[ConsoleLogger] (
|
|||
}
|
||||
|
||||
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 cursorDown(n: Int): String = s"\u001B[${n}B"
|
||||
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 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] lazy val terminalWidth = usingTerminal { t =>
|
||||
t.getWidth
|
||||
}
|
||||
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
|
||||
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
|
||||
def showProgress: Boolean = showProgressHolder.get
|
||||
|
|
@ -293,31 +296,7 @@ object ConsoleAppender {
|
|||
|
||||
private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
|
||||
|
||||
private[this] def jline1to2CompatMsg = "Found class jline.Terminal, but interface was expected"
|
||||
|
||||
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
|
||||
|
||||
private[this] def ansiSupported: Boolean = Terminal.isAnsiSupported
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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 = {
|
||||
if (ansiCodesSupported && useFormat) scala.Console.RESET
|
||||
else ""
|
||||
|
|
@ -541,11 +449,7 @@ class ConsoleAppender private[ConsoleAppender] (
|
|||
private def write(msg: String): Unit = {
|
||||
val toWrite =
|
||||
if (!useFormat || !ansiCodesSupported) EscHelpers.removeEscapeSequences(msg) else msg
|
||||
if (progressState.get != null) {
|
||||
supershellInterlaceMsg(toWrite)
|
||||
} else {
|
||||
out.println(toWrite)
|
||||
}
|
||||
out.println(toWrite)
|
||||
}
|
||||
|
||||
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 = {
|
||||
def appendEvent(oe: ObjectEvent[_]): Unit = {
|
||||
val contentType = oe.contentType
|
||||
contentType match {
|
||||
case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent])
|
||||
case "sbt.internal.util.ProgressEvent" =>
|
||||
appendProgressEvent(oe.message.asInstanceOf[ProgressEvent])
|
||||
oe.message match {
|
||||
case pe: ProgressEvent => ProgressState.updateProgressState(pe)
|
||||
case _ =>
|
||||
}
|
||||
case _ =>
|
||||
LogExchange.stringCodec[AnyRef](contentType) match {
|
||||
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(
|
||||
val progressLines: AtomicReference[Seq[String]],
|
||||
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 = {
|
||||
progressLines.set(Nil)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -9,9 +9,10 @@ package sbt
|
|||
|
||||
import java.io.File
|
||||
import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile }
|
||||
import sbt.internal.util.JLine
|
||||
import sbt.internal.util.Terminal
|
||||
import sbt.util.Logger
|
||||
import xsbti.compile.{ Inputs, Compilers }
|
||||
import xsbti.compile.{ Compilers, Inputs }
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
final class Console(compiler: AnalyzingCompiler) {
|
||||
|
|
@ -51,10 +52,7 @@ final class Console(compiler: AnalyzingCompiler) {
|
|||
loader,
|
||||
bindings
|
||||
)
|
||||
JLine.usingTerminal { t =>
|
||||
t.init
|
||||
Run.executeTrapExit(console0, log)
|
||||
}
|
||||
Terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ package sbt
|
|||
|
||||
import java.nio.file.Paths
|
||||
import sbt.util.Level
|
||||
import sbt.internal.util.{ AttributeKey, FullReader }
|
||||
import sbt.internal.util.{ AttributeKey, FullReader, LineReader, Terminal }
|
||||
import sbt.internal.util.complete.{
|
||||
Completion,
|
||||
Completions,
|
||||
DefaultParsers,
|
||||
History => CHistory,
|
||||
HistoryCommands,
|
||||
Parser,
|
||||
TokenCompletions
|
||||
TokenCompletions,
|
||||
History => CHistory
|
||||
}
|
||||
import sbt.internal.util.Types.{ const, idFun }
|
||||
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.client.NetworkClient
|
||||
import DefaultParsers._
|
||||
|
||||
import Function.tupled
|
||||
import Command.applyEffect
|
||||
import BasicCommandStrings._
|
||||
import CommandUtil._
|
||||
import BasicKeys._
|
||||
|
||||
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.util.control.NonFatal
|
||||
|
||||
|
|
@ -372,7 +375,8 @@ object BasicCommands {
|
|||
def oldshell: Command = Command.command(OldShell, Help.more(Shell, OldShellDetailed)) { s =>
|
||||
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 reader = new FullReader(history, s.combinedParser)
|
||||
val reader =
|
||||
new FullReader(history, s.combinedParser, LineReader.HandleCONT, Terminal.wrappedSystemIn)
|
||||
val line = reader.readLine(prompt)
|
||||
line match {
|
||||
case Some(line) =>
|
||||
|
|
|
|||
|
|
@ -62,4 +62,5 @@ case class ConsolePromptEvent(state: State) extends EventMessage
|
|||
/*
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -8,32 +8,55 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import sbt.internal.util._
|
||||
import BasicKeys._
|
||||
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 sjsonnew.JsonFormat
|
||||
import Util.AnyOps
|
||||
|
||||
private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel {
|
||||
private var askUserThread: Option[Thread] = None
|
||||
def makeAskUserThread(s: State): Thread = new Thread("ask-user-thread") {
|
||||
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 reader = new FullReader(history, s.combinedParser, JLine.HandleCONT, true)
|
||||
override def run(): Unit = {
|
||||
// This internally handles thread interruption and returns Some("")
|
||||
val line = reader.readLine(prompt)
|
||||
line match {
|
||||
case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name))))
|
||||
case None => append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name))))
|
||||
}
|
||||
askUserThread = None
|
||||
private[this] val askUserThread = new AtomicReference[AskUserThread]
|
||||
private[this] def getPrompt(s: State): String = s.get(shellPrompt) match {
|
||||
case Some(pf) => pf(s)
|
||||
case None =>
|
||||
def ansi(s: String): String = if (ConsoleAppender.formatEnabledInEnv) s"$s" else ""
|
||||
s"${ansi(ConsoleAppender.DeleteLine)}> ${ansi(ConsoleAppender.ClearScreenAfterCursor)}"
|
||||
}
|
||||
private[this] class AskUserThread(s: State) extends Thread("ask-user-thread") {
|
||||
private val history = s.get(historyPath).getOrElse(Some(new File(s.baseDir, ".history")))
|
||||
private val prompt = getPrompt(s)
|
||||
private val reader =
|
||||
new FullReader(
|
||||
history,
|
||||
s.combinedParser,
|
||||
LineReader.HandleCONT,
|
||||
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
|
||||
|
||||
|
|
@ -44,33 +67,24 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel
|
|||
def publishEventMessage(event: EventMessage): Unit =
|
||||
event match {
|
||||
case e: ConsolePromptEvent =>
|
||||
askUserThread match {
|
||||
case Some(_) =>
|
||||
case _ =>
|
||||
val x = makeAskUserThread(e.state)
|
||||
askUserThread = Some(x)
|
||||
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 _ =>
|
||||
if (Terminal.systemInIsAttached) {
|
||||
askUserThread.synchronized {
|
||||
askUserThread.get match {
|
||||
case null => askUserThread.set(makeAskUserThread(e.state))
|
||||
case t => t.redraw()
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
case _ => //
|
||||
}
|
||||
|
||||
def shutdown(): Unit =
|
||||
askUserThread match {
|
||||
case Some(x) if x.isAlive =>
|
||||
x.interrupt()
|
||||
askUserThread = None
|
||||
def shutdown(): Unit = askUserThread.synchronized {
|
||||
askUserThread.get match {
|
||||
case null =>
|
||||
case t if t.isAlive =>
|
||||
t.interrupt()
|
||||
askUserThread.set(null)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,19 +12,21 @@ package client
|
|||
import java.io.{ File, IOException }
|
||||
import java.util.UUID
|
||||
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.util.{ JLine, ConsoleAppender }
|
||||
import sbt.util.Level
|
||||
import sbt.io.syntax._
|
||||
import sbt.internal.protocol._
|
||||
import sbt.internal.util.{ ConsoleAppender, LineReader }
|
||||
import sbt.io.IO
|
||||
import sbt.io.syntax._
|
||||
import sbt.protocol._
|
||||
import sbt.util.Level
|
||||
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 =>
|
||||
private val channelName = new AtomicReference("_")
|
||||
private val status = new AtomicReference("Ready")
|
||||
|
|
@ -214,7 +216,7 @@ class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[Strin
|
|||
}
|
||||
|
||||
def shell(): Unit = {
|
||||
val reader = JLine.simple(None, JLine.HandleCONT, injectThreadSleep = true)
|
||||
val reader = LineReader.simple(None, LineReader.HandleCONT, injectThreadSleep = true)
|
||||
while (running.get) {
|
||||
reader.readLine("> ", None) match {
|
||||
case Some("shutdown") =>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
package sbt
|
||||
|
||||
import sbt.internal.util.{ JLine, SimpleReader }
|
||||
import sbt.internal.util.{ SimpleReader, Terminal }
|
||||
|
||||
trait CommandLineUIService extends InteractionService {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -716,7 +716,15 @@ object Defaults extends BuildCommon {
|
|||
forkOptions := forkOptionsTask.value,
|
||||
selectMainClass := mainClass.value orElse askForMainClass(discoveredMainClasses.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,
|
||||
run := foregroundRunTask.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] =
|
||||
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] =
|
||||
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 {
|
||||
case multiple if multiple.size > 1 =>
|
||||
logger.warn(
|
||||
"Multiple main classes detected. Run 'show discoveredMainClasses' to see the list"
|
||||
)
|
||||
case multiple if multiple.size > 1 && logWarning =>
|
||||
val msg =
|
||||
"multiple main classes detected: run 'show discoveredMainClasses' to see the list"
|
||||
logger.warn(msg)
|
||||
case _ =>
|
||||
}
|
||||
pickMainClass(classes)
|
||||
|
|
|
|||
|
|
@ -260,10 +260,7 @@ object EvaluateTask {
|
|||
ps.reset()
|
||||
ConsoleAppender.setShowProgress(true)
|
||||
val appender = MainAppender.defaultScreen(StandardMain.console)
|
||||
appender match {
|
||||
case c: ConsoleAppender => c.setProgressState(ps)
|
||||
case _ =>
|
||||
}
|
||||
ProgressState.set(ps)
|
||||
val log = LogManager.progressLogger(appender)
|
||||
Some(new TaskProgress(log))
|
||||
case _ => None
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ package sbt
|
|||
|
||||
import java.io.{ File, IOException }
|
||||
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.atomic.AtomicBoolean
|
||||
import java.util.{ Locale, Properties }
|
||||
|
||||
import sbt.BasicCommandStrings.{ Shell, TemplateCommand }
|
||||
import sbt.Project.LoadAction
|
||||
|
|
@ -23,7 +23,7 @@ import sbt.internal._
|
|||
import sbt.internal.inc.ScalaInstance
|
||||
import sbt.internal.util.Types.{ const, idFun }
|
||||
import sbt.internal.util._
|
||||
import sbt.internal.util.complete.{ SizeParser, Parser }
|
||||
import sbt.internal.util.complete.{ Parser, SizeParser }
|
||||
import sbt.io._
|
||||
import sbt.io.syntax._
|
||||
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.
|
||||
val clientModByEnv = SysProp.client
|
||||
val userCommands = configuration.arguments.map(_.trim)
|
||||
if (clientModByEnv || (userCommands.exists { cmd =>
|
||||
(cmd == DashClient) || (cmd == DashDashClient)
|
||||
})) {
|
||||
val args = userCommands.toList filterNot { cmd =>
|
||||
(cmd == DashClient) || (cmd == DashDashClient)
|
||||
val isClient: String => Boolean = cmd => (cmd == DashClient) || (cmd == DashDashClient)
|
||||
Terminal.withStreams {
|
||||
if (clientModByEnv || userCommands.exists(isClient)) {
|
||||
val args = userCommands.toList.filterNot(isClient)
|
||||
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 {
|
||||
ShutdownHooks.close()
|
||||
|
|
@ -764,22 +763,24 @@ object BuiltinCommands {
|
|||
|
||||
@tailrec
|
||||
private[this] def doLoadFailed(s: State, loadArg: String): State = {
|
||||
val result = (SimpleReader.readLine(
|
||||
"Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)"
|
||||
) getOrElse Quit)
|
||||
.toLowerCase(Locale.ENGLISH)
|
||||
def matches(s: String) = !result.isEmpty && (s startsWith result)
|
||||
def retry = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog
|
||||
def ignoreMsg =
|
||||
s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)")
|
||||
val result = Terminal.withRawSystemIn {
|
||||
Terminal.withEcho(toggle = true)(Terminal.wrappedSystemIn.read() match {
|
||||
case -1 => 'q'.toInt
|
||||
case b => b
|
||||
})
|
||||
}
|
||||
def retry: State = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog
|
||||
def ignoreMsg: String =
|
||||
if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded"
|
||||
|
||||
result match {
|
||||
case "" => retry
|
||||
case _ if matches("retry") => retry
|
||||
case _ if matches(Quit) => s.exit(ok = false)
|
||||
case _ if matches("ignore") => s.log.warn(s"Ignoring load failure: $ignoreMsg."); s
|
||||
case _ if matches("last") => LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s
|
||||
case _ => println("Invalid response."); doLoadFailed(s, loadArg)
|
||||
result.toChar match {
|
||||
case '\n' | '\r' => retry
|
||||
case 'r' | 'R' => retry
|
||||
case 'q' | 'Q' => s.exit(ok = false)
|
||||
case 'i' | 'I' => s.log.warn(s"Ignoring load failure: $ignoreMsg."); s
|
||||
case 'l' | 'L' => LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s
|
||||
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 =>
|
||||
import sbt.internal.{ ConsolePromptEvent, ConsoleUnpromptEvent }
|
||||
import sbt.internal.ConsolePromptEvent
|
||||
val exchange = StandardMain.exchange
|
||||
val welcomeState = displayWelcomeBanner(s0)
|
||||
val s1 = exchange run welcomeState
|
||||
|
|
@ -898,13 +899,15 @@ object BuiltinCommands {
|
|||
.getOpt(Keys.minForcegcInterval)
|
||||
.getOrElse(GCUtil.defaultMinForcegcInterval)
|
||||
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
|
||||
.copy(
|
||||
onFailure = Some(Exec(Shell, None)),
|
||||
remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands
|
||||
)
|
||||
.setInteractive(true)
|
||||
exchange publishEventMessage ConsoleUnpromptEvent(exec.source)
|
||||
if (exec.commandLine.trim.isEmpty) newState
|
||||
else newState.clearGlobalLog
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ package sbt
|
|||
import java.io.PrintWriter
|
||||
import java.util.Properties
|
||||
|
||||
import jline.TerminalFactory
|
||||
import sbt.internal.{ Aggregation, ShutdownHooks }
|
||||
import sbt.internal.langserver.ErrorCodes
|
||||
import sbt.internal.protocol.JsonRpcResponseError
|
||||
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.protocol._
|
||||
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
|
||||
// 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.
|
||||
val shutdownHook = ShutdownHooks.add(() => TerminalFactory.get().restore())
|
||||
val shutdownHook = ShutdownHooks.add(Terminal.restore)
|
||||
|
||||
try {
|
||||
runLoggedLoop(state, state.globalLogging.backing)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import sbt.internal.protocol.JsonRpcResponseError
|
|||
import sbt.internal.langserver.{ LogMessageParams, MessageType }
|
||||
import sbt.internal.server._
|
||||
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.{ Hash, IO }
|
||||
import sbt.protocol.{ EventMessage, ExecStatusEvent }
|
||||
|
|
@ -50,6 +50,7 @@ private[sbt] final class CommandExchange {
|
|||
private val channelBufferLock = new AnyRef {}
|
||||
private val commandChannelQueue = new LinkedBlockingQueue[CommandChannel]
|
||||
private val nextChannelId: AtomicInteger = new AtomicInteger(0)
|
||||
private[this] val activePrompt = new AtomicBoolean(false)
|
||||
private lazy val jsonFormat = new sjsonnew.BasicJsonProtocol with JValueFormats {}
|
||||
|
||||
def channels: List[CommandChannel] = channelBuffer.toList
|
||||
|
|
@ -83,7 +84,11 @@ private[sbt] final class CommandExchange {
|
|||
commandChannelQueue.poll(1, TimeUnit.SECONDS)
|
||||
slurpMessages()
|
||||
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 =>
|
||||
val newDeadline = if (deadline.fold(false)(_.isOverdue())) {
|
||||
GCUtil.forceGcWithInterval(interval, logger)
|
||||
|
|
@ -129,6 +134,7 @@ private[sbt] final class CommandExchange {
|
|||
|
||||
def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = {
|
||||
val name = newNetworkName
|
||||
if (needToFinishPromptLine()) ConsoleOut.systemOut.println("")
|
||||
s.log.info(s"new client connected: $name")
|
||||
val logger: Logger = {
|
||||
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.
|
||||
case entry: ConsolePromptEvent =>
|
||||
channels collect {
|
||||
case c: ConsoleChannel => c.publishEventMessage(entry)
|
||||
}
|
||||
case entry: ConsoleUnpromptEvent =>
|
||||
channels collect {
|
||||
case c: ConsoleChannel => c.publishEventMessage(entry)
|
||||
case c: ConsoleChannel =>
|
||||
c.publishEventMessage(entry)
|
||||
activePrompt.set(Terminal.systemInIsAttached)
|
||||
}
|
||||
case entry: ExecStatusEvent =>
|
||||
channels collect {
|
||||
|
|
@ -384,4 +388,5 @@ private[sbt] final class CommandExchange {
|
|||
|
||||
removeChannels(toDel.toList)
|
||||
}
|
||||
private[this] def needToFinishPromptLine(): Boolean = activePrompt.compareAndSet(true, false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package internal
|
|||
|
||||
import sbt.internal.classpath.AlternativeZincUtil
|
||||
import sbt.internal.inc.{ ScalaInstance, ZincLmUtil }
|
||||
import sbt.internal.util.JLine
|
||||
import sbt.internal.util.Terminal
|
||||
import sbt.util.Logger
|
||||
import xsbti.compile.ClasspathOptionsUtil
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ object ConsoleProject {
|
|||
val importString = imports.mkString("", ";\n", ";\n\n")
|
||||
val initCommands = importString + extra
|
||||
|
||||
JLine.usingTerminal { _ =>
|
||||
Terminal.withCanonicalIn {
|
||||
// TODO - Hook up dsl classpath correctly...
|
||||
(new Console(compiler))(
|
||||
unit.classpath,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import sbt.internal.io.WatchState
|
|||
import sbt.internal.nio._
|
||||
import sbt.internal.util.complete.Parser._
|
||||
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.Watch.{ Creation, Deletion, ShowOptions, Update }
|
||||
import sbt.nio.file.{ FileAttributes, Glob }
|
||||
|
|
@ -272,11 +272,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
f(s, valid, invalid)
|
||||
}
|
||||
|
||||
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = {
|
||||
val terminal = JLine.terminal
|
||||
terminal.init()
|
||||
terminal.setEchoEnabled(true)
|
||||
val wrapped = terminal.wrapInIfNeeded(System.in)
|
||||
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = Terminal.withRawSystemIn {
|
||||
val wrapped = Terminal.wrappedSystemIn
|
||||
if (Util.isNonCygwinWindows) {
|
||||
val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable {
|
||||
private[this] val buffer = new java.util.LinkedList[Int]
|
||||
|
|
@ -288,10 +285,13 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
override def run(): Unit = {
|
||||
try {
|
||||
if (!closed.get()) {
|
||||
buffer.add(wrapped.read())
|
||||
wrapped.read() match {
|
||||
case -1 => closed.set(true)
|
||||
case b => buffer.add(b)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case _: InterruptedException =>
|
||||
case _: InterruptedException => closed.set(true)
|
||||
}
|
||||
if (!closed.get()) run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package sbt
|
|||
package internal
|
||||
|
||||
import java.io.PrintWriter
|
||||
|
||||
import Def.ScopedKey
|
||||
import Scope.GlobalScope
|
||||
import Keys.{ logLevel, logManager, persistLogLevel, persistTraceLevel, sLog, traceLevel }
|
||||
|
|
@ -16,13 +17,14 @@ import sbt.internal.util.{
|
|||
AttributeKey,
|
||||
ConsoleAppender,
|
||||
ConsoleOut,
|
||||
MainAppender,
|
||||
ManagedLogger,
|
||||
ProgressState,
|
||||
Settings,
|
||||
SuppressedTraceContext,
|
||||
MainAppender
|
||||
SuppressedTraceContext
|
||||
}
|
||||
import MainAppender._
|
||||
import sbt.util.{ Level, Logger, LogExchange }
|
||||
import sbt.internal.util.ManagedLogger
|
||||
import sbt.util.{ Level, LogExchange, Logger }
|
||||
import org.apache.logging.log4j.core.Appender
|
||||
|
||||
sealed abstract class LogManager {
|
||||
|
|
@ -142,10 +144,7 @@ object LogManager {
|
|||
val extraBacked = state.globalLogging.backed :: relay :: Nil
|
||||
val ps = Project.extract(state).get(sbt.Keys.progressState in ThisBuild)
|
||||
val consoleOpt = consoleLocally(state, console)
|
||||
consoleOpt foreach {
|
||||
case a: ConsoleAppender => ps.foreach(a.setProgressState)
|
||||
case _ =>
|
||||
}
|
||||
ps.foreach(ProgressState.set)
|
||||
val config = MainAppender.MainAppenderConfig(
|
||||
consoleOpt,
|
||||
backed,
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import sbt.internal.util.{ JLine }
|
||||
import sbt.util.Show
|
||||
|
||||
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
|
||||
|
||||
object SettingGraph {
|
||||
|
|
@ -82,7 +82,7 @@ object Graph {
|
|||
// [info] |
|
||||
// [info] +-quux
|
||||
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
|
||||
def limitLine(s: String): String =
|
||||
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."
|
||||
|
|
|
|||
|
|
@ -90,11 +90,11 @@ object SysProp {
|
|||
|
||||
def fileCacheSize: Long =
|
||||
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 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 = {
|
||||
val coursierOpt = booleanOpt("sbt.coursier")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import sbt.internal.util._
|
|||
import sbt.util.Level
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* implements task progress display on the shell.
|
||||
|
|
@ -23,22 +24,24 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
|
|||
with ExecuteProgress[Task] {
|
||||
private[this] val lastTaskCount = new AtomicInteger(0)
|
||||
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
|
||||
extends Thread("task-progress-report-thread")
|
||||
with AutoCloseable {
|
||||
private[this] val isClosed = new AtomicBoolean(false)
|
||||
private[this] val firstTime = new AtomicBoolean(true)
|
||||
setDaemon(true)
|
||||
start()
|
||||
@tailrec override def run(): Unit = {
|
||||
if (!isClosed.get()) {
|
||||
try {
|
||||
report()
|
||||
Thread.sleep(sleepDuration)
|
||||
if (active.isEmpty) TaskProgress.this.stop()
|
||||
} catch {
|
||||
case _: InterruptedException =>
|
||||
}
|
||||
val duration =
|
||||
if (firstTime.compareAndSet(true, activeExceedingThreshold.nonEmpty)) threshold
|
||||
else sleepDuration
|
||||
Thread.sleep(duration.toMillis)
|
||||
} catch { case _: InterruptedException => isClosed.set(true) }
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
|
@ -65,9 +68,7 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
|
|||
|
||||
override def afterAllCompleted(results: RMap[Task, Result]): Unit = {
|
||||
// send an empty progress report to clear out the previous report
|
||||
val event = ProgressEvent("Info", Vector(), Some(lastTaskCount.get), None, None)
|
||||
import sbt.internal.util.codec.JsonProtocol._
|
||||
log.logEvent(Level.Info, event)
|
||||
appendProgress(ProgressEvent("Info", Vector(), Some(lastTaskCount.get), None, None))
|
||||
}
|
||||
private[this] val skipReportTasks =
|
||||
Set(
|
||||
|
|
@ -93,41 +94,40 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
|
|||
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 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 = {
|
||||
val currentTasks = active
|
||||
val currentTasks = activeExceedingThreshold
|
||||
val ltc = lastTaskCount.get
|
||||
val currentTasksCount = currentTasks.size
|
||||
def report0(tasks: Vector[Task[_]]): Unit = {
|
||||
if (tasks.nonEmpty) maybeStartThread()
|
||||
val event = ProgressEvent(
|
||||
"Info",
|
||||
tasks
|
||||
.map { task =>
|
||||
val elapsed = timings.get(task).currentElapsedMicros
|
||||
ProgressItem(taskName(task), elapsed)
|
||||
}
|
||||
.sortBy(_.name),
|
||||
Some(ltc),
|
||||
None,
|
||||
None
|
||||
)
|
||||
import sbt.internal.util.codec.JsonProtocol._
|
||||
log.logEvent(Level.Info, event)
|
||||
}
|
||||
if (containsSkipTasks(currentTasks)) {
|
||||
def event(tasks: Vector[(Task[_], Long)]): ProgressEvent = ProgressEvent(
|
||||
"Info",
|
||||
tasks
|
||||
.map { case (task, elapsed) => ProgressItem(taskName(task), elapsed) }
|
||||
.sortBy(_.elapsedMicros),
|
||||
Some(ltc),
|
||||
None,
|
||||
None
|
||||
)
|
||||
if (active.nonEmpty) maybeStartThread()
|
||||
if (containsSkipTasks(active)) {
|
||||
if (ltc > 0) {
|
||||
lastTaskCount.set(0)
|
||||
report0(Vector.empty)
|
||||
appendProgress(event(Vector.empty))
|
||||
}
|
||||
} else {
|
||||
lastTaskCount.set(currentTasksCount)
|
||||
report0(currentTasks)
|
||||
appendProgress(event(currentTasks))
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def containsSkipTasks(tasks: Vector[Task[_]]): Boolean =
|
||||
tasks
|
||||
.map(t => taskName(t))
|
||||
.exists(n => skipReportTasks.exists(m => m == n || n.endsWith("/ " + m)))
|
||||
tasks.map(taskName).exists(n => skipReportTasks.exists(m => m == n || n.endsWith("/ " + m)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package sbt
|
||||
|
||||
import sbt.internal.util.ConsoleAppender
|
||||
import sbt.internal.util.Util.{ AnyOps, none }
|
||||
|
||||
object SelectMainClass {
|
||||
|
|
@ -19,12 +20,14 @@ object SelectMainClass {
|
|||
case Nil => None
|
||||
case head :: Nil => Some(head)
|
||||
case multiple =>
|
||||
promptIfMultipleChoices flatMap { prompt =>
|
||||
println("\nMultiple main classes detected, select one to run:\n")
|
||||
for ((className, index) <- multiple.zipWithIndex)
|
||||
println(" [" + (index + 1) + "] " + className)
|
||||
promptIfMultipleChoices.flatMap { prompt =>
|
||||
val header = "\nMultiple main classes detected. Select one to run:\n"
|
||||
val classes = multiple.zipWithIndex
|
||||
.map { case (className, index) => s" [${index + 1}] $className" }
|
||||
.mkString("\n")
|
||||
println(ConsoleAppender.ClearScreenAfterCursor + header + classes)
|
||||
|
||||
val line = trim(prompt("\nEnter number: "))
|
||||
println("")
|
||||
toInt(line, multiple.length) map multiple.apply
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue