Upgrade LineReader to JLine3

This commit upgrades sbt to using jline3. The advantage to jline3 is
that it has a significantly better tab completion engine that is more
similar to what you get from zsh or fish.

The diff is bigger than I'd hoped because there are a number of
behaviors that are different in jline3 vs jline2 in how the library
consumes input streams and implements various features. I also was
unable to remove jline2 because we need it for older versions of the
scala console to work correctly with the thin client. As a result, the
changes are largely additive.

A good amount of this commit was in adding more protocol so that the
remote client can forward its jline3 terminal information to the server.

There were a number of minor changes that I made that either fixed
outstanding ui bugs from #5620 or regressions due to differences between
jline3 and jline2.

The number one thing that caused problems is that the jline3 LineReader
insists on using a NonBlockingInputStream. The implementation ofo
NonBlockingInputStream seems buggy. Moreover, sbt internally uses a
non blocking input stream for system in so jline is adding non blocking
to an already non blocking stream, which is frustrating.

A long term solution might be to consider insourcing LineReader.java
from jline3 and just adapting it to use an sbt terminal rather than
fighting with the jline3 api. This would also have the advantage of not
conflicting with other versions of jline3. Even if we don't, we may want to
shade jline3 if that is possible.
This commit is contained in:
Ethan Atkins 2020-06-30 08:57:57 -07:00
parent ed4d40d3e2
commit 2ecf5967ee
40 changed files with 1330 additions and 309 deletions

View File

@ -289,6 +289,7 @@ val completeProj = (project in file("internal") / "util-complete")
testedBaseSettings, testedBaseSettings,
name := "Completion", name := "Completion",
libraryDependencies += jline, libraryDependencies += jline,
libraryDependencies += jline3,
mimaSettings, mimaSettings,
// Parser is used publicly, so we can't break bincompat. // Parser is used publicly, so we can't break bincompat.
mimaBinaryIssueFilters := Seq( mimaBinaryIssueFilters := Seq(
@ -343,12 +344,20 @@ lazy val utilPosition = (project in file("internal") / "util-position")
lazy val utilLogging = (project in file("internal") / "util-logging") lazy val utilLogging = (project in file("internal") / "util-logging")
.enablePlugins(ContrabandPlugin, JsonCodecPlugin) .enablePlugins(ContrabandPlugin, JsonCodecPlugin)
.dependsOn(utilInterface) .dependsOn(utilInterface, collectionProj)
.settings( .settings(
utilCommonSettings, utilCommonSettings,
name := "Util Logging", name := "Util Logging",
libraryDependencies ++= libraryDependencies ++=
Seq(jline, log4jApi, log4jCore, disruptor, sjsonNewScalaJson.value, scalaReflect.value), Seq(
jline,
jline3,
log4jApi,
log4jCore,
disruptor,
sjsonNewScalaJson.value,
scalaReflect.value
),
libraryDependencies ++= Seq(scalacheck % "test", scalatest % "test"), libraryDependencies ++= Seq(scalacheck % "test", scalatest % "test"),
libraryDependencies ++= (scalaVersion.value match { libraryDependencies ++= (scalaVersion.value match {
case v if v.startsWith("2.12.") => List(compilerPlugin(silencerPlugin)) case v if v.startsWith("2.12.") => List(compilerPlugin(silencerPlugin))
@ -1047,8 +1056,7 @@ lazy val sbtClientProj = (project in file("client"))
crossPaths := false, crossPaths := false,
exportJars := true, exportJars := true,
libraryDependencies += jansi, libraryDependencies += jansi,
libraryDependencies += "net.java.dev.jna" % "jna" % "5.5.0", libraryDependencies += jline3Jansi,
libraryDependencies += "net.java.dev.jna" % "jna-platform" % "5.5.0",
libraryDependencies += scalatest % "test", libraryDependencies += scalatest % "test",
/* /*
* On windows, the raw classpath is too large to be a command argument to an * On windows, the raw classpath is too large to be a command argument to an

View File

@ -1,7 +1,20 @@
{ {
"resources":[ "resources":[
{"pattern":"jline/console/completer/CandidateListCompletionHandler.properties"}, {"pattern":"jline/console/completer/CandidateListCompletionHandler.properties"},
{"pattern":"library.properties"}, {"pattern":"org/jline/utils/ansi.caps"},
{"pattern":"org/jline/utils/capabilities.txt"},
{"pattern":"org/jline/utils/colors.txt"},
{"pattern":"org/jline/utils/dumb-color.caps"},
{"pattern":"org/jline/utils/xterm.caps"},
{"pattern":"org/jline/utils/xterm-256color.caps"},
{"pattern":"org/jline/utils/windows-256color.caps"},
{"pattern":"org/jline/utils/screen-256color.caps"},
{"pattern":"org/jline/utils/windows.caps"},
{"pattern":"org/jline/utils/windows-conemu.caps"},
{"pattern":"org/jline/utils/dumb.caps"},
{"pattern":"org/jline/utils/windows-vtp.caps"},
{"pattern":"org/jline/utils/screen.caps"},
{"pattern":"library.properties"},
{"pattern":"darwin/x86_64/libsbtipcsocket.dylib"}, {"pattern":"darwin/x86_64/libsbtipcsocket.dylib"},
{"pattern":"linux/x86_64/libsbtipcsocket.so"}, {"pattern":"linux/x86_64/libsbtipcsocket.so"},
{"pattern":"win32/x86_64/sbtipcsocket.dll"} {"pattern":"win32/x86_64/sbtipcsocket.dll"}

View File

@ -8,16 +8,28 @@
package sbt.internal.util package sbt.internal.util
import java.io._ import java.io._
import java.util.{ List => JList }
import jline.console.ConsoleReader import jline.console.ConsoleReader
import jline.console.history.{ FileHistory, MemoryHistory } import jline.console.history.{ FileHistory, MemoryHistory }
import org.jline.reader.{
Candidate,
Completer,
EndOfFileException,
LineReader => JLineReader,
LineReaderBuilder,
ParsedLine,
UserInterruptException,
}
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent.duration._ import scala.concurrent.duration._
import java.nio.channels.ClosedByInterruptException
trait LineReader { trait LineReader extends AutoCloseable {
def readLine(prompt: String, mask: Option[Char] = None): Option[String] def readLine(prompt: String, mask: Option[Char] = None): Option[String]
override def close(): Unit = {}
} }
object LineReader { object LineReader {
@ -25,7 +37,67 @@ object LineReader {
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT) !java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
val MaxHistorySize = 500 val MaxHistorySize = 500
private def completer(parser: Parser[_]): Completer = new Completer {
def complete(lr: JLineReader, pl: ParsedLine, candidates: JList[Candidate]): Unit = {
Parser.completions(parser, pl.line(), 10).get.foreach { c =>
/*
* For commands like `~` that delegate parsing to another parser, the `~` may be
* excluded from the completion result. For example,
* ~testOnly <TAB>
* might return results like
* 'testOnly ;'
* 'testOnly com.foo.FooSpec'
* ...
* If we use the raw display, JLine will reject the completions because they are
* missing the leading `~`. To workaround this, we append to the result to the
* line provided the line does not end with " ". This fixes the missing `~` in
* the prefix problem. We also need to split the line on space and take the
* last token and append to that otherwise the completion will double print
* the prefix, so that `testOnly com<Tab>` might expand to something like:
* `testOnly testOnly\ com.foo.FooSpec` instead of `testOnly com.foo.FooSpec`.
*/
if (c.append.nonEmpty) {
if (!pl.line().endsWith(" ")) {
candidates.add(new Candidate(pl.line().split(" ").last + c.append))
} else {
candidates.add(new Candidate(c.append))
}
}
}
}
}
def createReader( def createReader(
historyPath: Option[File],
parser: Parser[_],
terminal: Terminal,
prompt: Prompt = Prompt.Running,
): LineReader = {
val term = JLine3(terminal)
// We may want to consider insourcing LineReader.java from jline. We don't otherwise
// directly need jline3 for sbt.
val reader = LineReaderBuilder.builder().terminal(term).completer(completer(parser)).build()
historyPath.foreach(f => reader.setVariable(JLineReader.HISTORY_FILE, f))
new LineReader {
override def readLine(prompt: String, mask: Option[Char]): Option[String] = {
try terminal.withRawSystemIn {
Option(mask.map(reader.readLine(prompt, _)).getOrElse(reader.readLine(prompt)))
} catch {
case e: EndOfFileException =>
if (terminal == Terminal.console && System.console == null) None
else Some("exit")
case _: IOError => Some("exit")
case _: UserInterruptException | _: ClosedByInterruptException |
_: UncheckedIOException =>
throw new InterruptedException
} finally {
terminal.prompt.reset()
term.close()
}
}
}
}
def createJLine2Reader(
historyPath: Option[File], historyPath: Option[File],
terminal: Terminal, terminal: Terminal,
prompt: Prompt = Prompt.Running, prompt: Prompt = Prompt.Running,
@ -42,7 +114,6 @@ object LineReader {
cr.setHistoryEnabled(true) cr.setHistoryEnabled(true)
cr cr
} }
def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal) def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal)
def simple( def simple(
historyPath: Option[File], historyPath: Option[File],
@ -230,7 +301,7 @@ final class FullReader(
Terminal.console Terminal.console
) )
protected[this] val reader: ConsoleReader = { protected[this] val reader: ConsoleReader = {
val cr = LineReader.createReader(historyPath, terminal) val cr = LineReader.createJLine2Reader(historyPath, terminal)
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete) sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
cr cr
} }
@ -244,7 +315,7 @@ class SimpleReader private[sbt] (
def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) = def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
this(historyPath, handleCONT, Terminal.console) this(historyPath, handleCONT, Terminal.console)
protected[this] val reader: ConsoleReader = protected[this] val reader: ConsoleReader =
LineReader.createReader(historyPath, terminal) LineReader.createJLine2Reader(historyPath, terminal)
} }
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) { object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) {

View File

@ -9,6 +9,7 @@ package sbt.internal.util
import java.io.{ PrintStream, PrintWriter } import java.io.{ PrintStream, PrintWriter }
import java.lang.StringBuilder import java.lang.StringBuilder
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import org.apache.logging.log4j.core.appender.AbstractAppender import org.apache.logging.log4j.core.appender.AbstractAppender
@ -394,7 +395,8 @@ class ConsoleAppender private[ConsoleAppender] (
override def append(event: XLogEvent): Unit = { override def append(event: XLogEvent): Unit = {
val level = ConsoleAppender.toLevel(event.getLevel) val level = ConsoleAppender.toLevel(event.getLevel)
val message = event.getMessage val message = event.getMessage
appendMessage(level, message) try appendMessage(level, message)
catch { case _: ClosedChannelException => }
} }
/** /**

View File

@ -181,9 +181,10 @@ object EscHelpers {
else res(index) = 32 else res(index) = 32
case 'm' => case 'm' =>
case ';' => state = csi case ';' => state = csi
case _ => case b => state = csi
} }
digit.clear() digit.clear()
case b if state == esc => state = 0
case b => case b =>
res(index) = b res(index) = b
index += 1 index += 1

View File

@ -0,0 +1,223 @@
/*
* 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.{ EOFException, InputStream, OutputStream, PrintWriter }
import java.nio.charset.Charset
import java.util.{ Arrays, EnumSet }
import java.util.concurrent.atomic.AtomicBoolean
import org.jline.utils.InfoCmp.Capability
import org.jline.utils.{ NonBlocking, OSUtils }
import org.jline.terminal.{ Attributes, Size, Terminal => JTerminal }
import org.jline.terminal.Terminal.SignalHandler
import org.jline.terminal.impl.AbstractTerminal
import org.jline.terminal.impl.jansi.JansiSupportImpl
import org.jline.terminal.impl.jansi.win.JansiWinSysTerminal
import scala.collection.JavaConverters._
import scala.util.Try
private[util] object JLine3 {
private val capabilityMap = Capability
.values()
.map { c =>
c.toString -> c
}
.toMap
private[util] def system = {
/*
* For reasons that are unclear to me, TerminalBuilder fails to build
* windows terminals. The instructions about the classpath did not work:
* https://stackoverflow.com/questions/52851232/jline3-issues-with-windows-terminal
* We can deconstruct what TerminalBuilder does and inline it for now.
* It is possible that this workaround will break WSL but I haven't checked that.
*/
if (Util.isNonCygwinWindows) {
val support = new JansiSupportImpl
val winConsole = support.isWindowsConsole();
try {
val term = JansiWinSysTerminal.createTerminal(
"console",
"ansi",
OSUtils.IS_CONEMU,
Charset.forName("UTF-8"),
-1,
false,
SignalHandler.SIG_DFL,
true
)
term.disableScrolling()
term
} catch {
case _: Exception =>
org.jline.terminal.TerminalBuilder
.builder()
.system(false)
.paused(true)
.jansi(true)
.streams(Terminal.console.inputStream, Terminal.console.outputStream)
.build()
}
} else {
org.jline.terminal.TerminalBuilder
.builder()
.system(System.console != null)
.paused(true)
.jna(false)
.jansi(true)
.build()
}
}
private[sbt] def apply(term: Terminal): JTerminal = {
new AbstractTerminal(term.name, "ansi", Charset.forName("UTF-8"), SignalHandler.SIG_DFL) {
val closed = new AtomicBoolean(false)
setOnClose { () =>
if (closed.compareAndSet(false, true)) {
// This is necessary to shutdown the non blocking input reader
// so that it doesn't keep blocking
term.inputStream match {
case w: Terminal.WriteableInputStream => w.cancel()
case _ =>
}
}
}
parseInfoCmp()
override val input: InputStream = new InputStream {
override def read: Int = {
val res = try term.inputStream.read
catch { case _: InterruptedException => -2 }
if (res == 4 && term.prompt.render().endsWith(term.prompt.mkPrompt()))
throw new EOFException
res
}
}
override val output: OutputStream = new OutputStream {
override def write(b: Int): Unit = write(Array[Byte](b.toByte))
override def write(b: Array[Byte]): Unit = if (!closed.get) term.withPrintStream { ps =>
term.prompt match {
case a: Prompt.AskUser => a.write(b)
case _ =>
}
ps.write(b)
}
override def write(b: Array[Byte], offset: Int, len: Int) =
write(Arrays.copyOfRange(b, offset, offset + len))
override def flush(): Unit = term.withPrintStream(_.flush())
}
override val reader =
NonBlocking.nonBlocking(term.name, input, Charset.defaultCharset())
override val writer: PrintWriter = new PrintWriter(output, true)
/*
* For now assume that the terminal capabilities for client and server
* are the same.
*/
override def getStringCapability(cap: Capability): String = {
term.getStringCapability(cap.toString, jline3 = true)
}
override def getNumericCapability(cap: Capability): Integer = {
term.getNumericCapability(cap.toString, jline3 = true)
}
override def getBooleanCapability(cap: Capability): Boolean = {
term.getBooleanCapability(cap.toString, jline3 = true)
}
def getAttributes(): Attributes = attributesFromMap(term.getAttributes)
def getSize(): Size = new Size(term.getWidth, term.getHeight)
def setAttributes(a: Attributes): Unit = term.setAttributes(toMap(a))
def setSize(size: Size): Unit = term.setSize(size.getColumns, size.getRows)
/**
* Override enterRawMode because the default implementation modifies System.in
* to be non-blocking which means it immediately returns -1 if there is no
* data available, which is not desirable for us.
*/
override def enterRawMode(): Attributes = enterRawModeImpl(this)
}
}
private def enterRawModeImpl(term: JTerminal): Attributes = {
val prvAttr = term.getAttributes()
val newAttr = new Attributes(prvAttr)
newAttr.setLocalFlags(
EnumSet
.of(Attributes.LocalFlag.ICANON, Attributes.LocalFlag.ECHO, Attributes.LocalFlag.IEXTEN),
false
)
newAttr.setInputFlags(
EnumSet
.of(Attributes.InputFlag.IXON, Attributes.InputFlag.ICRNL, Attributes.InputFlag.INLCR),
false
)
term.setAttributes(newAttr)
prvAttr
}
private[util] def enterRawMode(term: JTerminal): Map[String, String] =
toMap(enterRawModeImpl(term))
private[util] def toMap(jattributes: Attributes): Map[String, String] = {
val result = new java.util.LinkedHashMap[String, String]
result.put(
"iflag",
jattributes.getInputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"oflag",
jattributes.getOutputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"cflag",
jattributes.getControlFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"lflag",
jattributes.getLocalFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ")
)
result.put(
"cchars",
jattributes.getControlChars.entrySet.iterator.asScala
.map { e =>
s"${e.getKey.name.toLowerCase},${e.getValue}"
}
.mkString(" ")
)
result.asScala.toMap
}
private[this] val iflagMap: Map[String, Attributes.InputFlag] =
Attributes.InputFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val oflagMap: Map[String, Attributes.OutputFlag] =
Attributes.OutputFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val cflagMap: Map[String, Attributes.ControlFlag] =
Attributes.ControlFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val lflagMap: Map[String, Attributes.LocalFlag] =
Attributes.LocalFlag.values.map(f => f.name.toLowerCase -> f).toMap
private[this] val charMap: Map[String, Attributes.ControlChar] =
Attributes.ControlChar.values().map(f => f.name.toLowerCase -> f).toMap
private[util] def attributesFromMap(map: Map[String, String]): Attributes = {
val attributes = new Attributes
map.get("iflag").foreach { flags =>
flags.split(" ").foreach(f => iflagMap.get(f).foreach(attributes.setInputFlag(_, true)))
}
map.get("oflag").foreach { flags =>
flags.split(" ").foreach(f => oflagMap.get(f).foreach(attributes.setOutputFlag(_, true)))
}
map.get("cflag").foreach { flags =>
flags.split(" ").foreach(f => cflagMap.get(f).foreach(attributes.setControlFlag(_, true)))
}
map.get("lflag").foreach { flags =>
flags.split(" ").foreach(f => lflagMap.get(f).foreach(attributes.setLocalFlag(_, true)))
}
map.get("cchars").foreach { chars =>
chars.split(" ").foreach { keyValue =>
keyValue.split(",") match {
case Array(k, v) =>
Try(v.toInt).foreach(i => charMap.get(k).foreach(c => attributes.setControlChar(c, i)))
case _ =>
}
}
}
attributes
}
}

View File

@ -14,7 +14,6 @@ import sbt.internal.util.ConsoleAppender.{
ClearScreenAfterCursor, ClearScreenAfterCursor,
CursorLeft1000, CursorLeft1000,
DeleteLine, DeleteLine,
cursorLeft,
cursorUp, cursorUp,
} }
@ -33,6 +32,10 @@ private[sbt] final class ProgressState(
blankZone, blankZone,
new AtomicReference(new ArrayBuffer[Byte]), new AtomicReference(new ArrayBuffer[Byte]),
) )
def currentLine: Option[String] =
new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption
.map(EscHelpers.stripColorsAndMoves)
.filter(_.nonEmpty)
def reset(): Unit = { def reset(): Unit = {
progressLines.set(Nil) progressLines.set(Nil)
padding.set(0) padding.set(0)
@ -44,8 +47,9 @@ private[sbt] final class ProgressState(
currentLineBytes.set(new ArrayBuffer[Byte]) currentLineBytes.set(new ArrayBuffer[Byte])
} }
private[util] def addBytes(terminal: Terminal, bytes: ArrayBuffer[Byte]): Unit = { private[this] val lineSeparatorBytes: Array[Byte] = System.lineSeparator.getBytes("UTF-8")
val previous = currentLineBytes.get private[util] def addBytes(terminal: Terminal, bytes: Seq[Byte]): Unit = {
val previous: ArrayBuffer[Byte] = currentLineBytes.get
val padding = this.padding.get val padding = this.padding.get
val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0 val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0
previous ++= bytes previous ++= bytes
@ -54,6 +58,16 @@ private[sbt] final class ProgressState(
val diff = newLineCount - prevLineCount val diff = newLineCount - prevLineCount
this.padding.set(math.max(padding - diff, 0)) this.padding.set(math.max(padding - diff, 0))
} }
val lines = new String(previous.toArray, "UTF-8")
if (lines.contains(System.lineSeparator)) {
currentLineBytes.set(new ArrayBuffer[Byte])
if (!lines.endsWith(System.lineSeparator)) {
lines
.split(System.lineSeparator)
.lastOption
.foreach(currentLineBytes.get ++= _.getBytes("UTF-8"))
}
}
} }
private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit = private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit =
@ -62,31 +76,50 @@ private[sbt] final class ProgressState(
val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes
pmpt.foreach(b => printStream.write(b & 0xFF)) pmpt.foreach(b => printStream.write(b & 0xFF))
} }
private[util] def reprint(terminal: Terminal, printStream: PrintStream): Unit = { private[util] def write(
printPrompt(terminal, printStream) terminal: Terminal,
if (progressLines.get.nonEmpty) { bytes: Array[Byte],
val lines = printProgress(terminal, terminal.getLastLine.getOrElse("")) printStream: PrintStream,
printStream.print(ClearScreenAfterCursor + lines) hasProgress: Boolean
} ): Unit = {
addBytes(terminal, bytes)
if (hasProgress && terminal.prompt != Prompt.Loading) {
terminal.prompt match {
case a: Prompt.AskUser if a.render.nonEmpty =>
printStream.print(System.lineSeparator + ClearScreenAfterCursor + CursorLeft1000)
printStream.flush()
case _ =>
}
printStream.write(bytes)
printStream.write(ClearScreenAfterCursor.getBytes("UTF-8"))
printStream.flush()
if (bytes.endsWith(lineSeparatorBytes)) {
if (progressLines.get.nonEmpty) {
val lastLine = terminal.prompt match {
case a: Prompt.AskUser => a.render()
case _ => currentLine.getOrElse("")
}
val lines = printProgress(terminal, lastLine)
printStream.print(ClearScreenAfterCursor + lines)
}
}
printPrompt(terminal, printStream)
} else printStream.write(bytes)
} }
private[util] def printProgress( private[util] def printProgress(terminal: Terminal, lastLine: String): String = {
terminal: Terminal,
lastLine: String
): String = {
val previousLines = progressLines.get val previousLines = progressLines.get
if (previousLines.nonEmpty) { if (previousLines.nonEmpty) {
val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_)) val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val (height, width) = terminal.getLineHeightAndWidth(lastLine) val (height, width) = terminal.getLineHeightAndWidth(lastLine)
val left = cursorLeft(1000) // resets the position to the left
val offset = width > 0 val offset = width > 0
val pad = math.max(padding.get - height, 0) val pad = math.max(padding.get - height, 0)
val start = (if (offset) "\n" else "") val start = (if (offset) s"\n$CursorLeft1000" else "")
val totalSize = currentLength + blankZone + pad val totalSize = currentLength + blankZone + pad
val blank = left + s"\n$DeleteLine" * (totalSize - currentLength) val blank = CursorLeft1000 + s"\n$DeleteLine" * (totalSize - currentLength)
val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine") val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine")
val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0)) val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0))
val resetCursor = resetCursorUp + left + lastLine val resetCursor = resetCursorUp + CursorLeft1000 + lastLine
start + blank + lines + resetCursor start + blank + lines + resetCursor
} else { } else {
ClearScreenAfterCursor ClearScreenAfterCursor
@ -108,6 +141,7 @@ private[sbt] object ProgressState {
terminal: Terminal terminal: Terminal
): Unit = { ): Unit = {
val state = terminal.progressState val state = terminal.progressState
val isAskUser = terminal.prompt.isInstanceOf[Prompt.AskUser]
val isRunning = terminal.prompt == Prompt.Running val isRunning = terminal.prompt == Prompt.Running
val isBatch = terminal.prompt == Prompt.Batch val isBatch = terminal.prompt == Prompt.Batch
val isWatch = terminal.prompt == Prompt.Watch val isWatch = terminal.prompt == Prompt.Watch
@ -115,31 +149,27 @@ private[sbt] object ProgressState {
if (terminal.isSupershellEnabled) { if (terminal.isSupershellEnabled) {
if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) { if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) {
terminal.withPrintStream { ps => terminal.withPrintStream { ps =>
val info = val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name)
if ((isRunning || isBatch || noPrompt) && pe.channelName val info = if ((isRunning || isBatch || noPrompt) && commandFromThisTerminal) {
.fold(true)(_ == terminal.name)) { pe.items.map { item =>
pe.items.map { item => val elapsed = item.elapsedMicros / 1000000L
val elapsed = item.elapsedMicros / 1000000L s" | => ${item.name} ${elapsed}s"
s" | => ${item.name} ${elapsed}s"
}
} else {
pe.command.toSeq.flatMap { cmd =>
val tail = if (isWatch) Nil else "enter 'cancel' to stop evaluation" :: Nil
s"sbt server is running '$cmd'" :: tail
}
} }
} else {
pe.command.toSeq.flatMap { cmd =>
val tail = if (isWatch) Nil else "enter 'cancel' to stop evaluation" :: Nil
s"sbt server is running '$cmd'" :: tail
}
}
val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_)) val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_))
val previousLines = state.progressLines.getAndSet(info) val previousLines = state.progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_)) 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 prevSize = prevLength + state.padding.get
val newPadding = math.max(0, prevSize - currentLength) val lastLine =
state.padding.set(newPadding) if (isAskUser) terminal.prompt.render() else terminal.getLastLine.getOrElse("")
state.padding.set(math.max(0, prevSize - currentLength))
state.printPrompt(terminal, ps) state.printPrompt(terminal, ps)
ps.print(state.printProgress(terminal, lastLine)) ps.print(state.printProgress(terminal, lastLine))
ps.flush() ps.flush()

