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:
Ethan Atkins 2020-06-22 07:08:25 -07:00
parent 120e6eb63d
commit 1b03c9b1a9
19 changed files with 887 additions and 358 deletions

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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 = {}
}
}

View File

@ -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
}
}
}

View File

@ -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) =>

View File

@ -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])

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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))
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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) + ".."

View File

@ -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)

View File

@ -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"