Support System.err in thin client

I noticed that when reloading the build, that certain errors are logged
by sbt to System.err. These were not shown to a thin client because we
weren't forwarding System.err. This change remedies that.

System.err is handled more simply than System.out. We do not put
System.err through the progress state because generally System.err is
tends to be unbuffered. I had hesitated to add System.err to the
Terminal interface at all to give users an escape hatch but I couldn't
get project loading to work well with the thin client without it.
This commit is contained in:
Ethan Atkins 2020-07-21 13:18:36 -07:00
parent d53ebaa686
commit e82c3405b9
4 changed files with 60 additions and 5 deletions

View File

@ -60,6 +60,12 @@ trait Terminal extends AutoCloseable {
*/
def outputStream: OutputStream
/**
* Gets the error stream for this Terminal.
* @return the error stream.
*/
def errorStream: OutputStream
/**
* Returns true if the terminal supports ansi characters.
*
@ -221,7 +227,7 @@ object Terminal {
if (System.console == null) {
originalOut.close()
originalIn.close()
System.err.close()
originalErr.close()
}
}
@ -316,6 +322,7 @@ object Terminal {
override def lineCount(line: String): Int = t.lineCount(line)
override def inputStream: InputStream = t.inputStream
override def outputStream: OutputStream = t.outputStream
override def errorStream: OutputStream = t.errorStream
override def isAnsiSupported: Boolean = t.isAnsiSupported
override def isColorEnabled: Boolean = t.isColorEnabled
override def isEchoEnabled: Boolean = t.isEchoEnabled
@ -359,14 +366,17 @@ object Terminal {
private[sbt] def withOut[T](out: PrintStream)(f: => T): T = {
val originalOut = System.out
val originalErr = System.err
val originalProxyOut = ConsoleOut.getGlobalProxy
try {
ConsoleOut.setGlobalProxy(ConsoleOut.printStreamOut(out))
System.setOut(out)
scala.Console.withOut(out)(f)
System.setErr(out)
scala.Console.withErr(out)(scala.Console.withOut(out)(f))
} finally {
ConsoleOut.setGlobalProxy(originalProxyOut)
System.setOut(originalOut)
System.setErr(originalErr)
}
}
@ -379,6 +389,7 @@ object Terminal {
}
}
private[this] val originalOut = new LinePrintStream(System.out)
private[this] val originalErr = System.err
private[this] val originalIn = System.in
private[sbt] class WriteableInputStream(in: InputStream, name: String)
extends InputStream
@ -476,9 +487,11 @@ object Terminal {
private[this] def withOut[T](f: => T): T = {
try {
System.setOut(proxyPrintStream)
scala.Console.withOut(proxyOutputStream)(f)
System.setErr(proxyErrorStream)
scala.Console.withErr(proxyErrorStream)(scala.Console.withOut(proxyOutputStream)(f))
} finally {
System.setOut(originalOut)
System.setErr(originalErr)
}
}
private[this] def withIn[T](f: => T): T =
@ -602,6 +615,15 @@ object Terminal {
private[this] val proxyPrintStream = new LinePrintStream(proxyOutputStream) {
override def toString: String = s"proxyPrintStream($proxyOutputStream)"
}
private[this] object proxyErrorOutputStream extends OutputStream {
private[this] def os: OutputStream = activeTerminal.get().errorStream
def write(byte: Int): Unit = os.write(byte)
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)
override def flush(): Unit = os.flush()
}
private[this] val proxyErrorStream = new PrintStream(proxyErrorOutputStream, true)
private[this] lazy val isWindows =
System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
private[this] object WrappedSystemIn extends InputStream {
@ -758,7 +780,7 @@ object Terminal {
val term: jline.Terminal with jline.Terminal2,
in: InputStream,
out: OutputStream
) extends TerminalImpl(in, out, "console0") {
) extends TerminalImpl(in, out, originalErr, "console0") {
private[util] lazy val system = JLine3.system
private[this] def isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")
override def getWidth: Int = system.getSize.getColumns
@ -815,6 +837,7 @@ object Terminal {
private[sbt] abstract class TerminalImpl private[sbt] (
val in: InputStream,
val out: OutputStream,
override val errorStream: OutputStream,
override private[sbt] val name: String
) extends Terminal {
private[this] val rawMode = new AtomicBoolean(false)
@ -907,6 +930,7 @@ object Terminal {
override def isSuccessEnabled: Boolean = false
override def isSupershellEnabled: Boolean = false
override def outputStream: java.io.OutputStream = _ => {}
override def errorStream: java.io.OutputStream = _ => {}
override private[sbt] def getAttributes: Map[String, String] = Map.empty
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = {}
override private[sbt] def setSize(width: Int, height: Int): Unit = {}

View File

@ -41,8 +41,10 @@ import Serialization.{
cancelRequest,
promptChannel,
systemIn,
systemErr,
systemOut,
systemOutFlush,
systemErrFlush,
terminalCapabilities,
terminalCapabilitiesResponse,
terminalPropertiesQuery,
@ -527,9 +529,19 @@ class NetworkClient(
case _ =>
}
Vector.empty
case (`systemErr`, Some(json)) =>
Converter.fromJson[Array[Byte]](json) match {
case Success(bytes) if bytes.nonEmpty && attached.get =>
synchronized(errorStream.write(bytes))
case _ =>
}
Vector.empty
case (`systemOutFlush`, _) =>
synchronized(printStream.flush())
Vector.empty
case (`systemErrFlush`, _) =>
synchronized(errorStream.flush())
Vector.empty
case (`promptChannel`, _) =>
batchMode.set(false)
Vector.empty

View File

@ -709,7 +709,24 @@ final class NetworkChannel(
write(java.util.Arrays.copyOfRange(b, off, off + len))
}
}
private class NetworkTerminal extends TerminalImpl(inputStream, outputStream, name) {
private[this] lazy val errorStream: OutputStream = new OutputStream {
private[this] val buffer = new LinkedBlockingQueue[Byte]
override def write(b: Int): Unit = buffer.synchronized {
buffer.put(b.toByte)
}
override def flush(): Unit = {
val list = new java.util.ArrayList[Byte]
buffer.synchronized(buffer.drainTo(list))
if (!list.isEmpty) jsonRpcNotify(Serialization.systemErr, list.asScala.toSeq)
}
override def write(b: Array[Byte]): Unit = buffer.synchronized {
b.foreach(buffer.put)
}
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
write(java.util.Arrays.copyOfRange(b, off, off + len))
}
}
private class NetworkTerminal extends TerminalImpl(inputStream, outputStream, errorStream, name) {
private[this] val pending = new AtomicBoolean(false)
private[this] val closed = new AtomicBoolean(false)
private[this] val properties = new AtomicReference[TerminalPropertiesResponse]

View File

@ -26,7 +26,9 @@ object Serialization {
private[sbt] val VsCode = "application/vscode-jsonrpc; charset=utf-8"
val systemIn = "sbt/systemIn"
val systemOut = "sbt/systemOut"
val systemErr = "sbt/systemErr"
val systemOutFlush = "sbt/systemOutFlush"
val systemErrFlush = "sbt/systemErrFlush"
val terminalPropertiesQuery = "sbt/terminalPropertiesQuery"
val terminalPropertiesResponse = "sbt/terminalPropertiesResponse"
val terminalCapabilities = "sbt/terminalCapabilities"