View File

@ -7,39 +7,29 @@
package sbt.internal.util package sbt.internal.util
import java.io.OutputStream
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
private[sbt] sealed trait Prompt { private[sbt] sealed trait Prompt {
def mkPrompt: () => String def mkPrompt: () => String
def render(): String def render(): String
def wrappedOutputStream(terminal: Terminal): OutputStream def reset(): Unit
} }
private[sbt] object Prompt { private[sbt] object Prompt {
private[sbt] case class AskUser(override val mkPrompt: () => String) extends Prompt { private[sbt] case class AskUser(override val mkPrompt: () => String) extends Prompt {
private[this] val bytes = new LinkedBlockingQueue[Int] private[this] val bytes = new LinkedBlockingQueue[Byte]
override def wrappedOutputStream(terminal: Terminal): OutputStream = new OutputStream { def write(b: Array[Byte]): Unit = b.foreach(bytes.put)
override def write(b: Int): Unit = { override def render(): String = {
if (b == 10) bytes.clear() val res = new String(bytes.asScala.toArray, "UTF-8")
else bytes.put(b) if (res.endsWith(System.lineSeparator)) "" else res
terminal.withPrintStream { p =>
p.write(b)
p.flush()
}
}
override def flush(): Unit = terminal.withPrintStream(_.flush())
} }
override def reset(): Unit = bytes.clear()
override def render(): String =
EscHelpers.stripMoves(new String(bytes.asScala.toArray.map(_.toByte)))
} }
private[sbt] trait NoPrompt extends Prompt { private[sbt] trait NoPrompt extends Prompt {
override val mkPrompt: () => String = () => "" override val mkPrompt: () => String = () => ""
override def render(): String = "" override def render(): String = ""
override def wrappedOutputStream(terminal: Terminal): OutputStream = terminal.outputStream override def reset(): Unit = {}
} }
private[sbt] case object Running extends NoPrompt private[sbt] case object Running extends NoPrompt
private[sbt] case object Batch extends NoPrompt private[sbt] case object Batch extends NoPrompt

View File

@ -7,19 +7,17 @@
package sbt.internal.util package sbt.internal.util
import java.io.{ InputStream, OutputStream, PrintStream } import java.io.{ InputStream, InterruptedIOException, OutputStream, PrintStream }
import java.nio.channels.ClosedChannelException import java.nio.channels.ClosedChannelException
import java.util.Locale import java.util.{ Arrays, Locale }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.{ ConcurrentHashMap, Executors, LinkedBlockingQueue, TimeUnit } import java.util.concurrent.{ ArrayBlockingQueue, Executors, LinkedBlockingQueue, TimeUnit }
import jline.DefaultTerminal2 import jline.DefaultTerminal2
import jline.console.ConsoleReader import jline.console.ConsoleReader
import sbt.internal.util.ConsoleAppender.{ ClearScreenAfterCursor, CursorLeft1000 }
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.mutable.ArrayBuffer
import scala.util.Try import scala.util.Try
import scala.util.control.NonFatal
trait Terminal extends AutoCloseable { trait Terminal extends AutoCloseable {
@ -111,9 +109,12 @@ trait Terminal extends AutoCloseable {
*/ */
private[sbt] def getLastLine: Option[String] private[sbt] def getLastLine: Option[String]
private[sbt] def getBooleanCapability(capability: String): Boolean private[sbt] def getBooleanCapability(capability: String, jline3: Boolean): Boolean
private[sbt] def getNumericCapability(capability: String): Int private[sbt] def getNumericCapability(capability: String, jline3: Boolean): Integer
private[sbt] def getStringCapability(capability: String): String private[sbt] def getStringCapability(capability: String, jline3: Boolean): String
private[sbt] def getAttributes: Map[String, String]
private[sbt] def setAttributes(attributes: Map[String, String]): Unit
private[sbt] def setSize(width: Int, height: Int): Unit
private[sbt] def name: String private[sbt] def name: String
private[sbt] def withRawSystemIn[T](f: => T): T = f private[sbt] def withRawSystemIn[T](f: => T): T = f
@ -142,7 +143,8 @@ trait Terminal extends AutoCloseable {
val len = l.length val len = l.length
if (width > 0 && len > 0) (len - 1 + width) / width else 0 if (width > 0 && len > 0) (len - 1 + width) / width else 0
} }
lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_)) if (lines.nonEmpty) lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_))
else 0
} }
} }
@ -188,13 +190,13 @@ object Terminal {
override def enableInterruptCharacter(): Unit = {} override def enableInterruptCharacter(): Unit = {}
override def getOutputEncoding: String = null override def getOutputEncoding: String = null
override def getBooleanCapability(capability: String): Boolean = { override def getBooleanCapability(capability: String): Boolean = {
term.getBooleanCapability(capability) term.getBooleanCapability(capability, jline3 = false)
} }
override def getNumericCapability(capability: String): Integer = { override def getNumericCapability(capability: String): Integer = {
term.getNumericCapability(capability) term.getNumericCapability(capability, jline3 = false)
} }
override def getStringCapability(capability: String): String = { override def getStringCapability(capability: String): String = {
term.getStringCapability(capability) term.getStringCapability(capability, jline3 = false)
} }
} }
} }
@ -253,18 +255,45 @@ object Terminal {
*/ */
private[sbt] def restore(): Unit = console.toJLine.restore() private[sbt] def restore(): Unit = console.toJLine.restore()
private[this] val hasProgress: AtomicBoolean = new AtomicBoolean(false)
/** /**
* *
* @param progress toggles whether or not the console terminal has progress
* @param f the thunk to run * @param f the thunk to run
* @tparam T the result type of the thunk * @tparam T the result type of the thunk
* @return the result of the thunk * @return the result of the thunk
*/ */
private[sbt] def withStreams[T](f: => T): T = private[sbt] def withStreams[T](isServer: Boolean)(f: => T): T =
if (System.getProperty("sbt.io.virtual", "true") == "true") { if (System.getProperty("sbt.io.virtual", "true") == "true") {
hasProgress.set(isServer)
try withOut(withIn(f)) try withOut(withIn(f))
finally { finally {
jline.TerminalFactory.reset() jline.TerminalFactory.reset()
console.close() if (isServer) {
console match {
case c: ConsoleTerminal if !isWindows =>
/*
* Entering raw mode in this way causes the standard in InputStream
* to become non-blocking. After we set it to non-blocking, we spin
* up a thread that reads from the inputstream and the resets it
* back to blocking mode. We can then close the console. We do
* this on a background thread to avoid blocking sbt's exit.
*/
val prev = c.system.enterRawMode()
val runnable: Runnable = () => {
c.inputStream.read()
c.system.setAttributes(prev)
c.close()
}
val thread = new Thread(runnable, "sbt-console-background-close")
thread.setDaemon(true)
thread.start()
case c => c.close()
}
} else {
console.close()
}
} }
} else f } else f
@ -281,10 +310,16 @@ object Terminal {
override def isEchoEnabled: Boolean = t.isEchoEnabled override def isEchoEnabled: Boolean = t.isEchoEnabled
override def isSuccessEnabled: Boolean = t.isSuccessEnabled override def isSuccessEnabled: Boolean = t.isSuccessEnabled
override def isSupershellEnabled: Boolean = t.isSupershellEnabled override def isSupershellEnabled: Boolean = t.isSupershellEnabled
override def getBooleanCapability(capability: String): Boolean = override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
t.getBooleanCapability(capability) t.getBooleanCapability(capability, jline3)
override def getNumericCapability(capability: String): Int = t.getNumericCapability(capability) override def getNumericCapability(capability: String, jline3: Boolean): Integer =
override def getStringCapability(capability: String): String = t.getStringCapability(capability) t.getNumericCapability(capability, jline3)
override def getStringCapability(capability: String, jline3: Boolean): String =
t.getStringCapability(capability, jline3)
override private[sbt] def getAttributes: Map[String, String] = t.getAttributes
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit =
t.setAttributes(attributes)
override private[sbt] def setSize(width: Int, height: Int): Unit = t.setSize(width, height)
override def withRawSystemIn[T](f: => T): T = t.withRawSystemIn(f) override def withRawSystemIn[T](f: => T): T = t.withRawSystemIn(f)
override def withCanonicalIn[T](f: => T): T = t.withCanonicalIn(f) override def withCanonicalIn[T](f: => T): T = t.withCanonicalIn(f)
override def printStream: PrintStream = t.printStream override def printStream: PrintStream = t.printStream
@ -322,18 +357,35 @@ object Terminal {
} }
} }
private[this] val originalOut = System.out val sepBytes = System.lineSeparator.getBytes("UTF-8")
private class LinePrintStream(outputStream: OutputStream)
extends PrintStream(outputStream, true) {
override def println(s: String): Unit = synchronized {
out.write(s.getBytes("UTF-8") ++ sepBytes)
out.flush()
}
}
private[this] val originalOut = new LinePrintStream(System.out)
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
with AutoCloseable { with AutoCloseable {
final def write(bytes: Int*): Unit = bytes.foreach(i => buffer.put(i)) final def write(bytes: Int*): Unit = waiting.synchronized {
waiting.poll match {
case null =>
bytes.foreach(b => buffer.put(b))
case w =>
if (bytes.length > 1) bytes.tail.foreach(b => buffer.put(b))
bytes.headOption.foreach(b => w.put(b))
}
}
private[this] val executor = private[this] val executor =
Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader")) Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader"))
private[this] val buffer = new LinkedBlockingQueue[Integer] private[this] val buffer = new LinkedBlockingQueue[Integer]
private[this] val closed = new AtomicBoolean(false) private[this] val closed = new AtomicBoolean(false)
private[this] val resultQueue = new LinkedBlockingQueue[LinkedBlockingQueue[Int]] private[this] val readQueue = new LinkedBlockingQueue[Unit]
private[this] val waiting = ConcurrentHashMap.newKeySet[LinkedBlockingQueue[Int]] private[this] val waiting = new ArrayBlockingQueue[LinkedBlockingQueue[Integer]](1)
private[this] val readThread = new AtomicReference[Thread]
/* /*
* Starts a loop that waits for consumers of the InputStream to call read. * Starts a loop that waits for consumers of the InputStream to call read.
* When read is called, we enqueue a `LinkedBlockingQueue[Int]` to which * When read is called, we enqueue a `LinkedBlockingQueue[Int]` to which
@ -354,11 +406,14 @@ object Terminal {
*/ */
private[this] val runnable: Runnable = () => { private[this] val runnable: Runnable = () => {
@tailrec def impl(): Unit = { @tailrec def impl(): Unit = {
val result = resultQueue.take val _ = readQueue.take
val b = in.read val b = in.read
// The downstream consumer may have been interrupted. Buffer the result // The downstream consumer may have been interrupted. Buffer the result
// when that hapens. // when that hapens.
if (waiting.contains(result)) result.put(b) else buffer.put(b) waiting.poll match {
case null => buffer.put(b)
case q => q.put(b)
}
if (b != -1 && !Thread.interrupted()) impl() if (b != -1 && !Thread.interrupted()) impl()
else closed.set(true) else closed.set(true)
} }
@ -370,21 +425,28 @@ object Terminal {
if (closed.get) -1 if (closed.get) -1
else else
synchronized { synchronized {
buffer.poll match { readThread.set(Thread.currentThread)
try buffer.poll match {
case null => case null =>
val result = new LinkedBlockingQueue[Int] val result = new LinkedBlockingQueue[Integer]
waiting.add(result) waiting.synchronized(waiting.put(result))
resultQueue.offer(result) readQueue.put(())
try result.take try result.take.toInt
catch { catch {
case e: InterruptedException => case e: InterruptedException =>
waiting.remove(result) waiting.remove(result)
throw e -1
} }
case b if b == -1 => throw new ClosedChannelException case b if b == -1 => throw new ClosedChannelException
case b => b case b => b.toInt
} } finally readThread.set(null)
} }
def cancel(): Unit = waiting.synchronized {
Option(readThread.getAndSet(null)).foreach(_.interrupt())
waiting.forEach(_.put(-2))
waiting.clear()
readQueue.clear()
}
override def available(): Int = { override def available(): Int = {
buffer.size buffer.size
@ -524,7 +586,7 @@ object Terminal {
} }
override def flush(): Unit = os.flush() override def flush(): Unit = os.flush()
} }
private[this] val proxyPrintStream = new PrintStream(proxyOutputStream, true) { private[this] val proxyPrintStream = new LinePrintStream(proxyOutputStream) {
override def toString: String = s"proxyPrintStream($proxyOutputStream)" override def toString: String = s"proxyPrintStream($proxyOutputStream)"
} }
private[this] lazy val isWindows = private[this] lazy val isWindows =
@ -592,9 +654,21 @@ object Terminal {
case t: jline.Terminal2 => t case t: jline.Terminal2 => t
case _ => new DefaultTerminal2(terminal) case _ => new DefaultTerminal2(terminal)
} }
override def init(): Unit = if (alive) terminal.init() override def init(): Unit =
override def restore(): Unit = if (alive) terminal.restore() if (alive)
override def reset(): Unit = if (alive) terminal.reset() try terminal.init()
catch {
case _: InterruptedException =>
}
override def restore(): Unit =
if (alive)
try terminal.restore()
catch {
case _: InterruptedException =>
}
override def reset(): Unit =
try terminal.reset()
catch { case _: InterruptedException => }
override def isSupported: Boolean = terminal.isSupported override def isSupported: Boolean = terminal.isSupported
override def getWidth: Int = props.map(_.width).getOrElse(terminal.getWidth) override def getWidth: Int = props.map(_.width).getOrElse(terminal.getWidth)
override def getHeight: Int = props.map(_.height).getOrElse(terminal.getHeight) override def getHeight: Int = props.map(_.height).getOrElse(terminal.getHeight)
@ -650,7 +724,7 @@ object Terminal {
fixTerminalProperty() fixTerminalProperty()
private[sbt] def createReader(term: Terminal, prompt: Prompt): ConsoleReader = { private[sbt] def createReader(term: Terminal, prompt: Prompt): ConsoleReader = {
new ConsoleReader(term.inputStream, prompt.wrappedOutputStream(term), term.toJLine) { new ConsoleReader(term.inputStream, term.outputStream, term.toJLine) {
override def readLine(prompt: String, mask: Character): String = override def readLine(prompt: String, mask: Character): String =
term.withRawSystemIn(super.readLine(prompt, mask)) term.withRawSystemIn(super.readLine(prompt, mask))
override def readLine(prompt: String): String = term.withRawSystemIn(super.readLine(prompt)) override def readLine(prompt: String): String = term.withRawSystemIn(super.readLine(prompt))
@ -662,6 +736,9 @@ object Terminal {
case term => term case term => term
} }
private val capabilityMap =
org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap
@deprecated("For compatibility only", "1.4.0") @deprecated("For compatibility only", "1.4.0")
private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine
private class ConsoleTerminal( private class ConsoleTerminal(
@ -669,28 +746,36 @@ object Terminal {
in: InputStream, in: InputStream,
out: OutputStream out: OutputStream
) extends TerminalImpl(in, out, "console0") { ) extends TerminalImpl(in, out, "console0") {
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 = term.getWidth override def getWidth: Int = system.getSize.getColumns
override def getHeight: Int = term.getHeight override def getHeight: Int = system.getSize.getRows
override def isAnsiSupported: Boolean = term.isAnsiSupported && !isCI override def isAnsiSupported: Boolean = term.isAnsiSupported && !isCI
override def isEchoEnabled: Boolean = term.isEchoEnabled override def isEchoEnabled: Boolean = system.echo()
override def isSuccessEnabled: Boolean = true override def isSuccessEnabled: Boolean = true
override def getBooleanCapability(capability: String): Boolean = override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
term.getBooleanCapability(capability) if (jline3) capabilityMap.get(capability).fold(false)(system.getBooleanCapability)
override def getNumericCapability(capability: String): Int = else term.getBooleanCapability(capability)
term.getNumericCapability(capability) override def getNumericCapability(capability: String, jline3: Boolean): Integer =
override def getStringCapability(capability: String): String = if (jline3) capabilityMap.get(capability).fold(null: Integer)(system.getNumericCapability)
term.getStringCapability(capability) else term.getNumericCapability(capability)
override def getStringCapability(capability: String, jline3: Boolean): String =
if (jline3) capabilityMap.get(capability).fold(null: String)(system.getStringCapability)
else term.getStringCapability(capability)
override private[sbt] def restore(): Unit = term.restore() override private[sbt] def restore(): Unit = term.restore()
override private[sbt] def getAttributes: Map[String, String] =
JLine3.toMap(system.getAttributes)
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit =
system.setAttributes(JLine3.attributesFromMap(attributes))
override private[sbt] def setSize(width: Int, height: Int): Unit =
system.setSize(new org.jline.terminal.Size(width, height))
override def withRawSystemIn[T](f: => T): T = term.synchronized { override def withRawSystemIn[T](f: => T): T = term.synchronized {
try { val prev = JLine3.enterRawMode(system)
term.init() try f
term.setEchoEnabled(false) catch { case _: InterruptedIOException => throw new InterruptedException } finally {
f setAttributes(prev)
} finally {
term.restore()
term.setEchoEnabled(true)
} }
} }
override def isColorEnabled: Boolean = override def isColorEnabled: Boolean =
@ -705,29 +790,33 @@ object Terminal {
case "true" => true case "true" => true
case _ => false case _ => false
}) })
override def close(): Unit = {
try system.close()
catch { case NonFatal(_) => }
super.close()
}
} }
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 private[sbt] val name: String override private[sbt] val name: String
) extends Terminal { ) 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 writeLock = new AnyRef
private[this] val writeableInputStream = in match { private[this] val writeableInputStream = in match {
case w: WriteableInputStream => w case w: WriteableInputStream => w
case _ => new WriteableInputStream(in, name) case _ => new WriteableInputStream(in, name)
} }
def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f
override def getLastLine: Option[String] = progressState.currentLine
private val combinedOutputStream = new OutputStream { private val combinedOutputStream = new OutputStream {
override def write(b: Int): Unit = { override def write(b: Int): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.write(b)) Option(bootOutputStreamHolder.get).foreach(_.write(b))
out.write(b) out.write(b)
} }
override def write(b: Array[Byte]): Unit = write(b, 0, b.length) override def write(b: Array[Byte]): Unit = {
write(b, 0, b.length)
}
override def write(b: Array[Byte], offset: Int, len: Int): Unit = { override def write(b: Array[Byte], offset: Int, len: Int): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.write(b, offset, len)) Option(bootOutputStreamHolder.get).foreach(_.write(b, offset, len))
out.write(b, offset, len) out.write(b, offset, len)
@ -740,54 +829,19 @@ object Terminal {
override val outputStream = new OutputStream { override val outputStream = new OutputStream {
override def write(b: Int): Unit = throwIfClosed { override def write(b: Int): Unit = throwIfClosed {
writeLock.synchronized { write(Array((b & 0xFF).toByte))
if (b == Int.MinValue) currentLine.set(new ArrayBuffer[Byte])
else doWrite(Vector((b & 0xFF).toByte))
if (b == 10) combinedOutputStream.flush()
}
} }
override def write(b: Array[Byte]): Unit = throwIfClosed(write(b, 0, b.length)) override def write(b: Array[Byte]): Unit = throwIfClosed {
override def write(b: Array[Byte], off: Int, len: Int): Unit = { writeLock.synchronized(doWrite(b))
throwIfClosed { }
writeLock.synchronized { override def write(b: Array[Byte], offset: Int, length: Int): Unit = throwIfClosed {
val lo = math.max(0, off) write(Arrays.copyOfRange(b, offset, offset + length))
val hi = math.min(math.max(off + len, 0), b.length)
doWrite(b.slice(off, off + len).toSeq)
}
}
} }
override def flush(): Unit = combinedOutputStream.flush() override def flush(): Unit = combinedOutputStream.flush()
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)
combinedOutputStream.write(buf.toArray)
combinedOutputStream.write(10)
currentLine.get match {
case s if s.nonEmpty => currentLine.set(new ArrayBuffer[Byte])
case _ =>
}
if (prompt != Prompt.Loading) 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
combinedOutputStream.write(remaining.toArray)
}
combinedOutputStream.flush()
}
} }
override private[sbt] val printStream: PrintStream = new PrintStream(outputStream, true) private def doWrite(bytes: Array[Byte]): Unit =
progressState.write(TerminalImpl.this, bytes, rawPrintStream, hasProgress.get)
override private[sbt] val printStream: PrintStream = new LinePrintStream(outputStream)
override def inputStream: InputStream = writeableInputStream override def inputStream: InputStream = writeableInputStream
private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*) private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*)
@ -801,17 +855,7 @@ object Terminal {
case _ => (0, 0) case _ => (0, 0)
} }
override def getLastLine: Option[String] = currentLine.get match { private[this] val rawPrintStream: PrintStream = new LinePrintStream(combinedOutputStream)
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(combinedOutputStream, true) {
override def close(): Unit = {}
}
override def withPrintStream[T](f: PrintStream => T): T = override def withPrintStream[T](f: PrintStream => T): T =
writeLock.synchronized(f(rawPrintStream)) writeLock.synchronized(f(rawPrintStream))
@ -821,12 +865,12 @@ object Terminal {
} }
private[sbt] val NullTerminal = new Terminal { private[sbt] val NullTerminal = new Terminal {
override def close(): Unit = {} override def close(): Unit = {}
override def getBooleanCapability(capability: String): Boolean = false override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false
override def getHeight: Int = 0 override def getHeight: Int = 0
override def getLastLine: Option[String] = None override def getLastLine: Option[String] = None
override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0) override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0)
override def getNumericCapability(capability: String): Int = -1 override def getNumericCapability(capability: String, jline3: Boolean): Integer = null
override def getStringCapability(capability: String): String = null override def getStringCapability(capability: String, jline3: Boolean): String = null
override def getWidth: Int = 0 override def getWidth: Int = 0
override def inputStream: java.io.InputStream = () => { override def inputStream: java.io.InputStream = () => {
try this.synchronized(this.wait) try this.synchronized(this.wait)
@ -839,6 +883,9 @@ 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 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 = {}
override private[sbt] def name: String = "NullTerminal" override private[sbt] def name: String = "NullTerminal"
override private[sbt] val printStream: java.io.PrintStream = override private[sbt] val printStream: java.io.PrintStream =
new PrintStream(outputStream, false) new PrintStream(outputStream, false)

View File

@ -63,4 +63,11 @@ class CleanStringSpec extends FlatSpec {
val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62)) val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62))
assert(EscHelpers.stripMoves(original) == "foo" + colorArrow + " " + scala.Console.RESET) assert(EscHelpers.stripMoves(original) == "foo" + colorArrow + " " + scala.Console.RESET)
} }
it should "remove unusual escape characters" in {
val original = new String(
Array[Byte](27, 91, 63, 49, 108, 27, 62, 27, 91, 63, 49, 48, 48, 48, 108, 27, 91, 63, 50, 48,
48, 52, 108)
)
assert(EscHelpers.stripColorsAndMoves(original).isEmpty)
}
} }

