mirror of https://github.com/sbt/sbt.git
Make Terminal a trait to support multiple clients
In order to support a multi-client sbt server ux, we need to factor `Terminal` out into a class instead of a singleton. Each terminal provides and outputstream and inputstream. In all of the places where we were previously relying on the `Terminal` singleton we need to update the code to use `Terminal.get`, which will redirect io to the terminal whose command is currently running. This commit does not implement the server side ui for network clients. It is just preparatory work for the multi-client ui. The Terminal implementations have thread safe access to the output stream. For this reason, I had to remove the sychronization on the ConsoleOut lockObject. There were code paths that led to deadlock when synchronizing on the lockObject.
This commit is contained in:
parent
120e6eb63d
commit
1b03c9b1a9
|
|
@ -18,7 +18,6 @@ import scala.concurrent.duration._
|
|||
|
||||
trait LineReader {
|
||||
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
|
||||
def redraw(): Unit = ()
|
||||
}
|
||||
|
||||
object LineReader {
|
||||
|
|
@ -26,8 +25,12 @@ object LineReader {
|
|||
!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)
|
||||
def createReader(
|
||||
historyPath: Option[File],
|
||||
terminal: Terminal,
|
||||
prompt: Prompt = Prompt.Running,
|
||||
): ConsoleReader = {
|
||||
val cr = Terminal.createReader(terminal, prompt)
|
||||
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
|
||||
cr.setBellEnabled(false)
|
||||
val h = historyPath match {
|
||||
|
|
@ -36,9 +39,11 @@ object LineReader {
|
|||
}
|
||||
h.setMaxSize(MaxHistorySize)
|
||||
cr.setHistory(h)
|
||||
cr.setHistoryEnabled(true)
|
||||
cr
|
||||
}
|
||||
|
||||
def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal)
|
||||
def simple(
|
||||
historyPath: Option[File],
|
||||
handleCONT: Boolean = HandleCONT,
|
||||
|
|
@ -56,18 +61,13 @@ abstract class JLine extends LineReader {
|
|||
|
||||
override def readLine(prompt: String, mask: Option[Char] = None): Option[String] =
|
||||
try {
|
||||
Terminal.withRawSystemIn(unsynchronizedReadLine(prompt, mask))
|
||||
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
|
||||
|
|
@ -144,16 +144,19 @@ private[sbt] object JLine {
|
|||
* 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")
|
||||
@deprecated(
|
||||
"Don't use jline.Terminal directly. Use Terminal.get.withCanonicalIn instead.",
|
||||
"1.4.0"
|
||||
)
|
||||
def usingTerminal[T](f: jline.Terminal => T): T =
|
||||
Terminal.withCanonicalIn(f(Terminal.deprecatedTeminal))
|
||||
Terminal.get.withCanonicalIn(f(Terminal.get.toJLine))
|
||||
|
||||
@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)
|
||||
val cr = Terminal.createReader(Terminal.console, Prompt.Running)
|
||||
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
|
||||
cr.setBellEnabled(false)
|
||||
val h = historyPath match {
|
||||
|
|
@ -165,8 +168,8 @@ private[sbt] object JLine {
|
|||
cr
|
||||
}
|
||||
|
||||
@deprecated("Avoid referencing JLine directly. Use Terminal.withRawSystemIn instead.", "1.4.0")
|
||||
def withJLine[T](action: => T): T = Terminal.withRawSystemIn(action)
|
||||
@deprecated("Avoid referencing JLine directly.", "1.4.0")
|
||||
def withJLine[T](action: => T): T = Terminal.get.withRawSystemIn(action)
|
||||
|
||||
@deprecated("Use LineReader.simple instead", "1.4.0")
|
||||
def simple(
|
||||
|
|
@ -211,7 +214,7 @@ final class FullReader(
|
|||
historyPath: Option[File],
|
||||
complete: Parser[_],
|
||||
val handleCONT: Boolean,
|
||||
inputStream: InputStream,
|
||||
terminal: Terminal
|
||||
) extends JLine {
|
||||
@deprecated("Use the constructor with no injectThreadSleep parameter", "1.4.0")
|
||||
def this(
|
||||
|
|
@ -219,9 +222,15 @@ final class FullReader(
|
|||
complete: Parser[_],
|
||||
handleCONT: Boolean = LineReader.HandleCONT,
|
||||
injectThreadSleep: Boolean = false
|
||||
) = this(historyPath, complete, handleCONT, JLine.makeInputStream(injectThreadSleep))
|
||||
) =
|
||||
this(
|
||||
historyPath,
|
||||
complete,
|
||||
handleCONT,
|
||||
Terminal.console
|
||||
)
|
||||
protected[this] val reader: ConsoleReader = {
|
||||
val cr = LineReader.createReader(historyPath, inputStream)
|
||||
val cr = LineReader.createReader(historyPath, terminal)
|
||||
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
|
||||
cr
|
||||
}
|
||||
|
|
@ -230,12 +239,15 @@ final class FullReader(
|
|||
class SimpleReader private[sbt] (
|
||||
historyPath: Option[File],
|
||||
val handleCONT: Boolean,
|
||||
inputStream: InputStream
|
||||
terminal: Terminal
|
||||
) extends JLine {
|
||||
def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
|
||||
this(historyPath, handleCONT, Terminal.wrappedSystemIn)
|
||||
this(historyPath, handleCONT, Terminal.console)
|
||||
protected[this] val reader: ConsoleReader =
|
||||
LineReader.createReader(historyPath, inputStream)
|
||||
LineReader.createReader(historyPath, terminal)
|
||||
}
|
||||
|
||||
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false)
|
||||
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) {
|
||||
def apply(terminal: Terminal): SimpleReader =
|
||||
new SimpleReader(None, LineReader.HandleCONT, terminal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,13 +100,11 @@ class ConsoleLogger private[ConsoleLogger] (
|
|||
override def trace(t: => Throwable): Unit =
|
||||
appender.trace(t, getTrace)
|
||||
|
||||
override def logAll(events: Seq[LogEvent]) =
|
||||
out.lockObject.synchronized { events.foreach(log) }
|
||||
override def logAll(events: Seq[LogEvent]) = events.foreach(log)
|
||||
}
|
||||
|
||||
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"
|
||||
|
|
@ -116,9 +114,27 @@ object ConsoleAppender {
|
|||
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 ClearPromptLine = CursorLeft1000 + ClearScreenAfterCursor
|
||||
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
|
||||
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
|
||||
def showProgress: Boolean = showProgressHolder.get
|
||||
private[ConsoleAppender] trait Properties {
|
||||
def isAnsiSupported: Boolean
|
||||
def isColorEnabled: Boolean
|
||||
def out: ConsoleOut
|
||||
}
|
||||
object Properties {
|
||||
def from(terminal: Terminal): Properties = new Properties {
|
||||
override def isAnsiSupported: Boolean = terminal.isAnsiSupported
|
||||
override def isColorEnabled: Boolean = terminal.isColorEnabled
|
||||
override def out = ConsoleOut.terminalOut(terminal)
|
||||
}
|
||||
def from(o: ConsoleOut, ansi: Boolean, color: Boolean): Properties = new Properties {
|
||||
override def isAnsiSupported: Boolean = ansi
|
||||
override def isColorEnabled: Boolean = color
|
||||
override def out = o
|
||||
}
|
||||
}
|
||||
|
||||
/** Hide stack trace altogether. */
|
||||
val noSuppressedMessage = (_: SuppressedTraceContext) => None
|
||||
|
|
@ -130,7 +146,7 @@ object ConsoleAppender {
|
|||
* 3. -Dsbt.colour=always/auto/never/true/false
|
||||
* 4. -Dsbt.log.format=always/auto/never/true/false
|
||||
*/
|
||||
val formatEnabledInEnv: Boolean = {
|
||||
lazy val formatEnabledInEnv: Boolean = {
|
||||
def useColorDefault: Boolean = {
|
||||
// This approximates that both stdin and stdio are connected,
|
||||
// so by default color will be turned off for pipes and redirects.
|
||||
|
|
@ -239,7 +255,38 @@ object ConsoleAppender {
|
|||
* @return A new `ConsoleAppender` that writes to `out`.
|
||||
*/
|
||||
def apply(name: String, out: ConsoleOut, useFormat: Boolean): ConsoleAppender =
|
||||
apply(name, out, formatEnabledInEnv, useFormat, noSuppressedMessage)
|
||||
apply(name, out, useFormat || formatEnabledInEnv, useFormat, noSuppressedMessage)
|
||||
|
||||
/**
|
||||
* A new `ConsoleAppender` identified by `name`, and that writes to `out`.
|
||||
*
|
||||
* @param name An identifier for the `ConsoleAppender`.
|
||||
* @param terminal The terminal to which this appender corresponds
|
||||
* @return A new `ConsoleAppender` that writes to `out`.
|
||||
*/
|
||||
def apply(name: String, terminal: Terminal): ConsoleAppender = {
|
||||
val appender = new ConsoleAppender(name, Properties.from(terminal), noSuppressedMessage)
|
||||
appender.start()
|
||||
appender
|
||||
}
|
||||
|
||||
/**
|
||||
* A new `ConsoleAppender` identified by `name`, and that writes to `out`.
|
||||
*
|
||||
* @param name An identifier for the `ConsoleAppender`.
|
||||
* @param terminal The terminal to which this appender corresponds
|
||||
* @param suppressedMessage How to handle stack traces.
|
||||
* @return A new `ConsoleAppender` that writes to `out`.
|
||||
*/
|
||||
def apply(
|
||||
name: String,
|
||||
terminal: Terminal,
|
||||
suppressedMessage: SuppressedTraceContext => Option[String]
|
||||
): ConsoleAppender = {
|
||||
val appender = new ConsoleAppender(name, Properties.from(terminal), suppressedMessage)
|
||||
appender.start()
|
||||
appender
|
||||
}
|
||||
|
||||
/**
|
||||
* A new `ConsoleAppender` identified by `name`, and that writes to `out`.
|
||||
|
|
@ -296,7 +343,7 @@ object ConsoleAppender {
|
|||
|
||||
private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
|
||||
|
||||
private[this] def ansiSupported: Boolean = Terminal.isAnsiSupported
|
||||
private[this] def ansiSupported: Boolean = Terminal.console.isAnsiSupported
|
||||
}
|
||||
|
||||
// See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2
|
||||
|
|
@ -312,14 +359,23 @@ object ConsoleAppender {
|
|||
*/
|
||||
class ConsoleAppender private[ConsoleAppender] (
|
||||
name: String,
|
||||
out: ConsoleOut,
|
||||
ansiCodesSupported: Boolean,
|
||||
useFormat: Boolean,
|
||||
properties: Properties,
|
||||
suppressedMessage: SuppressedTraceContext => Option[String]
|
||||
) extends AbstractAppender(name, null, LogExchange.dummyLayout, true, Array.empty) {
|
||||
def this(
|
||||
name: String,
|
||||
out: ConsoleOut,
|
||||
ansiCodesSupported: Boolean,
|
||||
useFormat: Boolean,
|
||||
suppressedMessage: SuppressedTraceContext => Option[String]
|
||||
) = this(name, Properties.from(out, ansiCodesSupported, useFormat), suppressedMessage)
|
||||
import scala.Console.{ BLUE, GREEN, RED, YELLOW }
|
||||
|
||||
private val reset: String = {
|
||||
private[util] def out: ConsoleOut = properties.out
|
||||
private[util] def ansiCodesSupported: Boolean = properties.isAnsiSupported
|
||||
private[util] def useFormat: Boolean = properties.isColorEnabled
|
||||
|
||||
private def reset: String = {
|
||||
if (ansiCodesSupported && useFormat) scala.Console.RESET
|
||||
else ""
|
||||
}
|
||||
|
|
@ -352,16 +408,15 @@ class ConsoleAppender private[ConsoleAppender] (
|
|||
* @param t The `Throwable` whose stack trace to log.
|
||||
* @param traceLevel How to shorten the stack trace.
|
||||
*/
|
||||
def trace(t: => Throwable, traceLevel: Int): Unit =
|
||||
out.lockObject.synchronized {
|
||||
if (traceLevel >= 0)
|
||||
write(StackTrace.trimmed(t, traceLevel))
|
||||
if (traceLevel <= 2) {
|
||||
val ctx = new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat)
|
||||
for (msg <- suppressedMessage(ctx))
|
||||
appendLog(NO_COLOR, "trace", NO_COLOR, msg)
|
||||
}
|
||||
def trace(t: => Throwable, traceLevel: Int): Unit = {
|
||||
if (traceLevel >= 0)
|
||||
write(StackTrace.trimmed(t, traceLevel))
|
||||
if (traceLevel <= 2) {
|
||||
val ctx = new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat)
|
||||
for (msg <- suppressedMessage(ctx))
|
||||
appendLog(NO_COLOR, "trace", NO_COLOR, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a `ControlEvent` to the log.
|
||||
|
|
@ -382,18 +437,6 @@ class ConsoleAppender private[ConsoleAppender] (
|
|||
appendLog(labelColor(level), level.toString, NO_COLOR, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats `msg` with `format, wrapped between `RESET`s
|
||||
*
|
||||
* @param format The format to use
|
||||
* @param msg The message to format
|
||||
* @return The formatted message.
|
||||
*/
|
||||
private def formatted(format: String, msg: String): String = {
|
||||
val builder = new java.lang.StringBuilder(reset.length * 2 + format.length + msg.length)
|
||||
builder.append(reset).append(format).append(msg).append(reset).toString
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the right color for the label given `level`.
|
||||
*
|
||||
|
|
@ -424,22 +467,24 @@ class ConsoleAppender private[ConsoleAppender] (
|
|||
messageColor: String,
|
||||
message: String
|
||||
): Unit =
|
||||
out.lockObject.synchronized {
|
||||
val builder: StringBuilder =
|
||||
new StringBuilder(labelColor.length + label.length + messageColor.length + reset.length * 3)
|
||||
try {
|
||||
val len =
|
||||
labelColor.length + label.length + messageColor.length + reset.length * 3 + ClearScreenAfterCursor.length
|
||||
val builder: StringBuilder = new StringBuilder(len)
|
||||
message.linesIterator.foreach { line =>
|
||||
builder.ensureCapacity(
|
||||
labelColor.length + label.length + messageColor.length + line.length + reset.length * 3 + 3
|
||||
)
|
||||
builder.ensureCapacity(len + line.length + 4)
|
||||
builder.setLength(0)
|
||||
|
||||
def fmted(a: String, b: String) = builder.append(reset).append(a).append(b).append(reset)
|
||||
|
||||
builder.append(reset).append('[')
|
||||
fmted(labelColor, label)
|
||||
builder.append("] ")
|
||||
fmted(messageColor, line)
|
||||
builder.append(ClearScreenAfterCursor)
|
||||
write(builder.toString)
|
||||
}
|
||||
}
|
||||
} catch { case _: InterruptedException => }
|
||||
|
||||
// success is called by ConsoleLogger.
|
||||
private[sbt] def success(message: => String): Unit = {
|
||||
|
|
@ -449,7 +494,11 @@ class ConsoleAppender private[ConsoleAppender] (
|
|||
private def write(msg: String): Unit = {
|
||||
val toWrite =
|
||||
if (!useFormat || !ansiCodesSupported) EscHelpers.removeEscapeSequences(msg) else msg
|
||||
out.println(toWrite)
|
||||
/*
|
||||
* Use print + flush rather than println to prevent log lines from getting interleaved.
|
||||
*/
|
||||
out.print(toWrite + "\n")
|
||||
out.flush()
|
||||
}
|
||||
|
||||
private def appendMessage(level: Level.Value, msg: Message): Unit =
|
||||
|
|
@ -519,45 +568,70 @@ private[sbt] final class ProgressState(
|
|||
new AtomicReference(Nil),
|
||||
new AtomicInteger(0),
|
||||
blankZone,
|
||||
new AtomicReference(new ArrayBuffer[Byte])
|
||||
new AtomicReference(new ArrayBuffer[Byte]),
|
||||
)
|
||||
def reset(): Unit = {
|
||||
progressLines.set(Nil)
|
||||
padding.set(0)
|
||||
currentLineBytes.set(new ArrayBuffer[Byte])
|
||||
}
|
||||
private[util] def clearBytes(): Unit = {
|
||||
val pad = padding.get
|
||||
if (currentLineBytes.get.isEmpty && pad > 0) padding.decrementAndGet()
|
||||
currentLineBytes.set(new ArrayBuffer[Byte])
|
||||
}
|
||||
|
||||
private[util] def addBytes(terminal: Terminal, bytes: ArrayBuffer[Byte]): Unit = {
|
||||
val previous = currentLineBytes.get
|
||||
val padding = this.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
|
||||
this.padding.set(math.max(padding - diff, 0))
|
||||
}
|
||||
}
|
||||
|
||||
private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit =
|
||||
if (terminal.prompt != Prompt.Running && terminal.prompt != Prompt.Batch) {
|
||||
val prefix = if (terminal.isAnsiSupported) s"$DeleteLine$CursorLeft1000" else ""
|
||||
val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes
|
||||
pmpt.foreach(b => printStream.write(b & 0xFF))
|
||||
}
|
||||
private[util] def reprint(terminal: Terminal, printStream: PrintStream): Unit = {
|
||||
printPrompt(terminal, printStream)
|
||||
if (progressLines.get.nonEmpty) {
|
||||
val lines = printProgress(terminal, terminal.getLastLine.getOrElse(""))
|
||||
printStream.print(ClearScreenAfterCursor + lines)
|
||||
}
|
||||
}
|
||||
|
||||
private[util] def printProgress(
|
||||
terminal: Terminal,
|
||||
lastLine: String
|
||||
): String = {
|
||||
val previousLines = progressLines.get
|
||||
if (previousLines.nonEmpty) {
|
||||
val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
|
||||
val (height, width) = terminal.getLineHeightAndWidth(lastLine)
|
||||
val left = cursorLeft(1000) // resets the position to the left
|
||||
val offset = width > 0
|
||||
val pad = math.max(padding.get - height, 0)
|
||||
val start = (if (offset) "\n" else "")
|
||||
val totalSize = currentLength + 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 resetCursor = resetCursorUp + left + lastLine
|
||||
start + blank + lines + resetCursor
|
||||
} else {
|
||||
ClearScreenAfterCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -566,51 +640,53 @@ private[sbt] object ProgressState {
|
|||
* at the info or greater level, we can decrement the padding because the console
|
||||
* line will have filled in the blank line.
|
||||
*/
|
||||
private[sbt] 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"
|
||||
private[sbt] def updateProgressState(
|
||||
pe: ProgressEvent,
|
||||
terminal: Terminal
|
||||
): Unit = {
|
||||
val state = terminal.progressState
|
||||
val isRunning = terminal.prompt == Prompt.Running
|
||||
val isBatch = terminal.prompt == Prompt.Batch
|
||||
val isWatch = terminal.prompt == Prompt.Watch
|
||||
if (terminal.isSupershellEnabled) {
|
||||
if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) {
|
||||
terminal.withPrintStream { ps =>
|
||||
val info = if (isRunning || isBatch && pe.channelName.fold(true)(_ == terminal.name)) {
|
||||
pe.items.map { item =>
|
||||
val elapsed = item.elapsedMicros / 1000000L
|
||||
s" | => ${item.name} ${elapsed}s"
|
||||
}
|
||||
} else {
|
||||
pe.command.toSeq.flatMap { cmd =>
|
||||
s"sbt server is running '$cmd'" :: tail
|
||||
}
|
||||
}
|
||||
|
||||
val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_))
|
||||
val previousLines = state.progressLines.getAndSet(info)
|
||||
if (previousLines != info) {
|
||||
val prevLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
|
||||
val lastLine = terminal.prompt match {
|
||||
case Prompt.Running | Prompt.Batch => terminal.getLastLine.getOrElse("")
|
||||
case a => a.render()
|
||||
}
|
||||
val prevSize = prevLength + state.padding.get
|
||||
|
||||
val newPadding = math.max(0, prevSize - currentLength)
|
||||
state.padding.set(newPadding)
|
||||
state.printPrompt(terminal, ps)
|
||||
ps.print(state.printProgress(terminal, lastLine))
|
||||
ps.flush()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
} else if (state.progressLines.get.nonEmpty) {
|
||||
state.progressLines.set(Nil)
|
||||
terminal.withPrintStream { ps =>
|
||||
val lastLine = terminal.getLastLine.getOrElse("")
|
||||
ps.print(lastLine + ClearScreenAfterCursor)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
package sbt.internal.util
|
||||
|
||||
import java.io.{ BufferedWriter, PrintStream, PrintWriter }
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
sealed trait ConsoleOut {
|
||||
val lockObject: AnyRef
|
||||
|
|
@ -18,7 +20,20 @@ sealed trait ConsoleOut {
|
|||
}
|
||||
|
||||
object ConsoleOut {
|
||||
def systemOut: ConsoleOut = printStreamOut(System.out)
|
||||
def systemOut: ConsoleOut = terminalOut
|
||||
private[sbt] def globalProxy: ConsoleOut = Proxy
|
||||
private[sbt] def setGlobalProxy(out: ConsoleOut): Unit = Proxy.set(out)
|
||||
private[sbt] def getGlobalProxy: ConsoleOut = Proxy.proxy.get
|
||||
private object Proxy extends ConsoleOut {
|
||||
private[ConsoleOut] val proxy = new AtomicReference[ConsoleOut](systemOut)
|
||||
private[this] def get: ConsoleOut = proxy.get
|
||||
def set(proxy: ConsoleOut): Unit = this.proxy.set(proxy)
|
||||
override val lockObject: AnyRef = proxy
|
||||
override def print(s: String): Unit = get.print(s)
|
||||
override def println(s: String): Unit = get.println(s)
|
||||
override def println(): Unit = get.println()
|
||||
override def flush(): Unit = get.flush()
|
||||
}
|
||||
|
||||
def overwriteContaining(s: String): (String, String) => Boolean =
|
||||
(cur, prev) => cur.contains(s) && prev.contains(s)
|
||||
|
|
@ -57,6 +72,28 @@ object ConsoleOut {
|
|||
}
|
||||
}
|
||||
|
||||
def terminalOut: ConsoleOut = new ConsoleOut {
|
||||
override val lockObject: AnyRef = System.out
|
||||
override def print(s: String): Unit = Terminal.get.printStream.print(s)
|
||||
override def println(s: String): Unit = Terminal.get.printStream.println(s)
|
||||
override def println(): Unit = Terminal.get.printStream.println()
|
||||
override def flush(): Unit = Terminal.get.printStream.flush()
|
||||
}
|
||||
|
||||
private[this] val consoleOutPerTerminal = new ConcurrentHashMap[Terminal, ConsoleOut]
|
||||
def terminalOut(terminal: Terminal): ConsoleOut = consoleOutPerTerminal.get(terminal) match {
|
||||
case null =>
|
||||
val res = new ConsoleOut {
|
||||
override val lockObject: AnyRef = terminal
|
||||
override def print(s: String): Unit = terminal.printStream.print(s)
|
||||
override def println(s: String): Unit = terminal.printStream.println(s)
|
||||
override def println(): Unit = terminal.printStream.println()
|
||||
override def flush(): Unit = terminal.printStream.flush()
|
||||
}
|
||||
consoleOutPerTerminal.put(terminal, res)
|
||||
res
|
||||
case c => c
|
||||
}
|
||||
def printStreamOut(out: PrintStream): ConsoleOut = new ConsoleOut {
|
||||
val lockObject = out
|
||||
def print(s: String) = out.print(s)
|
||||
|
|
|
|||
|
|
@ -75,8 +75,13 @@ object MainAppender {
|
|||
def defaultScreen(
|
||||
console: ConsoleOut,
|
||||
suppressedMessage: SuppressedTraceContext => Option[String]
|
||||
): Appender =
|
||||
ConsoleAppender(ConsoleAppender.generateName, console, suppressedMessage = suppressedMessage)
|
||||
): Appender = {
|
||||
ConsoleAppender(
|
||||
ConsoleAppender.generateName,
|
||||
Terminal.get,
|
||||
suppressedMessage = suppressedMessage
|
||||
)
|
||||
}
|
||||
|
||||
def defaultScreen(
|
||||
name: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.OutputStream
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
private[sbt] sealed trait Prompt {
|
||||
def mkPrompt: () => String
|
||||
def render(): String
|
||||
def wrappedOutputStream(terminal: Terminal): OutputStream
|
||||
}
|
||||
|
||||
private[sbt] object Prompt {
|
||||
private[sbt] case class AskUser(override val mkPrompt: () => String) extends Prompt {
|
||||
private[this] val bytes = new LinkedBlockingQueue[Int]
|
||||
override def wrappedOutputStream(terminal: Terminal): OutputStream = new OutputStream {
|
||||
override def write(b: Int): Unit = {
|
||||
if (b == 10) bytes.clear()
|
||||
else bytes.put(b)
|
||||
terminal.withPrintStream { p =>
|
||||
p.write(b)
|
||||
p.flush()
|
||||
}
|
||||
}
|
||||
override def flush(): Unit = terminal.withPrintStream(_.flush())
|
||||
}
|
||||
|
||||
override def render(): String =
|
||||
EscHelpers.stripMoves(new String(bytes.asScala.toArray.map(_.toByte)))
|
||||
}
|
||||
private[sbt] trait NoPrompt extends Prompt {
|
||||
override val mkPrompt: () => String = () => ""
|
||||
override def render(): String = ""
|
||||
override def wrappedOutputStream(terminal: Terminal): OutputStream = terminal.outputStream
|
||||
}
|
||||
private[sbt] case object Running extends NoPrompt
|
||||
private[sbt] case object Batch extends NoPrompt
|
||||
private[sbt] case object Watch extends NoPrompt
|
||||
}
|
||||
|
|
@ -10,17 +10,17 @@ 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 java.util.concurrent.{ ArrayBlockingQueue, CountDownLatch, Executors, LinkedBlockingQueue }
|
||||
|
||||
import jline.DefaultTerminal2
|
||||
import jline.console.ConsoleReader
|
||||
import sbt.internal.util.ConsoleAppender.{ ClearScreenAfterCursor, CursorLeft1000 }
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
object Terminal {
|
||||
trait Terminal extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Gets the current width of the terminal. The implementation reads a property from the jline
|
||||
|
|
@ -29,7 +29,7 @@ object Terminal {
|
|||
*
|
||||
* @return the terminal width.
|
||||
*/
|
||||
def getWidth: Int = terminal.getWidth
|
||||
def getWidth: Int
|
||||
|
||||
/**
|
||||
* Gets the current height of the terminal. The implementation reads a property from the jline
|
||||
|
|
@ -38,7 +38,7 @@ object Terminal {
|
|||
*
|
||||
* @return the terminal height.
|
||||
*/
|
||||
def getHeight: Int = terminal.getHeight
|
||||
def getHeight: Int
|
||||
|
||||
/**
|
||||
* Returns the height and width of the current line that is displayed on the terminal. If the
|
||||
|
|
@ -46,14 +46,83 @@ object Terminal {
|
|||
*
|
||||
* @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))
|
||||
}
|
||||
def getLineHeightAndWidth(line: String): (Int, Int)
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* Gets the input stream for this Terminal. This could be a wrapper around System.in for the
|
||||
* process or it could be a remote input stream for a network channel.
|
||||
* @return the input stream.
|
||||
*/
|
||||
def inputStream: InputStream
|
||||
|
||||
/**
|
||||
* Gets the input stream for this Terminal. This could be a wrapper around System.in for the
|
||||
* process or it could be a remote input stream for a network channel.
|
||||
* @return the input stream.
|
||||
*/
|
||||
def outputStream: OutputStream
|
||||
|
||||
/**
|
||||
* Returns true if the terminal supports ansi characters.
|
||||
*
|
||||
* @return true if the terminal supports ansi escape codes.
|
||||
*/
|
||||
def isAnsiSupported: Boolean
|
||||
|
||||
/**
|
||||
* Returns true if color is enabled for this terminal.
|
||||
*
|
||||
* @return true if color is enabled for this terminal.
|
||||
*/
|
||||
def isColorEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Returns true if the terminal has echo enabled.
|
||||
*
|
||||
* @return true if the terminal has echo enabled.
|
||||
*/
|
||||
def isEchoEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Returns true if the terminal has success enabled, which it may not if it is for batch
|
||||
* commands because the client will print the success results when received from the
|
||||
* server.
|
||||
*
|
||||
* @return true if the terminal has success enabled
|
||||
*/
|
||||
def isSuccessEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Returns true if the terminal has supershell enabled.
|
||||
*
|
||||
* @return true if the terminal has supershell enabled.
|
||||
*/
|
||||
def isSupershellEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Returns the last line written to the terminal's output stream.
|
||||
* @return the last line
|
||||
*/
|
||||
def getLastLine: Option[String]
|
||||
|
||||
def getBooleanCapability(capability: String): Boolean
|
||||
def getNumericCapability(capability: String): Int
|
||||
def getStringCapability(capability: String): String
|
||||
|
||||
private[sbt] def name: String
|
||||
private[sbt] def withRawSystemIn[T](f: => T): T = f
|
||||
private[sbt] def withCanonicalIn[T](f: => T): T = f
|
||||
private[sbt] def write(bytes: Int*): Unit
|
||||
private[sbt] def printStream: PrintStream
|
||||
private[sbt] def withPrintStream[T](f: PrintStream => T): T
|
||||
private[sbt] def restore(): Unit = {}
|
||||
private[sbt] val progressState = new ProgressState(1)
|
||||
private[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Running)
|
||||
private[sbt] final def prompt: Prompt = promptHolder.get
|
||||
private[sbt] final def setPrompt(newPrompt: Prompt): Unit = promptHolder.set(newPrompt)
|
||||
|
||||
/**
|
||||
* Returns the number of lines that the input string will cover given the current width of the
|
||||
|
|
@ -62,9 +131,9 @@ object Terminal {
|
|||
* @param line the input line
|
||||
* @return the number of lines that the line will cover on the terminal
|
||||
*/
|
||||
def lineCount(line: String): Int = {
|
||||
private[sbt] def lineCount(line: String): Int = {
|
||||
val lines = EscHelpers.stripColorsAndMoves(line).split('\n')
|
||||
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
|
||||
|
|
@ -72,14 +141,58 @@ object Terminal {
|
|||
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 }
|
||||
}
|
||||
|
||||
object Terminal {
|
||||
def consoleLog(string: String): Unit = {
|
||||
Terminal.console.printStream.println(s"[info] $string")
|
||||
}
|
||||
private[sbt] def set(terminal: Terminal) = {
|
||||
currentTerminal.set(terminal)
|
||||
jline.TerminalFactory.set(terminal.toJLine)
|
||||
}
|
||||
implicit class TerminalOps(private val term: Terminal) extends AnyVal {
|
||||
def ansi(richString: => String, string: => String): String =
|
||||
if (term.isAnsiSupported) richString else string
|
||||
private[sbt] def toJLine: jline.Terminal with jline.Terminal2 = term match {
|
||||
case t: ConsoleTerminal => t.term
|
||||
case _ =>
|
||||
new jline.Terminal with jline.Terminal2 {
|
||||
override def init(): Unit = {}
|
||||
override def restore(): Unit = {}
|
||||
override def reset(): Unit = {}
|
||||
override def isSupported: Boolean = true
|
||||
override def getWidth: Int = term.getWidth
|
||||
override def getHeight: Int = term.getHeight
|
||||
override def isAnsiSupported: Boolean = term.isAnsiSupported
|
||||
override def wrapOutIfNeeded(out: OutputStream): OutputStream = out
|
||||
override def wrapInIfNeeded(in: InputStream): InputStream = in
|
||||
override def hasWeirdWrap: Boolean = false
|
||||
override def isEchoEnabled: Boolean = term.isEchoEnabled
|
||||
override def setEchoEnabled(enabled: Boolean): Unit = {}
|
||||
override def disableInterruptCharacter(): Unit = {}
|
||||
override def enableInterruptCharacter(): Unit = {}
|
||||
override def getOutputEncoding: String = null
|
||||
override def getBooleanCapability(capability: String): Boolean = {
|
||||
term.getBooleanCapability(capability)
|
||||
}
|
||||
override def getNumericCapability(capability: String): Integer = {
|
||||
term.getNumericCapability(capability)
|
||||
}
|
||||
override def getStringCapability(capability: String): String = {
|
||||
term.getStringCapability(capability)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def close(): Unit = {
|
||||
if (System.console == null) {
|
||||
originalOut.close()
|
||||
originalIn.close()
|
||||
System.err.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if System.in is attached. When sbt is run as a subprocess, like in scripted or
|
||||
|
|
@ -90,15 +203,21 @@ object Terminal {
|
|||
*/
|
||||
def systemInIsAttached: Boolean = attached.get
|
||||
|
||||
def read: Int = inputStream.get match {
|
||||
case null => -1
|
||||
case is => is.read
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
private[sbt] def throwOnClosedSystemIn(in: InputStream): InputStream = new InputStream {
|
||||
override def available(): Int = in.available()
|
||||
override def read(): Int = in.read() match {
|
||||
case -1 => throw new ClosedChannelException
|
||||
case r if r >= 0 => r
|
||||
case _ => -1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,27 +233,7 @@ object Terminal {
|
|||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
private[sbt] def restore(): Unit = console.toJLine.restore()
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -147,142 +246,193 @@ object Terminal {
|
|||
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
|
||||
private[this] object ProxyTerminal extends Terminal {
|
||||
private def t: Terminal = currentTerminal.get
|
||||
override def getWidth: Int = t.getWidth
|
||||
override def getHeight: Int = t.getHeight
|
||||
override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line)
|
||||
override def lineCount(line: String): Int = t.lineCount(line)
|
||||
override def inputStream: InputStream = t.inputStream
|
||||
override def outputStream: OutputStream = t.outputStream
|
||||
override def isAnsiSupported: Boolean = t.isAnsiSupported
|
||||
override def isColorEnabled: Boolean = t.isColorEnabled
|
||||
override def isEchoEnabled: Boolean = t.isEchoEnabled
|
||||
override def isSuccessEnabled: Boolean = t.isSuccessEnabled
|
||||
override def isSupershellEnabled: Boolean = t.isSupershellEnabled
|
||||
override def getBooleanCapability(capability: String): Boolean =
|
||||
t.getBooleanCapability(capability)
|
||||
override def getNumericCapability(capability: String): Int = t.getNumericCapability(capability)
|
||||
override def getStringCapability(capability: String): String = t.getStringCapability(capability)
|
||||
override def withRawSystemIn[T](f: => T): T = t.withRawSystemIn(f)
|
||||
override def withCanonicalIn[T](f: => T): T = t.withCanonicalIn(f)
|
||||
override def printStream: PrintStream = t.printStream
|
||||
override def withPrintStream[T](f: PrintStream => T): T = t.withPrintStream(f)
|
||||
override def restore(): Unit = t.restore()
|
||||
override def close(): Unit = {}
|
||||
override private[sbt] def write(bytes: Int*): Unit = t.write(bytes: _*)
|
||||
override def getLastLine: Option[String] = t.getLastLine
|
||||
override private[sbt] def name: String = t.name
|
||||
}
|
||||
private[sbt] def get: Terminal = ProxyTerminal
|
||||
|
||||
private[sbt] def withIn[T](in: InputStream)(f: => T): T = {
|
||||
val original = inputStream.get
|
||||
try {
|
||||
inputStream.set(in)
|
||||
System.setIn(in)
|
||||
scala.Console.withIn(in)(f)
|
||||
} finally {
|
||||
inputStream.set(original)
|
||||
System.setIn(original)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[sbt] def withOut[T](out: PrintStream)(f: => T): T = {
|
||||
val originalOut = System.out
|
||||
val originalProxyOut = ConsoleOut.getGlobalProxy
|
||||
try {
|
||||
ConsoleOut.setGlobalProxy(ConsoleOut.printStreamOut(out))
|
||||
System.setOut(out)
|
||||
scala.Console.withOut(out)(f)
|
||||
} finally {
|
||||
ConsoleOut.setGlobalProxy(originalProxyOut)
|
||||
System.setOut(originalOut)
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
private[this] class WriteableInputStream(in: InputStream, name: String)
|
||||
extends InputStream
|
||||
with AutoCloseable {
|
||||
final def write(bytes: Int*): Unit = bytes.foreach(buffer.put)
|
||||
private[this] val executor =
|
||||
Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader"))
|
||||
private[this] val buffer = new LinkedBlockingQueue[Int]
|
||||
private[this] val latch = new CountDownLatch(1)
|
||||
private[this] val closed = new AtomicBoolean(false)
|
||||
private[this] def takeOne: Int = if (closed.get) -1 else buffer.take
|
||||
private[this] val runnable: Runnable = () => {
|
||||
@tailrec def impl(): Unit = {
|
||||
val b = in.read
|
||||
buffer.put(b)
|
||||
if (b != -1) impl()
|
||||
else closed.set(true)
|
||||
}
|
||||
try {
|
||||
latch.await()
|
||||
impl()
|
||||
} catch { case _: InterruptedException => }
|
||||
}
|
||||
executor.submit(runnable)
|
||||
override def read(): Int = {
|
||||
latch.countDown()
|
||||
takeOne match {
|
||||
case -1 => throw new ClosedChannelException
|
||||
case b => b
|
||||
}
|
||||
}
|
||||
|
||||
override def available(): Int = {
|
||||
latch.countDown()
|
||||
buffer.size
|
||||
}
|
||||
override def close(): Unit = {
|
||||
executor.shutdownNow()
|
||||
()
|
||||
}
|
||||
@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] val nonBlockingIn: WriteableInputStream =
|
||||
new WriteableInputStream(jline.TerminalFactory.get.wrapInIfNeeded(originalIn), "console")
|
||||
private[this] val inputStream = new AtomicReference[InputStream](System.in)
|
||||
private[this] def withOut[T](f: => T): T = {
|
||||
val thread = new WriteThread
|
||||
try {
|
||||
System.setOut(SystemPrintStream)
|
||||
scala.Console.withOut(SystemPrintStream)(f)
|
||||
System.setOut(proxyPrintStream)
|
||||
scala.Console.withOut(proxyOutputStream)(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)
|
||||
inputStream.set(Terminal.wrappedSystemIn)
|
||||
System.setIn(wrappedSystemIn)
|
||||
scala.Console.withIn(proxyInputStream)(f)
|
||||
} finally System.setIn(originalIn)
|
||||
|
||||
private[sbt] def withPrintStream[T](f: PrintStream => T): T = writeLock.synchronized {
|
||||
f(originalOut)
|
||||
private[sbt] def withPrintStream[T](f: PrintStream => T): T = console.withPrintStream(f)
|
||||
private[this] val attached = new AtomicBoolean(true)
|
||||
private[this] val terminalHolder = new AtomicReference(wrap(jline.TerminalFactory.get))
|
||||
private[this] val currentTerminal = new AtomicReference[Terminal](terminalHolder.get)
|
||||
jline.TerminalFactory.set(terminalHolder.get.toJLine)
|
||||
private[this] object proxyInputStream extends InputStream {
|
||||
def read(): Int = currentTerminal.get().inputStream.read()
|
||||
}
|
||||
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)))
|
||||
private[this] object proxyOutputStream extends OutputStream {
|
||||
private[this] def os = currentTerminal.get().outputStream
|
||||
def write(byte: Int): Unit = {
|
||||
os.write(byte)
|
||||
os.flush()
|
||||
if (byte == 10) os.flush()
|
||||
}
|
||||
def write(s: String): Unit = s.getBytes.foreach(lineBuffer.put)
|
||||
override def flush(): Unit = writeLock.synchronized(flushQueue.put(()))
|
||||
override def write(bytes: Array[Byte]): Unit = write(bytes, 0, bytes.length)
|
||||
override def write(bytes: Array[Byte], offset: Int, len: Int): Unit = {
|
||||
os.write(bytes, offset, len)
|
||||
os.flush()
|
||||
}
|
||||
override def flush(): Unit = os.flush()
|
||||
}
|
||||
private object SystemPrintStream extends PrintStream(SystemOutputStream, true)
|
||||
private[this] val proxyPrintStream = new PrintStream(proxyOutputStream, true) {
|
||||
override def toString: String = s"proxyPrintStream($proxyOutputStream)"
|
||||
override def println(s: String): Unit = {
|
||||
proxyOutputStream.write(s"$s\n".getBytes("UTF-8"))
|
||||
proxyOutputStream.flush()
|
||||
}
|
||||
}
|
||||
private[this] lazy val isWindows =
|
||||
System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
|
||||
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
|
||||
private[this] val in = proxyInputStream
|
||||
override def available(): Int = if (attached.get) in.available() else 0
|
||||
override def read(): Int = synchronized {
|
||||
if (attached.get) {
|
||||
val res = in.read
|
||||
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] def wrap(terminal: jline.Terminal): Terminal = {
|
||||
val term: jline.Terminal with jline.Terminal2 = new jline.Terminal with jline.Terminal2 {
|
||||
/*
|
||||
* JLine spams the log with stacktraces if we directly interrupt the thread that is shelling
|
||||
* out to run an stty command. To avoid this, run certain commands on a background thread.
|
||||
*/
|
||||
private[this] def doInBackground[T](f: => T): Unit = {
|
||||
val result = new ArrayBlockingQueue[Either[Throwable, Any]](1)
|
||||
new Thread("sbt-terminal-background-work-thread") {
|
||||
setDaemon(true)
|
||||
start()
|
||||
override def run(): Unit = {
|
||||
try result.put(Right(f))
|
||||
catch { case t: Throwable => result.put(Left(t)) }
|
||||
}
|
||||
}
|
||||
result.take match {
|
||||
case Left(e) => throw e
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
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()
|
||||
private[this] val term2: jline.Terminal2 = terminal match {
|
||||
case t: jline.Terminal2 => t
|
||||
case _ => new DefaultTerminal2(terminal)
|
||||
}
|
||||
override def init(): Unit = if (alive) doInBackground(terminal.init())
|
||||
override def restore(): Unit = if (alive) doInBackground(terminal.restore())
|
||||
override def reset(): Unit = if (alive) doInBackground(terminal.reset())
|
||||
override def isSupported: Boolean = terminal.isSupported
|
||||
override def getWidth: Int = terminal.getWidth
|
||||
override def getHeight: Int = terminal.getHeight
|
||||
|
|
@ -291,22 +441,33 @@ object Terminal {
|
|||
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)
|
||||
}
|
||||
|
||||
/*
|
||||
* Do this on a background thread so that jline doesn't spam the logs if interrupted
|
||||
*/
|
||||
override def setEchoEnabled(enabled: Boolean): Unit =
|
||||
if (alive) doInBackground(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
|
||||
override def getBooleanCapability(capability: String): Boolean =
|
||||
term2.getBooleanCapability(capability)
|
||||
override def getNumericCapability(capability: String): Integer =
|
||||
term2.getNumericCapability(capability)
|
||||
override def getStringCapability(capability: String): String = {
|
||||
term2.getStringCapability(capability)
|
||||
}
|
||||
}
|
||||
term.restore()
|
||||
term.setEchoEnabled(true)
|
||||
term
|
||||
new ConsoleTerminal(term, nonBlockingIn, originalOut)
|
||||
}
|
||||
|
||||
private[util] def reset(): Unit = {
|
||||
private[sbt] def reset(): Unit = {
|
||||
jline.TerminalFactory.reset()
|
||||
console.close()
|
||||
terminalHolder.set(wrap(jline.TerminalFactory.get))
|
||||
}
|
||||
|
||||
|
|
@ -329,14 +490,178 @@ object Terminal {
|
|||
}
|
||||
fixTerminalProperty()
|
||||
|
||||
private[sbt] def createReader(in: InputStream): ConsoleReader =
|
||||
new ConsoleReader(in, System.out, terminal)
|
||||
private[sbt] def createReader(term: Terminal, prompt: Prompt): ConsoleReader = {
|
||||
new ConsoleReader(term.inputStream, prompt.wrappedOutputStream(term), term.toJLine) {
|
||||
override def readLine(prompt: String, mask: Character): String =
|
||||
term.withRawSystemIn(super.readLine(prompt, mask))
|
||||
override def readLine(prompt: String): String = term.withRawSystemIn(super.readLine(prompt))
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def terminal: jline.Terminal = terminalHolder.get match {
|
||||
private[sbt] def console: 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
|
||||
private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine
|
||||
private class ConsoleTerminal(
|
||||
val term: jline.Terminal with jline.Terminal2,
|
||||
in: InputStream,
|
||||
out: OutputStream
|
||||
) extends TerminalImpl(in, out, "console0") {
|
||||
private[this] def isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")
|
||||
override def getWidth: Int = term.getWidth
|
||||
override def getHeight: Int = term.getHeight
|
||||
override def isAnsiSupported: Boolean = term.isAnsiSupported && !isCI
|
||||
override def isEchoEnabled: Boolean = term.isEchoEnabled
|
||||
override def isSuccessEnabled: Boolean = true
|
||||
override def getBooleanCapability(capability: String): Boolean =
|
||||
term.getBooleanCapability(capability)
|
||||
override def getNumericCapability(capability: String): Int =
|
||||
term.getNumericCapability(capability)
|
||||
override def getStringCapability(capability: String): String =
|
||||
term.getStringCapability(capability)
|
||||
override private[sbt] def restore(): Unit = term.restore()
|
||||
|
||||
override def withRawSystemIn[T](f: => T): T = term.synchronized {
|
||||
try {
|
||||
term.init()
|
||||
term.setEchoEnabled(false)
|
||||
f
|
||||
} finally {
|
||||
term.restore()
|
||||
term.setEchoEnabled(true)
|
||||
}
|
||||
}
|
||||
override def isColorEnabled: Boolean = ConsoleAppender.formatEnabledInEnv
|
||||
|
||||
override def isSupershellEnabled: Boolean = System.getProperty("sbt.supershell") match {
|
||||
case null => !(sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")) && isColorEnabled
|
||||
case "true" => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
private[sbt] abstract class TerminalImpl private[sbt] (
|
||||
val in: InputStream,
|
||||
val out: OutputStream,
|
||||
override private[sbt] val name: String
|
||||
) extends Terminal {
|
||||
private[this] val directWrite = new AtomicBoolean(false)
|
||||
private[this] val currentLine = new AtomicReference(new ArrayBuffer[Byte])
|
||||
private[this] val lineBuffer = new LinkedBlockingQueue[Byte]
|
||||
private[this] val flushQueue = new LinkedBlockingQueue[Seq[Byte]]
|
||||
private[this] val writeLock = new AnyRef
|
||||
private[this] val writeableInputStream = in match {
|
||||
case w: WriteableInputStream => w
|
||||
case _ => new WriteableInputStream(in, name)
|
||||
}
|
||||
def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f
|
||||
|
||||
override val outputStream = new OutputStream {
|
||||
override def write(b: Int): Unit = throwIfClosed {
|
||||
writeLock.synchronized {
|
||||
if (b == Int.MinValue) currentLine.set(new ArrayBuffer[Byte])
|
||||
else doWrite(Vector((b & 0xFF).toByte))
|
||||
if (b == 10) out.flush()
|
||||
}
|
||||
}
|
||||
override def write(b: Array[Byte]): Unit = throwIfClosed(write(b, 0, b.length))
|
||||
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
|
||||
throwIfClosed {
|
||||
writeLock.synchronized {
|
||||
val lo = math.max(0, off)
|
||||
val hi = math.min(math.max(off + len, 0), b.length)
|
||||
doWrite(b.slice(off, off + len).toSeq)
|
||||
}
|
||||
}
|
||||
}
|
||||
private[this] val clear = s"$CursorLeft1000$ClearScreenAfterCursor"
|
||||
private def doWrite(bytes: Seq[Byte]): Unit = {
|
||||
def doWrite(b: Byte): Unit = out.write(b & 0xFF)
|
||||
val remaining = bytes.foldLeft(new ArrayBuffer[Byte]) { (buf, i) =>
|
||||
if (i == 10) {
|
||||
progressState.addBytes(TerminalImpl.this, buf)
|
||||
progressState.clearBytes()
|
||||
val cl = currentLine.get
|
||||
if (buf.nonEmpty && isAnsiSupported && cl.isEmpty) clear.getBytes.foreach(doWrite)
|
||||
out.write(buf.toArray)
|
||||
out.write(10)
|
||||
currentLine.get match {
|
||||
case s if s.nonEmpty => currentLine.set(new ArrayBuffer[Byte])
|
||||
case _ =>
|
||||
}
|
||||
progressState.reprint(TerminalImpl.this, rawPrintStream)
|
||||
new ArrayBuffer[Byte]
|
||||
} else buf += i
|
||||
}
|
||||
if (remaining.nonEmpty) {
|
||||
val cl = currentLine.get
|
||||
if (isAnsiSupported && cl.isEmpty) {
|
||||
clear.getBytes.foreach(doWrite)
|
||||
}
|
||||
cl ++= remaining
|
||||
out.write(remaining.toArray)
|
||||
}
|
||||
out.flush()
|
||||
}
|
||||
}
|
||||
override private[sbt] val printStream: PrintStream = new PrintStream(outputStream, true)
|
||||
override def inputStream: InputStream = writeableInputStream
|
||||
|
||||
private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*)
|
||||
private[this] val isStopped = new AtomicBoolean(false)
|
||||
|
||||
override def getLineHeightAndWidth(line: String): (Int, Int) = getWidth match {
|
||||
case width if width > 0 =>
|
||||
val position = EscHelpers.cursorPosition(line)
|
||||
val count = (position + width - 1) / width
|
||||
(count, position - (math.max((count - 1), 0) * width))
|
||||
case _ => (0, 0)
|
||||
}
|
||||
|
||||
override def getLastLine: Option[String] = currentLine.get match {
|
||||
case bytes if bytes.isEmpty => None
|
||||
case bytes =>
|
||||
// TODO there are ghost characters when the user deletes prompt characters
|
||||
// when they are given the cancellation option
|
||||
Some(new String(bytes.toArray).replaceAllLiterally(ClearScreenAfterCursor, ""))
|
||||
}
|
||||
|
||||
private[this] val rawPrintStream: PrintStream = new PrintStream(out, true) {
|
||||
override def close(): Unit = {}
|
||||
}
|
||||
override def withPrintStream[T](f: PrintStream => T): T =
|
||||
writeLock.synchronized(f(rawPrintStream))
|
||||
|
||||
override def close(): Unit = if (isStopped.compareAndSet(false, true)) {
|
||||
writeableInputStream.close()
|
||||
}
|
||||
}
|
||||
private[sbt] val NullTerminal = new Terminal {
|
||||
override def close(): Unit = {}
|
||||
override def getBooleanCapability(capability: String): Boolean = false
|
||||
override def getHeight: Int = 0
|
||||
override def getLastLine: Option[String] = None
|
||||
override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0)
|
||||
override def getNumericCapability(capability: String): Int = -1
|
||||
override def getStringCapability(capability: String): String = null
|
||||
override def getWidth: Int = 0
|
||||
override def inputStream: java.io.InputStream = () => {
|
||||
try this.synchronized(this.wait)
|
||||
catch { case _: InterruptedException => }
|
||||
-1
|
||||
}
|
||||
override def isAnsiSupported: Boolean = false
|
||||
override def isColorEnabled: Boolean = false
|
||||
override def isEchoEnabled: Boolean = false
|
||||
override def isSuccessEnabled: Boolean = false
|
||||
override def isSupershellEnabled: Boolean = false
|
||||
override def outputStream: java.io.OutputStream = _ => {}
|
||||
override private[sbt] def name: String = "NullTerminal"
|
||||
override private[sbt] val printStream: java.io.PrintStream =
|
||||
new PrintStream(outputStream, false)
|
||||
override private[sbt] def withPrintStream[T](f: java.io.PrintStream => T): T = f(printStream)
|
||||
override private[sbt] def write(bytes: Int*): Unit = {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.nio.channels.ClosedChannelException
|
||||
import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile }
|
||||
import sbt.internal.util.Terminal
|
||||
import sbt.util.Logger
|
||||
|
|
@ -45,14 +46,30 @@ final class Console(compiler: AnalyzingCompiler) {
|
|||
initialCommands: String,
|
||||
cleanupCommands: String
|
||||
)(loader: Option[ClassLoader], bindings: Seq[(String, Any)])(implicit log: Logger): Try[Unit] = {
|
||||
def console0() =
|
||||
compiler.console(classpath map { x =>
|
||||
apply(classpath, options, initialCommands, cleanupCommands, Terminal.get)(loader, bindings)
|
||||
}
|
||||
def apply(
|
||||
classpath: Seq[File],
|
||||
options: Seq[String],
|
||||
initialCommands: String,
|
||||
cleanupCommands: String,
|
||||
terminal: Terminal
|
||||
)(loader: Option[ClassLoader], bindings: Seq[(String, Any)])(implicit log: Logger): Try[Unit] = {
|
||||
def console0(): Unit =
|
||||
try compiler.console(classpath map { x =>
|
||||
PlainVirtualFile(x.toPath)
|
||||
}, options, initialCommands, cleanupCommands, log)(
|
||||
loader,
|
||||
bindings
|
||||
)
|
||||
Terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
|
||||
catch { case _: InterruptedException | _: ClosedChannelException => }
|
||||
val previous = sys.props.get("scala.color").getOrElse("auto")
|
||||
try {
|
||||
sys.props("scala.color") = if (terminal.isColorEnabled) "true" else "false"
|
||||
terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
|
||||
} finally {
|
||||
sys.props("scala.color") = previous
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ object BasicCommands {
|
|||
oldshell,
|
||||
client,
|
||||
read,
|
||||
alias
|
||||
alias,
|
||||
)
|
||||
|
||||
def nop: Command = Command.custom(s => success(() => s))
|
||||
|
|
@ -375,8 +375,7 @@ 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, LineReader.HandleCONT, Terminal.wrappedSystemIn)
|
||||
val reader = new FullReader(history, s.combinedParser, LineReader.HandleCONT, Terminal.console)
|
||||
val line = reader.readLine(prompt)
|
||||
line match {
|
||||
case Some(line) =>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package internal
|
|||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
import sbt.internal.util.Terminal
|
||||
import sbt.protocol.EventMessage
|
||||
|
||||
/**
|
||||
|
|
@ -44,6 +45,7 @@ abstract class CommandChannel {
|
|||
def publishBytes(bytes: Array[Byte]): Unit
|
||||
def shutdown(): Unit
|
||||
def name: String
|
||||
private[sbt] def terminal: Terminal
|
||||
}
|
||||
|
||||
// case class Exec(commandLine: String, source: Option[CommandSource])
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel
|
|||
history,
|
||||
s.combinedParser,
|
||||
LineReader.HandleCONT,
|
||||
Terminal.throwOnClosedSystemIn
|
||||
Terminal.console,
|
||||
)
|
||||
setDaemon(true)
|
||||
start()
|
||||
|
|
@ -49,7 +49,6 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel
|
|||
} 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()
|
||||
}
|
||||
|
|
@ -80,4 +79,5 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel
|
|||
case _ => ()
|
||||
}
|
||||
}
|
||||
override private[sbt] def terminal = Terminal.console
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ trait CommandLineUIService extends InteractionService {
|
|||
}
|
||||
}
|
||||
|
||||
override def terminalWidth: Int = Terminal.getWidth
|
||||
override def terminalWidth: Int = Terminal.get.getWidth
|
||||
|
||||
override def terminalHeight: Int = Terminal.getHeight
|
||||
override def terminalHeight: Int = Terminal.get.getHeight
|
||||
}
|
||||
|
||||
object CommandLineUIService extends CommandLineUIService
|
||||
|
|
|
|||
|
|
@ -1501,8 +1501,8 @@ object Defaults extends BuildCommon {
|
|||
Some(s => {
|
||||
def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() }
|
||||
print(s)
|
||||
Terminal.withRawSystemIn {
|
||||
Terminal.wrappedSystemIn.read match {
|
||||
Terminal.get.withRawSystemIn {
|
||||
Terminal.read match {
|
||||
case -1 => None
|
||||
case b =>
|
||||
val res = b.toChar.toString
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package sbt
|
|||
|
||||
import java.io.{ File, IOException }
|
||||
import java.net.URI
|
||||
import java.nio.channels.ClosedChannelException
|
||||
import java.nio.file.{ FileAlreadyExistsException, FileSystems, Files }
|
||||
import java.util.Properties
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
|
|
@ -21,6 +22,7 @@ import sbt.internal.CommandStrings.BootCommand
|
|||
import sbt.internal._
|
||||
import sbt.internal.client.BspClient
|
||||
import sbt.internal.inc.ScalaInstance
|
||||
import sbt.internal.io.Retry
|
||||
import sbt.internal.nio.CheckBuildSources
|
||||
import sbt.internal.util.Types.{ const, idFun }
|
||||
import sbt.internal.util._
|
||||
|
|
@ -145,13 +147,17 @@ object StandardMain {
|
|||
/** The common interface to standard output, used for all built-in ConsoleLoggers. */
|
||||
val console: ConsoleOut =
|
||||
ConsoleOut.systemOutOverwrite(ConsoleOut.overwriteContaining("Resolving "))
|
||||
ConsoleOut.setGlobalProxy(console)
|
||||
|
||||
private[this] def initialGlobalLogging(file: Option[File]): GlobalLogging = {
|
||||
file.foreach(f => if (!f.exists()) IO.createDirectory(f))
|
||||
def createTemp(attempt: Int = 0): File = Retry {
|
||||
file.foreach(f => if (!f.exists()) IO.createDirectory(f))
|
||||
File.createTempFile("sbt-global-log", ".log", file.orNull)
|
||||
}
|
||||
GlobalLogging.initial(
|
||||
MainAppender.globalDefault(console),
|
||||
File.createTempFile("sbt-global-log", ".log", file.orNull),
|
||||
console
|
||||
MainAppender.globalDefault(ConsoleOut.globalProxy),
|
||||
createTemp(),
|
||||
ConsoleOut.globalProxy
|
||||
)
|
||||
}
|
||||
def initialGlobalLogging(file: File): GlobalLogging = initialGlobalLogging(Option(file))
|
||||
|
|
@ -770,12 +776,11 @@ object BuiltinCommands {
|
|||
@tailrec
|
||||
private[this] def doLoadFailed(s: State, loadArg: String): State = {
|
||||
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
|
||||
})
|
||||
}
|
||||
val terminal = Terminal.get
|
||||
val result = try terminal.withRawSystemIn(terminal.inputStream.read) match {
|
||||
case -1 => 'q'.toInt
|
||||
case b => b
|
||||
} catch { case _: ClosedChannelException => 'q' }
|
||||
def retry: State = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog
|
||||
def ignoreMsg: String =
|
||||
if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded"
|
||||
|
|
|
|||
|
|
@ -309,6 +309,6 @@ private[sbt] final class CommandExchange {
|
|||
.withChannelName(currentExec.flatMap(_.source.map(_.channelName)))
|
||||
case _ => pe
|
||||
}
|
||||
ProgressState.updateProgressState(newPE)
|
||||
channels.foreach(c => ProgressState.updateProgressState(newPE, c.terminal))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ object ConsoleProject {
|
|||
val importString = imports.mkString("", ";\n", ";\n\n")
|
||||
val initCommands = importString + extra
|
||||
|
||||
Terminal.withCanonicalIn {
|
||||
Terminal.get.withCanonicalIn {
|
||||
// TODO - Hook up dsl classpath correctly...
|
||||
(new Console(compiler))(
|
||||
unit.classpath,
|
||||
|
|
|
|||
|
|
@ -272,45 +272,46 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
f(s, valid, invalid)
|
||||
}
|
||||
|
||||
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]
|
||||
private[this] val closed = new AtomicBoolean(false)
|
||||
private[this] val thread = new Thread("Continuous-input-stream-reader") {
|
||||
setDaemon(true)
|
||||
start()
|
||||
@tailrec
|
||||
override def run(): Unit = {
|
||||
try {
|
||||
if (!closed.get()) {
|
||||
wrapped.read() match {
|
||||
case -1 => closed.set(true)
|
||||
case b => buffer.add(b)
|
||||
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R =
|
||||
Terminal.get.withRawSystemIn {
|
||||
val wrapped = Terminal.get.inputStream
|
||||
if (Util.isNonCygwinWindows) {
|
||||
val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable {
|
||||
private[this] val buffer = new java.util.LinkedList[Int]
|
||||
private[this] val closed = new AtomicBoolean(false)
|
||||
private[this] val thread = new Thread("Continuous-input-stream-reader") {
|
||||
setDaemon(true)
|
||||
start()
|
||||
@tailrec
|
||||
override def run(): Unit = {
|
||||
try {
|
||||
if (!closed.get()) {
|
||||
wrapped.read() match {
|
||||
case -1 => closed.set(true)
|
||||
case b => buffer.add(b)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case _: InterruptedException => closed.set(true)
|
||||
}
|
||||
} catch {
|
||||
case _: InterruptedException => closed.set(true)
|
||||
if (!closed.get()) run()
|
||||
}
|
||||
if (!closed.get()) run()
|
||||
}
|
||||
override def available(): Int = buffer.size()
|
||||
override def read(): Int = buffer.poll()
|
||||
override def close(): Unit = if (closed.compareAndSet(false, true)) {
|
||||
thread.interrupt()
|
||||
}
|
||||
}
|
||||
override def available(): Int = buffer.size()
|
||||
override def read(): Int = buffer.poll()
|
||||
override def close(): Unit = if (closed.compareAndSet(false, true)) {
|
||||
thread.interrupt()
|
||||
try {
|
||||
f(inputStream)
|
||||
} finally {
|
||||
inputStream.close()
|
||||
}
|
||||
} else {
|
||||
f(wrapped)
|
||||
}
|
||||
try {
|
||||
f(inputStream)
|
||||
} finally {
|
||||
inputStream.close()
|
||||
}
|
||||
} else {
|
||||
f(wrapped)
|
||||
}
|
||||
}
|
||||
|
||||
private[sbt] def runToTermination(
|
||||
state: State,
|
||||
|
|
|
|||
|
|
@ -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(Terminal.getWidth, defaultWidth) - 8
|
||||
val maxColumn = math.max(Terminal.get.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) + ".."
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import sbt.internal.protocol.{
|
|||
JsonRpcResponseError,
|
||||
JsonRpcResponseMessage
|
||||
}
|
||||
import sbt.internal.util.ReadJsonFromInputStream
|
||||
import sbt.internal.util.{ ReadJsonFromInputStream, Terminal }
|
||||
import sbt.internal.util.complete.Parser
|
||||
import sbt.protocol._
|
||||
import sbt.util.Logger
|
||||
|
|
@ -47,6 +47,8 @@ final class NetworkChannel(
|
|||
private var initialized = false
|
||||
private val pendingRequests: mutable.Map[String, JsonRpcRequestMessage] = mutable.Map()
|
||||
|
||||
override private[sbt] def terminal: Terminal = Terminal.NullTerminal
|
||||
|
||||
private lazy val callback: ServerCallback = new ServerCallback {
|
||||
def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit =
|
||||
self.respondResult(event, execId)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ object Dependencies {
|
|||
val sjsonNewScalaJson = sjsonNew("sjson-new-scalajson")
|
||||
val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash")
|
||||
|
||||
val jline = "jline" % "jline" % "2.14.6"
|
||||
val jline = "org.scala-sbt.jline" % "jline" % "2.14.7-sbt-5e51b9d4f9631ebfa29753ce4accc57808e7fd6b"
|
||||
val jansi = "org.fusesource.jansi" % "jansi" % "1.12"
|
||||
val scalatest = "org.scalatest" %% "scalatest" % "3.0.8"
|
||||
val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0"
|
||||
val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue