Merge pull request #5319 from eatkins/terminal

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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