View File

@ -12,7 +12,6 @@ package client
import java.io.{ File, IOException, InputStream, PrintStream } import java.io.{ File, IOException, InputStream, PrintStream }
import java.lang.ProcessBuilder.Redirect import java.lang.ProcessBuilder.Redirect
import java.net.Socket import java.net.Socket
import java.nio.channels.ClosedChannelException
import java.nio.file.Files import java.nio.file.Files
import java.util.UUID import java.util.UUID
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
@ -43,10 +42,14 @@ import Serialization.{
promptChannel, promptChannel,
systemIn, systemIn,
systemOut, systemOut,
systemOutFlush,
terminalCapabilities, terminalCapabilities,
terminalCapabilitiesResponse, terminalCapabilitiesResponse,
terminalPropertiesQuery, terminalPropertiesQuery,
terminalPropertiesResponse terminalPropertiesResponse,
getTerminalAttributes,
setTerminalAttributes,
setTerminalSize,
} }
import NetworkClient.Arguments import NetworkClient.Arguments
@ -199,7 +202,6 @@ class NetworkClient(
case _ => (false, None) case _ => (false, None)
} }
if (rebootCommands.nonEmpty) { if (rebootCommands.nonEmpty) {
if (Terminal.console.getLastLine.isDefined) Terminal.console.printStream.println()
rebooting.set(true) rebooting.set(true)
attached.set(false) attached.set(false)
connectionHolder.getAndSet(null) match { connectionHolder.getAndSet(null) match {
@ -212,7 +214,7 @@ class NetworkClient(
rebooting.set(false) rebooting.set(false)
rebootCommands match { rebootCommands match {
case Some((execId, cmd)) if execId.nonEmpty => case Some((execId, cmd)) if execId.nonEmpty =>
if (batchMode.get && !pendingResults.contains(execId) && cmd.isEmpty) { if (batchMode.get && !pendingResults.containsKey(execId) && cmd.nonEmpty) {
console.appendLog( console.appendLog(
Level.Error, Level.Error,
s"received request to re-run unknown command '$cmd' after reboot" s"received request to re-run unknown command '$cmd' after reboot"
@ -230,8 +232,6 @@ class NetworkClient(
} else { } else {
if (!rebooting.get() && running.compareAndSet(true, false) && log) { if (!rebooting.get() && running.compareAndSet(true, false) && log) {
if (!arguments.commandArguments.contains(Shutdown)) { if (!arguments.commandArguments.contains(Shutdown)) {
if (Terminal.console.getLastLine.isDefined)
Terminal.console.printStream.println()
console.appendLog(Level.Error, "sbt server disconnected") console.appendLog(Level.Error, "sbt server disconnected")
exitClean.set(false) exitClean.set(false)
} }
@ -306,7 +306,6 @@ class NetworkClient(
Some(process) Some(process)
case _ => case _ =>
if (log) { if (log) {
if (Terminal.console.getLastLine.isDefined) Terminal.console.printStream.println()
console.appendLog(Level.Info, "sbt server is booting up") console.appendLog(Level.Info, "sbt server is booting up")
} }
None None
@ -522,17 +521,15 @@ class NetworkClient(
} }
} else Vector() } else Vector()
case (`systemOut`, Some(json)) => case (`systemOut`, Some(json)) =>
Converter.fromJson[Seq[Byte]](json) match { Converter.fromJson[Array[Byte]](json) match {
case Success(params) => case Success(bytes) if bytes.nonEmpty && attached.get =>
if (params.nonEmpty) { synchronized(printStream.write(bytes))
if (attached.get) { case _ =>
printStream.write(params.toArray)
printStream.flush()
}
}
case Failure(_) =>
} }
Vector.empty Vector.empty
case (`systemOutFlush`, _) =>
synchronized(printStream.flush())
Vector.empty
case (`promptChannel`, _) => case (`promptChannel`, _) =>
batchMode.set(false) batchMode.set(false)
Vector.empty Vector.empty
@ -589,16 +586,23 @@ class NetworkClient(
} }
def onRequest(msg: JsonRpcRequestMessage): Unit = { def onRequest(msg: JsonRpcRequestMessage): Unit = {
import sbt.protocol.codec.JsonProtocol._
(msg.method, msg.params) match { (msg.method, msg.params) match {
case (`terminalCapabilities`, Some(json)) => case (`terminalCapabilities`, Some(json)) =>
import sbt.protocol.codec.JsonProtocol._
Converter.fromJson[TerminalCapabilitiesQuery](json) match { Converter.fromJson[TerminalCapabilitiesQuery](json) match {
case Success(terminalCapabilitiesQuery) => case Success(terminalCapabilitiesQuery) =>
val jline3 = terminalCapabilitiesQuery.jline3
val response = TerminalCapabilitiesResponse( val response = TerminalCapabilitiesResponse(
terminalCapabilitiesQuery.boolean.map(Terminal.console.getBooleanCapability), terminalCapabilitiesQuery.boolean
terminalCapabilitiesQuery.numeric.map(Terminal.console.getNumericCapability), .map(Terminal.console.getBooleanCapability(_, jline3)),
terminalCapabilitiesQuery.numeric
.map(
c => Option(Terminal.console.getNumericCapability(c, jline3)).fold(-1)(_.toInt)
),
terminalCapabilitiesQuery.string terminalCapabilitiesQuery.string
.map(s => Option(Terminal.console.getStringCapability(s)).getOrElse("null")), .map(
s => Option(Terminal.console.getStringCapability(s, jline3)).getOrElse("null")
),
) )
sendCommandResponse( sendCommandResponse(
terminalCapabilitiesResponse, terminalCapabilitiesResponse,
@ -617,6 +621,37 @@ class NetworkClient(
isEchoEnabled = Terminal.console.isEchoEnabled isEchoEnabled = Terminal.console.isEchoEnabled
) )
sendCommandResponse(terminalPropertiesResponse, response, msg.id) sendCommandResponse(terminalPropertiesResponse, response, msg.id)
case (`setTerminalAttributes`, Some(json)) =>
Converter.fromJson[TerminalSetAttributesCommand](json) match {
case Success(attributes) =>
val attrs = Map(
"iflag" -> attributes.iflag,
"oflag" -> attributes.oflag,
"cflag" -> attributes.cflag,
"lflag" -> attributes.lflag,
"cchars" -> attributes.cchars,
)
Terminal.console.setAttributes(attrs)
sendCommandResponse("", TerminalSetAttributesResponse(), msg.id)
case Failure(_) =>
}
case (`getTerminalAttributes`, _) =>
val attrs = Terminal.console.getAttributes
val response = TerminalAttributesResponse(
iflag = attrs.getOrElse("iflag", ""),
oflag = attrs.getOrElse("oflag", ""),
cflag = attrs.getOrElse("cflag", ""),
lflag = attrs.getOrElse("lflag", ""),
cchars = attrs.getOrElse("cchars", ""),
)
sendCommandResponse("", response, msg.id)
case (`setTerminalSize`, Some(json)) =>
Converter.fromJson[TerminalSetSizeCommand](json) match {
case Success(size) =>
Terminal.console.setSize(size.width, size.height)
sendCommandResponse("", TerminalSetSizeResponse(), msg.id)
case Failure(_) =>
}
case _ => case _ =>
} }
} }
@ -851,7 +886,7 @@ class NetworkClient(
} }
} }
try Terminal.console.withRawSystemIn(read()) try Terminal.console.withRawSystemIn(read())
catch { case _: InterruptedException | _: ClosedChannelException => stopped.set(true) } catch { case NonFatal(_) => stopped.set(true) }
} }
def drain(): Unit = inLock.synchronized { def drain(): Unit = inLock.synchronized {
@ -897,20 +932,18 @@ object NetworkClient {
override def success(msg: String): Unit = appender.success(msg) override def success(msg: String): Unit = appender.success(msg)
} }
} }
private def simpleConsoleInterface(printStream: PrintStream): ConsoleInterface = private def simpleConsoleInterface(doPrintln: String => Unit): ConsoleInterface =
new ConsoleInterface { new ConsoleInterface {
import scala.Console.{ GREEN, RED, RESET, YELLOW } import scala.Console.{ GREEN, RED, RESET, YELLOW }
override def appendLog(level: Level.Value, message: => String): Unit = { override def appendLog(level: Level.Value, message: => String): Unit = synchronized {
val prefix = level match { val prefix = level match {
case Level.Error => s"[$RED$level$RESET]" case Level.Error => s"[$RED$level$RESET]"
case Level.Warn => s"[$YELLOW$level$RESET]" case Level.Warn => s"[$YELLOW$level$RESET]"
case _ => s"[$RESET$level$RESET]" case _ => s"[$RESET$level$RESET]"
} }
message.split("\n").foreach { line => message.linesIterator.foreach(line => doPrintln(s"$prefix $line"))
if (!line.trim.isEmpty) printStream.println(s"$prefix $line")
}
} }
override def success(msg: String): Unit = printStream.println(s"[${GREEN}success$RESET] $msg") override def success(msg: String): Unit = doPrintln(s"[${GREEN}success$RESET] $msg")
} }
private[client] class Arguments( private[client] class Arguments(
val baseDirectory: File, val baseDirectory: File,
@ -961,8 +994,29 @@ object NetworkClient {
baseDirectory: File, baseDirectory: File,
args: Array[String], args: Array[String],
inputStream: InputStream, inputStream: InputStream,
errorStream: PrintStream,
printStream: PrintStream, printStream: PrintStream,
errorStream: PrintStream,
useJNI: Boolean
): Int = {
val client =
simpleClient(
NetworkClient.parseArgs(args).withBaseDirectory(baseDirectory),
inputStream,
printStream,
errorStream,
useJNI,
)
try {
if (client.connect(log = true, promptCompleteUsers = false)) client.run()
else 1
} catch { case _: Exception => 1 } finally client.close()
}
def client(
baseDirectory: File,
args: Array[String],
inputStream: InputStream,
errorStream: PrintStream,
terminal: Terminal,
useJNI: Boolean useJNI: Boolean
): Int = { ): Int = {
val client = val client =
@ -970,8 +1024,8 @@ object NetworkClient {
NetworkClient.parseArgs(args).withBaseDirectory(baseDirectory), NetworkClient.parseArgs(args).withBaseDirectory(baseDirectory),
inputStream, inputStream,
errorStream, errorStream,
printStream,
useJNI, useJNI,
terminal
) )
try { try {
if (client.connect(log = true, promptCompleteUsers = false)) client.run() if (client.connect(log = true, promptCompleteUsers = false)) client.run()
@ -982,17 +1036,27 @@ object NetworkClient {
arguments: Arguments, arguments: Arguments,
inputStream: InputStream, inputStream: InputStream,
errorStream: PrintStream, errorStream: PrintStream,
printStream: PrintStream,
useJNI: Boolean, useJNI: Boolean,
): NetworkClient = terminal: Terminal
new NetworkClient( ): NetworkClient = {
arguments, val doPrint: String => Unit = line => {
NetworkClient.simpleConsoleInterface(printStream), if (terminal.getLastLine.isDefined) terminal.printStream.println()
inputStream, terminal.printStream.println(line)
errorStream, }
printStream, val interface = NetworkClient.simpleConsoleInterface(doPrint)
useJNI, val printStream = terminal.printStream
) new NetworkClient(arguments, interface, inputStream, errorStream, printStream, useJNI)
}
private def simpleClient(
arguments: Arguments,
inputStream: InputStream,
printStream: PrintStream,
errorStream: PrintStream,
useJNI: Boolean,
): NetworkClient = {
val interface = NetworkClient.simpleConsoleInterface(printStream.println)
new NetworkClient(arguments, interface, inputStream, errorStream, printStream, useJNI)
}
def main(args: Array[String]): Unit = { def main(args: Array[String]): Unit = {
val (jnaArg, restOfArgs) = args.partition(_ == "--jna") val (jnaArg, restOfArgs) = args.partition(_ == "--jna")
val useJNI = jnaArg.isEmpty val useJNI = jnaArg.isEmpty
@ -1005,8 +1069,9 @@ object NetworkClient {
System.out.flush() System.out.flush()
}) })
Runtime.getRuntime.addShutdownHook(hook) Runtime.getRuntime.addShutdownHook(hook)
System.exit(Terminal.withStreams { System.exit(Terminal.withStreams(false) {
try client(base, restOfArgs, System.in, System.err, System.out, useJNI) val term = Terminal.console
try client(base, restOfArgs, term.inputStream, System.err, term, useJNI)
finally { finally {
Runtime.getRuntime.removeShutdownHook(hook) Runtime.getRuntime.removeShutdownHook(hook)
hook.run() hook.run()

View File

@ -11,14 +11,14 @@ import java.io.File
import java.nio.channels.ClosedChannelException import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import jline.console.history.PersistentHistory //import jline.console.history.PersistentHistory
import sbt.BasicCommandStrings.{ Cancel, TerminateAction, Shutdown } import sbt.BasicCommandStrings.{ Cancel, TerminateAction, Shutdown }
import sbt.BasicKeys.{ historyPath, terminalShellPrompt } import sbt.BasicKeys.{ historyPath, terminalShellPrompt }
import sbt.State import sbt.State
import sbt.internal.CommandChannel import sbt.internal.CommandChannel
import sbt.internal.util.ConsoleAppender.{ ClearPromptLine, ClearScreenAfterCursor, DeleteLine } import sbt.internal.util.ConsoleAppender.{ ClearPromptLine, ClearScreenAfterCursor, DeleteLine }
import sbt.internal.util._ import sbt.internal.util._
import sbt.internal.util.complete.{ JLineCompletion, Parser } import sbt.internal.util.complete.{ Parser }
import scala.annotation.tailrec import scala.annotation.tailrec
@ -47,44 +47,31 @@ private[sbt] object UITask {
def terminalReader(parser: Parser[_])( def terminalReader(parser: Parser[_])(
terminal: Terminal, terminal: Terminal,
state: State state: State
): Reader = { ): Reader = { () =>
val lineReader = LineReader.createReader(history(state), terminal, terminal.prompt) try {
JLineCompletion.installCustomCompletor(lineReader, parser)
() => {
val clear = terminal.ansi(ClearPromptLine, "") val clear = terminal.ansi(ClearPromptLine, "")
try { @tailrec def impl(): Either[String, String] = {
@tailrec def impl(): Either[String, String] = { val reader = LineReader.createReader(history(state), parser, terminal, terminal.prompt)
lineReader.readLine(clear + terminal.prompt.mkPrompt()) match { (try reader.readLine(clear + terminal.prompt.mkPrompt())
case null if terminal == Terminal.console && System.console == null => finally reader.close) match {
// No stdin is attached to the process so just ignore the result and case None if terminal == Terminal.console && System.console == null =>
// block until the thread is interrupted. // No stdin is attached to the process so just ignore the result and
this.synchronized(this.wait()) // block until the thread is interrupted.
Right("") // should be unreachable this.synchronized(this.wait())
// JLine returns null on ctrl+d when there is no other input. This interprets Right("") // should be unreachable
// ctrl+d with no imput as an exit // JLine returns null on ctrl+d when there is no other input. This interprets
case null => Left(TerminateAction) // ctrl+d with no imput as an exit
case s: String => case None => Left(TerminateAction)
lineReader.getHistory match { case Some(s: String) =>
case p: PersistentHistory => s.trim() match {
p.add(s) case "" => impl()
p.flush() case cmd @ (`Shutdown` | `TerminateAction` | `Cancel`) => Left(cmd)
case _ => case cmd => Right(cmd)
} }
s match {
case "" => impl()
case cmd @ (`Shutdown` | `TerminateAction` | `Cancel`) => Left(cmd)
case cmd =>
if (terminal.prompt != Prompt.Batch) terminal.setPrompt(Prompt.Running)
terminal.printStream.write(Int.MinValue)
Right(cmd)
}
}
} }
impl() }
} catch { impl()
case _: InterruptedException => Right("") } catch { case e: InterruptedException => Right("") }
} finally lineReader.close()
}
} }
} }
private[this] def history(s: State): Option[File] = private[this] def history(s: State): Option[File] =

View File

@ -61,7 +61,7 @@ public final class MetaBuildLoader extends URLClassLoader {
*/ */
public static MetaBuildLoader makeLoader(final AppProvider appProvider) throws IOException { public static MetaBuildLoader makeLoader(final AppProvider appProvider) throws IOException {
final Pattern pattern = final Pattern pattern =
Pattern.compile("(test-interface-[0-9.]+|jline-[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar"); Pattern.compile("^(test-interface-[0-9.]+|jline-[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar");
final File[] cp = appProvider.mainClasspath(); final File[] cp = appProvider.mainClasspath();
final URL[] interfaceURLs = new URL[3]; final URL[] interfaceURLs = new URL[3];
final File[] extra = final File[] extra =

View File

@ -1504,13 +1504,13 @@ object Defaults extends BuildCommon {
def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() } def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() }
print(s) print(s)
Terminal.get.withRawSystemIn { Terminal.get.withRawSystemIn {
Terminal.get.inputStream.read match { try Terminal.get.inputStream.read match {
case -1 => None case -1 | -2 => None
case b => case b =>
val res = b.toChar.toString val res = b.toChar.toString
println(res) println(res)
Some(res) Some(res)
} } catch { case e: InterruptedException => None }
} }
}), }),
classes classes

View File

@ -78,7 +78,7 @@ private[sbt] object xMain {
BspClient.run(dealiasBaseDirectory(configuration)) BspClient.run(dealiasBaseDirectory(configuration))
} else { } else {
bootServerSocket.foreach(l => Terminal.setBootStreams(l.inputStream, l.outputStream)) bootServerSocket.foreach(l => Terminal.setBootStreams(l.inputStream, l.outputStream))
Terminal.withStreams { Terminal.withStreams(true) {
if (clientModByEnv || userCommands.exists(isClient)) { if (clientModByEnv || userCommands.exists(isClient)) {
val args = userCommands.toList.filterNot(isClient) val args = userCommands.toList.filterNot(isClient)
NetworkClient.run(dealiasBaseDirectory(configuration), args) NetworkClient.run(dealiasBaseDirectory(configuration), args)

View File

@ -133,10 +133,17 @@ private[sbt] final class CommandExchange {
} }
} }
// Do not manually run GC until the user has been idling for at least the min gc interval. // Do not manually run GC until the user has been idling for at least the min gc interval.
impl(interval match { val exec = impl(interval match {
case d: FiniteDuration => Some(d.fromNow) case d: FiniteDuration => Some(d.fromNow)
case _ => None case _ => None
}, idleDeadline) }, idleDeadline)
exec.source.foreach { s =>
channelForName(s.channelName).foreach {
case c if c.terminal.prompt != Prompt.Batch => c.terminal.setPrompt(Prompt.Running)
case _ =>
}
}
exec
} }
private def addConsoleChannel(): Unit = private def addConsoleChannel(): Unit =
@ -412,6 +419,10 @@ private[sbt] final class CommandExchange {
case _ => case _ =>
} }
case _ => case _ =>
channels.foreach {
case nc: NetworkChannel => nc.shutdown(true, Some(("", "")))
case c => c.shutdown(false)
}
} }
private[sbt] def shutdown(name: String): Unit = { private[sbt] def shutdown(name: String): Unit = {
@ -448,7 +459,9 @@ private[sbt] final class CommandExchange {
case mt: FastTrackTask => case mt: FastTrackTask =>
mt.task match { mt.task match {
case `attach` => mt.channel.prompt(ConsolePromptEvent(lastState.get)) case `attach` => mt.channel.prompt(ConsolePromptEvent(lastState.get))
case `Cancel` => Option(currentExecRef.get).foreach(cancel) case `Cancel` =>
Option(currentExecRef.get).foreach(cancel)
mt.channel.prompt(ConsolePromptEvent(lastState.get))
case t if t.startsWith(ContinuousCommands.stopWatch) => case t if t.startsWith(ContinuousCommands.stopWatch) =>
ContinuousCommands.stopWatchImpl(mt.channel.name) ContinuousCommands.stopWatchImpl(mt.channel.name)
mt.channel match { mt.channel match {
@ -458,6 +471,10 @@ private[sbt] final class CommandExchange {
commandQueue.add(Exec(t, None, None)) commandQueue.add(Exec(t, None, None))
case `TerminateAction` => exit(mt) case `TerminateAction` => exit(mt)
case `Shutdown` => case `Shutdown` =>
val console = Terminal.console
val needNewLine = console.prompt.isInstanceOf[Prompt.AskUser]
console.setPrompt(Prompt.Batch)
if (needNewLine) console.printStream.println()
channels.find(_.name == mt.channel.name) match { channels.find(_.name == mt.channel.name) match {
case Some(c: NetworkChannel) => c.shutdown(false) case Some(c: NetworkChannel) => c.shutdown(false)
case _ => case _ =>

View File

@ -1217,7 +1217,6 @@ private[sbt] object ContinuousCommands {
) extends Thread(s"sbt-${channel.name}-watch-ui-thread") ) extends Thread(s"sbt-${channel.name}-watch-ui-thread")
with UITask { with UITask {
override private[sbt] def reader: UITask.Reader = () => { override private[sbt] def reader: UITask.Reader = () => {
channel.terminal.printStream.write(Int.MinValue)
def stop = Right(s"${ContinuousCommands.stopWatch} ${channel.name}") def stop = Right(s"${ContinuousCommands.stopWatch} ${channel.name}")
val exitAction: Watch.Action = { val exitAction: Watch.Action = {
Watch.apply( Watch.apply(

View File

@ -53,11 +53,12 @@ private[sbt] class TaskProgress private ()
if (firstTime.compareAndSet(true, activeExceedingThreshold.isEmpty)) threshold if (firstTime.compareAndSet(true, activeExceedingThreshold.isEmpty)) threshold
else sleepDuration else sleepDuration
val limit = duration.fromNow val limit = duration.fromNow
while (Deadline.now < limit) { while (Deadline.now < limit && !isClosed.get && active.nonEmpty) {
var task = tasks.poll((limit - Deadline.now).toMillis, TimeUnit.MILLISECONDS) var task = tasks.poll((limit - Deadline.now).toMillis, TimeUnit.MILLISECONDS)
while (task != null) { while (task != null) {
if (containsSkipTasks(Vector(task)) || lastTaskCount.get == 0) doReport() if (containsSkipTasks(Vector(task)) || lastTaskCount.get == 0) doReport()
task = tasks.poll task = tasks.poll
tasks.clear()
} }
} }
} catch { } catch {

View File

@ -99,7 +99,6 @@ final class NetworkChannel(
addFastTrackTask(attach) addFastTrackTask(attach)
} }
private[sbt] def prompt(): Unit = { private[sbt] def prompt(): Unit = {
terminal.setPrompt(Prompt.Running)
interactive.set(true) interactive.set(true)
jsonRpcNotify(promptChannel, "") jsonRpcNotify(promptChannel, "")
} }
@ -641,7 +640,7 @@ final class NetworkChannel(
case -1 => throw new ClosedChannelException() case -1 => throw new ClosedChannelException()
case b => b case b => b
} }
} catch { case _: IOException => -1 } } catch { case e: IOException => -1 }
} }
override def available(): Int = inputBuffer.size override def available(): Int = inputBuffer.size
} }
@ -774,25 +773,82 @@ final class NetworkChannel(
Some(result(queue.take)) Some(result(queue.take))
} }
} }
override def getBooleanCapability(capability: String): Boolean = override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
getCapability( getCapability(
TerminalCapabilitiesQuery(boolean = Some(capability), numeric = None, string = None), TerminalCapabilitiesQuery(
boolean = Some(capability),
numeric = None,
string = None,
jline3
),
_.boolean.getOrElse(false) _.boolean.getOrElse(false)
).getOrElse(false) ).getOrElse(false)
override def getNumericCapability(capability: String): Int = override def getNumericCapability(capability: String, jline3: Boolean): Integer =
getCapability( getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = Some(capability), string = None), TerminalCapabilitiesQuery(
_.numeric.getOrElse(-1) boolean = None,
).getOrElse(-1) numeric = Some(capability),
override def getStringCapability(capability: String): String = string = None,
jline3
),
(_: TerminalCapabilitiesResponse).numeric.map(Integer.valueOf).getOrElse(-1: Integer)
).getOrElse(-1: Integer)
override def getStringCapability(capability: String, jline3: Boolean): String =
getCapability( getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = None, string = Some(capability)), TerminalCapabilitiesQuery(
boolean = None,
numeric = None,
string = Some(capability),
jline3
),
_.string.flatMap { _.string.flatMap {
case "null" => None case "null" => None
case s => Some(s) case s => Some(s)
}.orNull }.orNull
).getOrElse("") ).getOrElse("")
override private[sbt] def getAttributes: Map[String, String] =
if (closed.get) Map.empty
else {
import sbt.protocol.codec.JsonProtocol._
val queue = VirtualTerminal.sendTerminalAttributesQuery(
name,
jsonRpcRequest
)
try {
val a = queue.take
Map(
"iflag" -> a.iflag,
"oflag" -> a.oflag,
"cflag" -> a.cflag,
"lflag" -> a.lflag,
"cchars" -> a.cchars
)
} catch { case _: InterruptedException => Map.empty }
}
override private[sbt] def setAttributes(attributes: Map[String, String]): Unit =
if (!closed.get) {
import sbt.protocol.codec.JsonProtocol._
val attrs = TerminalSetAttributesCommand(
iflag = attributes.getOrElse("iflag", ""),
oflag = attributes.getOrElse("oflag", ""),
cflag = attributes.getOrElse("cflag", ""),
lflag = attributes.getOrElse("lflag", ""),
cchars = attributes.getOrElse("cchars", ""),
)
val queue = VirtualTerminal.setTerminalAttributes(name, jsonRpcRequest, attrs)
try queue.take
catch { case _: InterruptedException => }
}
override def setSize(width: Int, height: Int): Unit =
if (!closed.get) {
import sbt.protocol.codec.JsonProtocol._
val size = TerminalSetSizeCommand(width, height)
val queue = VirtualTerminal.setTerminalSize(name, jsonRpcRequest, size)
try queue.take
catch { case _: InterruptedException => }
}
override def toString: String = s"NetworkTerminal($name)" override def toString: String = s"NetworkTerminal($name)"
override def close(): Unit = if (closed.compareAndSet(false, true)) { override def close(): Unit = if (closed.compareAndSet(false, true)) {
val threads = blockedThreads.synchronized { val threads = blockedThreads.synchronized {

View File

@ -25,16 +25,27 @@ import sbt.protocol.Serialization.{
import sjsonnew.support.scalajson.unsafe.Converter import sjsonnew.support.scalajson.unsafe.Converter
import sbt.protocol.{ import sbt.protocol.{
Attach, Attach,
TerminalAttributesQuery,
TerminalAttributesResponse,
TerminalCapabilitiesQuery, TerminalCapabilitiesQuery,
TerminalCapabilitiesResponse, TerminalCapabilitiesResponse,
TerminalPropertiesResponse TerminalPropertiesResponse,
TerminalSetAttributesCommand,
TerminalSetSizeCommand,
} }
import sbt.protocol.codec.JsonProtocol._
object VirtualTerminal { object VirtualTerminal {
private[this] val pendingTerminalProperties = private[this] val pendingTerminalProperties =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalPropertiesResponse]]() new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalPropertiesResponse]]()
private[this] val pendingTerminalCapabilities = private[this] val pendingTerminalCapabilities =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalCapabilitiesResponse]] new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalCapabilitiesResponse]]
private[this] val pendingTerminalAttributes =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalAttributesResponse]]
private[this] val pendingTerminalSetAttributes =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]]
private[this] val pendingTerminalSetSize =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]]
private[sbt] def sendTerminalPropertiesQuery( private[sbt] def sendTerminalPropertiesQuery(
channelName: String, channelName: String,
jsonRpcRequest: (String, String, String) => Unit jsonRpcRequest: (String, String, String) => Unit
@ -70,6 +81,39 @@ object VirtualTerminal {
case _ => case _ =>
} }
} }
private[sbt] def sendTerminalAttributesQuery(
channelName: String,
jsonRpcRequest: (String, String, TerminalAttributesQuery) => Unit,
): ArrayBlockingQueue[TerminalAttributesResponse] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[TerminalAttributesResponse](1)
pendingTerminalAttributes.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, TerminalAttributesQuery())
queue
}
private[sbt] def setTerminalAttributes(
channelName: String,
jsonRpcRequest: (String, String, TerminalSetAttributesCommand) => Unit,
query: TerminalSetAttributesCommand
): ArrayBlockingQueue[Unit] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[Unit](1)
pendingTerminalSetAttributes.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, query)
queue
}
private[sbt] def setTerminalSize(
channelName: String,
jsonRpcRequest: (String, String, TerminalSetSizeCommand) => Unit,
query: TerminalSetSizeCommand
): ArrayBlockingQueue[Unit] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[Unit](1)
pendingTerminalSetSize.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, query)
queue
}
val handler = ServerHandler { cb => val handler = ServerHandler { cb =>
ServerIntent(requestHandler(cb), responseHandler(cb), notificationHandler(cb)) ServerIntent(requestHandler(cb), responseHandler(cb), notificationHandler(cb))
} }
@ -77,7 +121,6 @@ object VirtualTerminal {
private val requestHandler: Handler[JsonRpcRequestMessage] = private val requestHandler: Handler[JsonRpcRequestMessage] =
callback => { callback => {
case r if r.method == attach => case r if r.method == attach =>
import sbt.protocol.codec.JsonProtocol.AttachFormat
val isInteractive = r.params val isInteractive = r.params
.flatMap(Converter.fromJson[Attach](_).toOption.map(_.interactive)) .flatMap(Converter.fromJson[Attach](_).toOption.map(_.interactive))
.exists(identity) .exists(identity)
@ -89,7 +132,6 @@ object VirtualTerminal {
private val responseHandler: Handler[JsonRpcResponseMessage] = private val responseHandler: Handler[JsonRpcResponseMessage] =
callback => { callback => {
case r if pendingTerminalProperties.get((callback.name, r.id)) != null => case r if pendingTerminalProperties.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response = val response =
r.result.flatMap(Converter.fromJson[TerminalPropertiesResponse](_).toOption) r.result.flatMap(Converter.fromJson[TerminalPropertiesResponse](_).toOption)
pendingTerminalProperties.remove((callback.name, r.id)) match { pendingTerminalProperties.remove((callback.name, r.id)) match {
@ -97,7 +139,6 @@ object VirtualTerminal {
case buffer => response.foreach(buffer.put) case buffer => response.foreach(buffer.put)
} }
case r if pendingTerminalCapabilities.get((callback.name, r.id)) != null => case r if pendingTerminalCapabilities.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response = val response =
r.result.flatMap( r.result.flatMap(
Converter.fromJson[TerminalCapabilitiesResponse](_).toOption Converter.fromJson[TerminalCapabilitiesResponse](_).toOption
@ -107,6 +148,24 @@ object VirtualTerminal {
case buffer => case buffer =>
buffer.put(response.getOrElse(TerminalCapabilitiesResponse(None, None, None))) buffer.put(response.getOrElse(TerminalCapabilitiesResponse(None, None, None)))
} }
case r if pendingTerminalAttributes.get((callback.name, r.id)) != null =>
val response =
r.result.flatMap(Converter.fromJson[TerminalAttributesResponse](_).toOption)
pendingTerminalAttributes.remove((callback.name, r.id)) match {
case null =>
case buffer =>
buffer.put(response.getOrElse(TerminalAttributesResponse("", "", "", "", "")))
}
case r if pendingTerminalSetAttributes.get((callback.name, r.id)) != null =>
pendingTerminalSetAttributes.remove((callback.name, r.id)) match {
case null =>
case buffer => buffer.put(())
}
case r if pendingTerminalSetSize.get((callback.name, r.id)) != null =>
pendingTerminalSetSize.remove((callback.name, r.id)) match {
case null =>
case buffer => buffer.put(())
}
} }
private val notificationHandler: Handler[JsonRpcNotificationMessage] = private val notificationHandler: Handler[JsonRpcNotificationMessage] =
callback => { callback => {

View File

@ -84,7 +84,9 @@ object Dependencies {
val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash") val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash")
val jline = "org.scala-sbt.jline" % "jline" % "2.14.7-sbt-5e51b9d4f9631ebfa29753ce4accc57808e7fd6b" val jline = "org.scala-sbt.jline" % "jline" % "2.14.7-sbt-5e51b9d4f9631ebfa29753ce4accc57808e7fd6b"
val jansi = "org.fusesource.jansi" % "jansi" % "1.12" val jline3 = "org.jline" % "jline" % "3.15.0"
val jline3Jansi = "org.jline" % "jline-terminal-jansi" % "3.15.0"
val jansi = "org.fusesource.jansi" % "jansi" % "1.18"
val scalatest = "org.scalatest" %% "scalatest" % "3.0.8" val scalatest = "org.scalatest" %% "scalatest" % "3.0.8"
val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0" val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0"
val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1" val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1"

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalAttributesQuery private () extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case _: TerminalAttributesQuery => true
case _ => false
}
override def hashCode: Int = {
37 * (17 + "sbt.protocol.TerminalAttributesQuery".##)
}
override def toString: String = {
"TerminalAttributesQuery()"
}
private[this] def copy(): TerminalAttributesQuery = {
new TerminalAttributesQuery()
}
}
object TerminalAttributesQuery {
def apply(): TerminalAttributesQuery = new TerminalAttributesQuery()
}

View File

@ -0,0 +1,48 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalAttributesResponse private (
val iflag: String,
val oflag: String,
val cflag: String,
val lflag: String,
val cchars: String) extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalAttributesResponse => (this.iflag == x.iflag) && (this.oflag == x.oflag) && (this.cflag == x.cflag) && (this.lflag == x.lflag) && (this.cchars == x.cchars)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalAttributesResponse".##) + iflag.##) + oflag.##) + cflag.##) + lflag.##) + cchars.##)
}
override def toString: String = {
"TerminalAttributesResponse(" + iflag + ", " + oflag + ", " + cflag + ", " + lflag + ", " + cchars + ")"
}
private[this] def copy(iflag: String = iflag, oflag: String = oflag, cflag: String = cflag, lflag: String = lflag, cchars: String = cchars): TerminalAttributesResponse = {
new TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars)
}
def withIflag(iflag: String): TerminalAttributesResponse = {
copy(iflag = iflag)
}
def withOflag(oflag: String): TerminalAttributesResponse = {
copy(oflag = oflag)
}
def withCflag(cflag: String): TerminalAttributesResponse = {
copy(cflag = cflag)
}
def withLflag(lflag: String): TerminalAttributesResponse = {
copy(lflag = lflag)
}
def withCchars(cchars: String): TerminalAttributesResponse = {
copy(cchars = cchars)
}
}
object TerminalAttributesResponse {
def apply(iflag: String, oflag: String, cflag: String, lflag: String, cchars: String): TerminalAttributesResponse = new TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars)
}

View File

@ -7,22 +7,23 @@ package sbt.protocol
final class TerminalCapabilitiesQuery private ( final class TerminalCapabilitiesQuery private (
val boolean: Option[String], val boolean: Option[String],
val numeric: Option[String], val numeric: Option[String],
val string: Option[String]) extends sbt.protocol.CommandMessage() with Serializable { val string: Option[String],
val jline3: Boolean) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match { override def equals(o: Any): Boolean = o match {
case x: TerminalCapabilitiesQuery => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string) case x: TerminalCapabilitiesQuery => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string) && (this.jline3 == x.jline3)
case _ => false case _ => false
} }
override def hashCode: Int = { override def hashCode: Int = {
37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesQuery".##) + boolean.##) + numeric.##) + string.##) 37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesQuery".##) + boolean.##) + numeric.##) + string.##) + jline3.##)
} }
override def toString: String = { override def toString: String = {
"TerminalCapabilitiesQuery(" + boolean + ", " + numeric + ", " + string + ")" "TerminalCapabilitiesQuery(" + boolean + ", " + numeric + ", " + string + ", " + jline3 + ")"
} }
private[this] def copy(boolean: Option[String] = boolean, numeric: Option[String] = numeric, string: Option[String] = string): TerminalCapabilitiesQuery = { private[this] def copy(boolean: Option[String] = boolean, numeric: Option[String] = numeric, string: Option[String] = string, jline3: Boolean = jline3): TerminalCapabilitiesQuery = {
new TerminalCapabilitiesQuery(boolean, numeric, string) new TerminalCapabilitiesQuery(boolean, numeric, string, jline3)
} }
def withBoolean(boolean: Option[String]): TerminalCapabilitiesQuery = { def withBoolean(boolean: Option[String]): TerminalCapabilitiesQuery = {
copy(boolean = boolean) copy(boolean = boolean)
@ -42,9 +43,12 @@ final class TerminalCapabilitiesQuery private (
def withString(string: String): TerminalCapabilitiesQuery = { def withString(string: String): TerminalCapabilitiesQuery = {
copy(string = Option(string)) copy(string = Option(string))
} }
def withJline3(jline3: Boolean): TerminalCapabilitiesQuery = {
copy(jline3 = jline3)
}
} }
object TerminalCapabilitiesQuery { object TerminalCapabilitiesQuery {
def apply(boolean: Option[String], numeric: Option[String], string: Option[String]): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string) def apply(boolean: Option[String], numeric: Option[String], string: Option[String], jline3: Boolean): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string, jline3)
def apply(boolean: String, numeric: String, string: String): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(string)) def apply(boolean: String, numeric: String, string: String, jline3: Boolean): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(string), jline3)
} }

