Buffer terminal lines

It is useful to store a buffer of the lines written to each terminal. We
can use those lines to replay the terminal log lines to a different
client. This is particularly nice when a remote client connects to sbt
while it's booting. We can show the remote client all the lines
displayed by the console prior to the client connecting.
This commit is contained in:
Ethan Atkins 2020-07-07 10:38:24 -07:00
parent 6faf460a1b
commit e1c9ed5a55
3 changed files with 45 additions and 5 deletions

View File

@ -8,16 +8,18 @@
package sbt.internal.util
import java.io.PrintStream
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.atomic.{ AtomicInteger, AtomicReference }
import sbt.internal.util.ConsoleAppender.{
ClearScreenAfterCursor,
CursorLeft1000,
DeleteLine,
cursorUp,
cursorUp
}
import scala.collection.mutable.ArrayBuffer
import scala.collection.JavaConverters._
private[sbt] final class ProgressState(
val progressLines: AtomicReference[Seq[String]],
@ -41,6 +43,9 @@ private[sbt] final class ProgressState(
padding.set(0)
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[this] val lineBuffer = new ArrayBlockingQueue[String](300)
private[util] def getLines: Seq[String] = lineBuffer.asScala.toVector
private[this] def appendLine(line: String) = while (!lineBuffer.offer(line)) { lineBuffer.poll }
private[util] def clearBytes(): Unit = {
val pad = padding.get
if (currentLineBytes.get.isEmpty && pad > 0) padding.decrementAndGet()
@ -62,10 +67,12 @@ private[sbt] final class ProgressState(
if (lines.contains(System.lineSeparator)) {
currentLineBytes.set(new ArrayBuffer[Byte])
if (!lines.endsWith(System.lineSeparator)) {
lines
.split(System.lineSeparator)
.lastOption
val allLines = lines.split(System.lineSeparator)
allLines.dropRight(1).foreach(appendLine)
allLines.lastOption
.foreach(currentLineBytes.get ++= _.getBytes("UTF-8"))
} else if (lines.contains(System.lineSeparator)) {
lines.split(System.lineSeparator).foreach(appendLine)
}
}
}

View File

@ -109,6 +109,16 @@ trait Terminal extends AutoCloseable {
*/
private[sbt] def getLastLine: Option[String]
/**
* Returns the buffered lines that have been written to the terminal. The
* main use case is to display the system startup log lines when a client
* connects to a booting server. This could also be used to implement a more
* tmux like experience where multiple clients connect to the same console.
*
* @return the lines
*/
private[sbt] def getLines: Seq[String]
private[sbt] def getBooleanCapability(capability: String, jline3: Boolean): Boolean
private[sbt] def getNumericCapability(capability: String, jline3: Boolean): Integer
private[sbt] def getStringCapability(capability: String, jline3: Boolean): String
@ -328,6 +338,7 @@ object Terminal {
override def close(): Unit = {}
override private[sbt] def write(bytes: Int*): Unit = t.write(bytes: _*)
override def getLastLine: Option[String] = t.getLastLine
override def getLines: Seq[String] = t.getLines
override private[sbt] def name: String = t.name
}
private[sbt] def get: Terminal = ProxyTerminal
@ -731,7 +742,7 @@ object Terminal {
}
}
private[sbt] def console: Terminal = consoleTerminalHolder.get match {
def console: Terminal = consoleTerminalHolder.get match {
case null => throw new IllegalStateException("Uninitialized terminal.")
case term => term
}
@ -808,6 +819,7 @@ object Terminal {
}
def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f
override def getLastLine: Option[String] = progressState.currentLine
override def getLines: Seq[String] = progressState.getLines
private val combinedOutputStream = new OutputStream {
override def write(b: Int): Unit = {
@ -868,6 +880,7 @@ object Terminal {
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false
override def getHeight: Int = 0
override def getLastLine: Option[String] = None
override def getLines: Seq[String] = Nil
override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0)
override def getNumericCapability(capability: String, jline3: Boolean): Integer = null
override def getStringCapability(capability: String, jline3: Boolean): String = null

View File

@ -35,6 +35,7 @@ import org.scalasbt.ipcsocket.UnixDomainSocket;
import org.scalasbt.ipcsocket.Win32NamedPipeServerSocket;
import org.scalasbt.ipcsocket.Win32NamedPipeSocket;
import org.scalasbt.ipcsocket.Win32SecurityLevel;
import sbt.internal.util.Terminal;
import xsbti.AppConfiguration;
/**
@ -102,6 +103,16 @@ public class BootServerSocket implements AutoCloseable {
service.submit(
() -> {
try {
Terminal.console()
.getLines()
.foreach(
l -> {
try {
write((l + System.lineSeparator()).getBytes("UTF-8"));
} catch (final IOException e) {
}
return 0;
});
final InputStream inputStream = socket.getInputStream();
while (alive.get()) {
try {
@ -134,6 +145,15 @@ public class BootServerSocket implements AutoCloseable {
}
}
private void write(final byte[] b) {
try {
if (alive.get()) socket.getOutputStream().write(b);
} catch (final IOException e) {
alive.set(false);
close();
}
}
private void write(final byte[] b, final int offset, final int len) {
try {
if (alive.get()) socket.getOutputStream().write(b, offset, len);