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

View File

@ -41,8 +41,10 @@ import Serialization.{
cancelRequest, cancelRequest,
promptChannel, promptChannel,
systemIn, systemIn,
systemErr,
systemOut, systemOut,
systemOutFlush, systemOutFlush,
systemErrFlush,
terminalCapabilities, terminalCapabilities,
terminalCapabilitiesResponse, terminalCapabilitiesResponse,
terminalPropertiesQuery, terminalPropertiesQuery,
@ -527,9 +529,19 @@ class NetworkClient(
case _ => case _ =>
} }
Vector.empty 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`, _) => case (`systemOutFlush`, _) =>
synchronized(printStream.flush()) synchronized(printStream.flush())
Vector.empty Vector.empty
case (`systemErrFlush`, _) =>
synchronized(errorStream.flush())
Vector.empty
case (`promptChannel`, _) => case (`promptChannel`, _) =>
batchMode.set(false) batchMode.set(false)
Vector.empty Vector.empty

View File

@ -709,7 +709,24 @@ final class NetworkChannel(
write(java.util.Arrays.copyOfRange(b, off, off + len)) 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 pending = new AtomicBoolean(false)
private[this] val closed = new AtomicBoolean(false) private[this] val closed = new AtomicBoolean(false)
private[this] val properties = new AtomicReference[TerminalPropertiesResponse] 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" private[sbt] val VsCode = "application/vscode-jsonrpc; charset=utf-8"
val systemIn = "sbt/systemIn" val systemIn = "sbt/systemIn"
val systemOut = "sbt/systemOut" val systemOut = "sbt/systemOut"
val systemErr = "sbt/systemErr"
val systemOutFlush = "sbt/systemOutFlush" val systemOutFlush = "sbt/systemOutFlush"
val systemErrFlush = "sbt/systemErrFlush"
val terminalPropertiesQuery = "sbt/terminalPropertiesQuery" val terminalPropertiesQuery = "sbt/terminalPropertiesQuery"
val terminalPropertiesResponse = "sbt/terminalPropertiesResponse" val terminalPropertiesResponse = "sbt/terminalPropertiesResponse"
val terminalCapabilities = "sbt/terminalCapabilities" val terminalCapabilities = "sbt/terminalCapabilities"