View File

@ -0,0 +1,48 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetAttributesCommand private (
val iflag: String,
val oflag: String,
val cflag: String,
val lflag: String,
val cchars: String) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalSetAttributesCommand => (this.iflag == x.iflag) && (this.oflag == x.oflag) && (this.cflag == x.cflag) && (this.lflag == x.lflag) && (this.cchars == x.cchars)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalSetAttributesCommand".##) + iflag.##) + oflag.##) + cflag.##) + lflag.##) + cchars.##)
}
override def toString: String = {
"TerminalSetAttributesCommand(" + iflag + ", " + oflag + ", " + cflag + ", " + lflag + ", " + cchars + ")"
}
private[this] def copy(iflag: String = iflag, oflag: String = oflag, cflag: String = cflag, lflag: String = lflag, cchars: String = cchars): TerminalSetAttributesCommand = {
new TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars)
}
def withIflag(iflag: String): TerminalSetAttributesCommand = {
copy(iflag = iflag)
}
def withOflag(oflag: String): TerminalSetAttributesCommand = {
copy(oflag = oflag)
}
def withCflag(cflag: String): TerminalSetAttributesCommand = {
copy(cflag = cflag)
}
def withLflag(lflag: String): TerminalSetAttributesCommand = {
copy(lflag = lflag)
}
def withCchars(cchars: String): TerminalSetAttributesCommand = {
copy(cchars = cchars)
}
}
object TerminalSetAttributesCommand {
def apply(iflag: String, oflag: String, cflag: String, lflag: String, cchars: String): TerminalSetAttributesCommand = new TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars)
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetAttributesResponse private () extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case _: TerminalSetAttributesResponse => true
case _ => false
}
override def hashCode: Int = {
37 * (17 + "sbt.protocol.TerminalSetAttributesResponse".##)
}
override def toString: String = {
"TerminalSetAttributesResponse()"
}
private[this] def copy(): TerminalSetAttributesResponse = {
new TerminalSetAttributesResponse()
}
}
object TerminalSetAttributesResponse {
def apply(): TerminalSetAttributesResponse = new TerminalSetAttributesResponse()
}

View File

@ -0,0 +1,36 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetSizeCommand private (
val width: Int,
val height: Int) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalSetSizeCommand => (this.width == x.width) && (this.height == x.height)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (17 + "sbt.protocol.TerminalSetSizeCommand".##) + width.##) + height.##)
}
override def toString: String = {
"TerminalSetSizeCommand(" + width + ", " + height + ")"
}
private[this] def copy(width: Int = width, height: Int = height): TerminalSetSizeCommand = {
new TerminalSetSizeCommand(width, height)
}
def withWidth(width: Int): TerminalSetSizeCommand = {
copy(width = width)
}
def withHeight(height: Int): TerminalSetSizeCommand = {
copy(height = height)
}
}
object TerminalSetSizeCommand {
def apply(width: Int, height: Int): TerminalSetSizeCommand = new TerminalSetSizeCommand(width, height)
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalSetSizeResponse private () extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case _: TerminalSetSizeResponse => true
case _ => false
}
override def hashCode: Int = {
37 * (17 + "sbt.protocol.TerminalSetSizeResponse".##)
}
override def toString: String = {
"TerminalSetSizeResponse()"
}
private[this] def copy(): TerminalSetSizeResponse = {
new TerminalSetSizeResponse()
}
}
object TerminalSetSizeResponse {
def apply(): TerminalSetSizeResponse = new TerminalSetSizeResponse()
}

View File

@ -6,6 +6,6 @@
package sbt.protocol.codec package sbt.protocol.codec
import _root_.sjsonnew.JsonFormat import _root_.sjsonnew.JsonFormat
trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats => trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats with sbt.protocol.codec.TerminalSetAttributesCommandFormats with sbt.protocol.codec.TerminalAttributesQueryFormats with sbt.protocol.codec.TerminalSetSizeCommandFormats =>
implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat5[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery]("type") implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat8[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery, sbt.protocol.TerminalSetAttributesCommand, sbt.protocol.TerminalAttributesQuery, sbt.protocol.TerminalSetSizeCommand]("type")
} }

View File

@ -6,6 +6,6 @@
package sbt.protocol.codec package sbt.protocol.codec
import _root_.sjsonnew.JsonFormat import _root_.sjsonnew.JsonFormat
trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats with sbt.internal.util.codec.JValueFormats with sbt.protocol.codec.SettingQuerySuccessFormats with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats => trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats with sbt.internal.util.codec.JValueFormats with sbt.protocol.codec.SettingQuerySuccessFormats with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats with sbt.protocol.codec.TerminalSetAttributesResponseFormats with sbt.protocol.codec.TerminalAttributesResponseFormats with sbt.protocol.codec.TerminalSetSizeResponseFormats =>
implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat7[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure, sbt.protocol.TerminalPropertiesResponse, sbt.protocol.TerminalCapabilitiesResponse]("type") implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat10[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure, sbt.protocol.TerminalPropertiesResponse, sbt.protocol.TerminalCapabilitiesResponse, sbt.protocol.TerminalSetAttributesResponse, sbt.protocol.TerminalAttributesResponse, sbt.protocol.TerminalSetSizeResponse]("type")
} }

View File

@ -10,6 +10,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.SettingQueryFormats
with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.AttachFormats
with sbt.protocol.codec.TerminalCapabilitiesQueryFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats
with sbt.protocol.codec.TerminalSetAttributesCommandFormats
with sbt.protocol.codec.TerminalAttributesQueryFormats
with sbt.protocol.codec.TerminalSetSizeCommandFormats
with sbt.protocol.codec.CommandMessageFormats with sbt.protocol.codec.CommandMessageFormats
with sbt.protocol.codec.CompletionParamsFormats with sbt.protocol.codec.CompletionParamsFormats
with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.ChannelAcceptedEventFormats
@ -20,6 +23,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.SettingQueryFailureFormats
with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats
with sbt.protocol.codec.TerminalCapabilitiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats
with sbt.protocol.codec.TerminalSetAttributesResponseFormats
with sbt.protocol.codec.TerminalAttributesResponseFormats
with sbt.protocol.codec.TerminalSetSizeResponseFormats
with sbt.protocol.codec.EventMessageFormats with sbt.protocol.codec.EventMessageFormats
with sbt.protocol.codec.SettingQueryResponseFormats with sbt.protocol.codec.SettingQueryResponseFormats
with sbt.protocol.codec.CompletionResponseFormats with sbt.protocol.codec.CompletionResponseFormats

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalAttributesQueryFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalAttributesQueryFormat: JsonFormat[sbt.protocol.TerminalAttributesQuery] = new JsonFormat[sbt.protocol.TerminalAttributesQuery] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalAttributesQuery = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
unbuilder.endObject()
sbt.protocol.TerminalAttributesQuery()
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalAttributesQuery, builder: Builder[J]): Unit = {
builder.beginObject()
builder.endObject()
}
}
}

View File

@ -0,0 +1,35 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalAttributesResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalAttributesResponseFormat: JsonFormat[sbt.protocol.TerminalAttributesResponse] = new JsonFormat[sbt.protocol.TerminalAttributesResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalAttributesResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val iflag = unbuilder.readField[String]("iflag")
val oflag = unbuilder.readField[String]("oflag")
val cflag = unbuilder.readField[String]("cflag")
val lflag = unbuilder.readField[String]("lflag")
val cchars = unbuilder.readField[String]("cchars")
unbuilder.endObject()
sbt.protocol.TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalAttributesResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("iflag", obj.iflag)
builder.addField("oflag", obj.oflag)
builder.addField("cflag", obj.cflag)
builder.addField("lflag", obj.lflag)
builder.addField("cchars", obj.cchars)
builder.endObject()
}
}
}

View File

@ -14,8 +14,9 @@ implicit lazy val TerminalCapabilitiesQueryFormat: JsonFormat[sbt.protocol.Termi
val boolean = unbuilder.readField[Option[String]]("boolean") val boolean = unbuilder.readField[Option[String]]("boolean")
val numeric = unbuilder.readField[Option[String]]("numeric") val numeric = unbuilder.readField[Option[String]]("numeric")
val string = unbuilder.readField[Option[String]]("string") val string = unbuilder.readField[Option[String]]("string")
val jline3 = unbuilder.readField[Boolean]("jline3")
unbuilder.endObject() unbuilder.endObject()
sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string) sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string, jline3)
case None => case None =>
deserializationError("Expected JsObject but found None") deserializationError("Expected JsObject but found None")
} }
@ -25,6 +26,7 @@ implicit lazy val TerminalCapabilitiesQueryFormat: JsonFormat[sbt.protocol.Termi
builder.addField("boolean", obj.boolean) builder.addField("boolean", obj.boolean)
builder.addField("numeric", obj.numeric) builder.addField("numeric", obj.numeric)
builder.addField("string", obj.string) builder.addField("string", obj.string)
builder.addField("jline3", obj.jline3)
builder.endObject() builder.endObject()
} }
} }

View File

@ -0,0 +1,35 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetAttributesCommandFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetAttributesCommandFormat: JsonFormat[sbt.protocol.TerminalSetAttributesCommand] = new JsonFormat[sbt.protocol.TerminalSetAttributesCommand] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetAttributesCommand = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val iflag = unbuilder.readField[String]("iflag")
val oflag = unbuilder.readField[String]("oflag")
val cflag = unbuilder.readField[String]("cflag")
val lflag = unbuilder.readField[String]("lflag")
val cchars = unbuilder.readField[String]("cchars")
unbuilder.endObject()
sbt.protocol.TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetAttributesCommand, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("iflag", obj.iflag)
builder.addField("oflag", obj.oflag)
builder.addField("cflag", obj.cflag)
builder.addField("lflag", obj.lflag)
builder.addField("cchars", obj.cchars)
builder.endObject()
}
}
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetAttributesResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetAttributesResponseFormat: JsonFormat[sbt.protocol.TerminalSetAttributesResponse] = new JsonFormat[sbt.protocol.TerminalSetAttributesResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetAttributesResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
unbuilder.endObject()
sbt.protocol.TerminalSetAttributesResponse()
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetAttributesResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.endObject()
}
}
}

View File

@ -0,0 +1,29 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetSizeCommandFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetSizeCommandFormat: JsonFormat[sbt.protocol.TerminalSetSizeCommand] = new JsonFormat[sbt.protocol.TerminalSetSizeCommand] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetSizeCommand = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val width = unbuilder.readField[Int]("width")
val height = unbuilder.readField[Int]("height")
unbuilder.endObject()
sbt.protocol.TerminalSetSizeCommand(width, height)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetSizeCommand, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("width", obj.width)
builder.addField("height", obj.height)
builder.endObject()
}
}
}

View File

@ -0,0 +1,27 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait TerminalSetSizeResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalSetSizeResponseFormat: JsonFormat[sbt.protocol.TerminalSetSizeResponse] = new JsonFormat[sbt.protocol.TerminalSetSizeResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetSizeResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
unbuilder.endObject()
sbt.protocol.TerminalSetSizeResponse()
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalSetSizeResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.endObject()
}
}
}

View File

@ -85,22 +85,50 @@ type ExecutionEvent {
} }
type TerminalPropertiesResponse implements EventMessage { type TerminalPropertiesResponse implements EventMessage {
width: Int! width: Int!
height: Int! height: Int!
isAnsiSupported: Boolean! isAnsiSupported: Boolean!
isColorEnabled: Boolean! isColorEnabled: Boolean!
isSupershellEnabled: Boolean! isSupershellEnabled: Boolean!
isEchoEnabled: Boolean! isEchoEnabled: Boolean!
} }
type TerminalCapabilitiesQuery implements CommandMessage { type TerminalCapabilitiesQuery implements CommandMessage {
boolean: String boolean: String
numeric: String numeric: String
string: String string: String
jline3: Boolean!
} }
type TerminalCapabilitiesResponse implements EventMessage { type TerminalCapabilitiesResponse implements EventMessage {
boolean: Boolean boolean: Boolean
numeric: Int numeric: Int
string: String string: String
} }
type TerminalSetAttributesCommand implements CommandMessage {
iflag: String!,
oflag: String!,
cflag: String!,
lflag: String!,
cchars: String!,
}
type TerminalSetAttributesResponse implements EventMessage {}
type TerminalAttributesQuery implements CommandMessage {}
type TerminalAttributesResponse implements EventMessage {
iflag: String!,
oflag: String!,
cflag: String!,
lflag: String!,
cchars: String!,
}
type TerminalSetSizeCommand implements CommandMessage {
width: Int!
height: Int!
}
type TerminalSetSizeResponse implements EventMessage {}

View File

@ -26,6 +26,7 @@ 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 systemOutFlush = "sbt/systemOutFlush"
val terminalPropertiesQuery = "sbt/terminalPropertiesQuery" val terminalPropertiesQuery = "sbt/terminalPropertiesQuery"
val terminalPropertiesResponse = "sbt/terminalPropertiesResponse" val terminalPropertiesResponse = "sbt/terminalPropertiesResponse"
val terminalCapabilities = "sbt/terminalCapabilities" val terminalCapabilities = "sbt/terminalCapabilities"
@ -34,6 +35,9 @@ object Serialization {
val attachResponse = "sbt/attachResponse" val attachResponse = "sbt/attachResponse"
val cancelRequest = "sbt/cancelRequest" val cancelRequest = "sbt/cancelRequest"
val promptChannel = "sbt/promptChannel" val promptChannel = "sbt/promptChannel"
val setTerminalAttributes = "sbt/setTerminalAttributes"
val getTerminalAttributes = "sbt/getTerminalAttributes"
val setTerminalSize = "sbt/setTerminalSize"
val CancelAll = "__CancelAll" val CancelAll = "__CancelAll"
@deprecated("unused", since = "1.4.0") @deprecated("unused", since = "1.4.0")