Merge pull request #5671 from eatkins/upgrade-to-jline3

Upgrade to jline3
This commit is contained in:
eugene yokota 2020-07-11 20:35:10 -04:00 committed by GitHub
commit ab5e57afb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1766 additions and 496 deletions

View File

@ -108,7 +108,7 @@ def commonBaseSettings: Seq[Setting[_]] = Def.settings(
)
def commonSettings: Seq[Setting[_]] =
commonBaseSettings :+
addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.4" cross CrossVersion.binary)
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full)
def utilCommonSettings: Seq[Setting[_]] =
commonBaseSettings :+ (crossScalaVersions := (scala212 :: scala213 :: Nil))
@ -246,9 +246,14 @@ lazy val bundledLauncherProj =
val collectionProj = (project in file("internal") / "util-collection")
.settings(
testedBaseSettings,
utilCommonSettings,
Util.keywordsSettings,
name := "Collections",
libraryDependencies ++= Seq(sjsonNewScalaJson.value),
libraryDependencies ++= (CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, major)) if major <= 12 => Seq()
case _ => Seq("org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0")
}),
mimaSettings,
mimaBinaryIssueFilters ++= Seq(
// Added private[sbt] method to capture State attributes.
@ -289,6 +294,7 @@ val completeProj = (project in file("internal") / "util-complete")
testedBaseSettings,
name := "Completion",
libraryDependencies += jline,
libraryDependencies += jline3,
mimaSettings,
// Parser is used publicly, so we can't break bincompat.
mimaBinaryIssueFilters := Seq(
@ -343,12 +349,20 @@ lazy val utilPosition = (project in file("internal") / "util-position")
lazy val utilLogging = (project in file("internal") / "util-logging")
.enablePlugins(ContrabandPlugin, JsonCodecPlugin)
.dependsOn(utilInterface)
.dependsOn(utilInterface, collectionProj)
.settings(
utilCommonSettings,
name := "Util Logging",
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 ++= (scalaVersion.value match {
case v if v.startsWith("2.12.") => List(compilerPlugin(silencerPlugin))
@ -1047,8 +1061,7 @@ lazy val sbtClientProj = (project in file("client"))
crossPaths := false,
exportJars := true,
libraryDependencies += jansi,
libraryDependencies += "net.java.dev.jna" % "jna" % "5.5.0",
libraryDependencies += "net.java.dev.jna" % "jna-platform" % "5.5.0",
libraryDependencies += jline3Jansi,
libraryDependencies += scalatest % "test",
/*
* On windows, the raw classpath is too large to be a command argument to an
@ -1099,8 +1112,8 @@ lazy val sbtClientProj = (project in file("client"))
}
thread.start()
proc.waitFor(5, java.util.concurrent.TimeUnit.MINUTES)
assert(proc.exitValue == 0, s"Exit value ${proc.exitValue} was nonzero")
nativeExecutablePath.value
file("").toPath
},
graalNativeImageOptions := Seq(
"--no-fallback",

View File

@ -1,7 +1,20 @@
{
"resources":[
{"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":"linux/x86_64/libsbtipcsocket.so"},
{"pattern":"win32/x86_64/sbtipcsocket.dll"}

View File

@ -0,0 +1,13 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import scala.collection.parallel.ParSeq
private[util] object Par {
def apply[R](s: Seq[R]): ParSeq[R] = s.par
}

View File

@ -0,0 +1,14 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import scala.collection.parallel.CollectionConverters._
import scala.collection.parallel.ParSeq
private[util] object Par {
def apply[R](s: Seq[R]): ParSeq[R] = s.par
}

View File

@ -66,7 +66,7 @@ object IMap {
put(k, f(this get k getOrElse init))
def mapValues[V2[_]](f: V ~> V2) =
new IMap0[K, V2](backing.mapValues(x => f(x)))
new IMap0[K, V2](Map(backing.iterator.map { case (k, v) => k -> f(v) }.toArray: _*))
def mapSeparate[VL[_], VR[_]](f: V ~> λ[T => Either[VL[T], VR[T]]]) = {
val mapped = backing.iterator.map {

View File

@ -7,8 +7,6 @@
package sbt.internal.util
import scala.language.existentials
import Types._
import sbt.util.Show
import Util.{ nil, nilSeq }
@ -242,7 +240,7 @@ trait Init[ScopeType] {
if (s.definitive) Vector(s) else ss :+ s
def addLocal(init: Seq[Setting[_]])(implicit scopeLocal: ScopeLocal): Seq[Setting[_]] =
init.par.map(_.dependencies flatMap scopeLocal).toVector.flatten ++ init
Par(init).map(_.dependencies flatMap scopeLocal).toVector.flatten ++ init
def delegate(sMap: ScopedMap)(
implicit delegates: ScopeType => Seq[ScopeType],
@ -465,7 +463,7 @@ trait Init[ScopeType] {
def dependencies = settings.flatMap(_.dependencies)
// This is mainly for use in the cyclic reference error message
override def toString =
s"Derived settings for ${key.label}, ${definedAtString(settings.map(_.setting))}"
s"Derived settings for ${key.label}, ${definedAtString(settings.map(_.setting).toSeq)}"
}
// separate `derived` settings from normal settings (`defs`)

View File

@ -8,16 +8,28 @@
package sbt.internal.util
import java.io._
import java.util.{ List => JList }
import jline.console.ConsoleReader
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 scala.annotation.tailrec
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]
override def close(): Unit = {}
}
object LineReader {
@ -25,7 +37,67 @@ object LineReader {
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
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(
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.withRawInput {
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],
terminal: Terminal,
prompt: Prompt = Prompt.Running,
@ -42,7 +114,6 @@ object LineReader {
cr.setHistoryEnabled(true)
cr
}
def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal)
def simple(
historyPath: Option[File],
@ -169,7 +240,7 @@ private[sbt] object JLine {
}
@deprecated("Avoid referencing JLine directly.", "1.4.0")
def withJLine[T](action: => T): T = Terminal.get.withRawSystemIn(action)
def withJLine[T](action: => T): T = Terminal.get.withRawInput(action)
@deprecated("Use LineReader.simple instead", "1.4.0")
def simple(
@ -230,7 +301,7 @@ final class FullReader(
Terminal.console
)
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)
cr
}
@ -244,7 +315,7 @@ class SimpleReader private[sbt] (
def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
this(historyPath, handleCONT, Terminal.console)
protected[this] val reader: ConsoleReader =
LineReader.createReader(historyPath, terminal)
LineReader.createJLine2Reader(historyPath, terminal)
}
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) {

View File

@ -9,7 +9,8 @@ package sbt.internal.util
import java.io.{ PrintStream, PrintWriter }
import java.lang.StringBuilder
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, AtomicReference }
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import org.apache.logging.log4j.core.appender.AbstractAppender
import org.apache.logging.log4j.core.{ LogEvent => XLogEvent }
@ -18,8 +19,6 @@ import org.apache.logging.log4j.{ Level => XLevel }
import sbt.internal.util.ConsoleAppender._
import sbt.util._
import scala.collection.mutable.ArrayBuffer
object ConsoleLogger {
// These are provided so other modules do not break immediately.
@deprecated("Use EscHelpers.ESC instead", "0.13.x")
@ -396,7 +395,8 @@ class ConsoleAppender private[ConsoleAppender] (
override def append(event: XLogEvent): Unit = {
val level = ConsoleAppender.toLevel(event.getLevel)
val message = event.getMessage
appendMessage(level, message)
try appendMessage(level, message)
catch { case _: ClosedChannelException => }
}
/**
@ -553,135 +553,3 @@ class ConsoleAppender private[ConsoleAppender] (
}
final class SuppressedTraceContext(val traceLevel: Int, val useFormat: Boolean)
private[sbt] final class ProgressState(
val progressLines: AtomicReference[Seq[String]],
val padding: AtomicInteger,
val blankZone: Int,
val currentLineBytes: AtomicReference[ArrayBuffer[Byte]],
) {
def this(blankZone: Int) =
this(
new AtomicReference(Nil),
new AtomicInteger(0),
blankZone,
new AtomicReference(new ArrayBuffer[Byte]),
)
def reset(): Unit = {
progressLines.set(Nil)
padding.set(0)
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[util] def clearBytes(): Unit = {
val pad = padding.get
if (currentLineBytes.get.isEmpty && pad > 0) padding.decrementAndGet()
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[util] def addBytes(terminal: Terminal, bytes: ArrayBuffer[Byte]): Unit = {
val previous = currentLineBytes.get
val padding = this.padding.get
val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0
previous ++= bytes
if (padding > 0) {
val newLineCount = terminal.lineCount(new String(previous.toArray))
val diff = newLineCount - prevLineCount
this.padding.set(math.max(padding - diff, 0))
}
}
private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit =
if (terminal.prompt != Prompt.Running && terminal.prompt != Prompt.Batch) {
val prefix = if (terminal.isAnsiSupported) s"$DeleteLine$CursorLeft1000" else ""
val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes
pmpt.foreach(b => printStream.write(b & 0xFF))
}
private[util] def reprint(terminal: Terminal, printStream: PrintStream): Unit = {
printPrompt(terminal, printStream)
if (progressLines.get.nonEmpty) {
val lines = printProgress(terminal, terminal.getLastLine.getOrElse(""))
printStream.print(ClearScreenAfterCursor + lines)
}
}
private[util] def printProgress(
terminal: Terminal,
lastLine: String
): String = {
val previousLines = progressLines.get
if (previousLines.nonEmpty) {
val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val (height, width) = terminal.getLineHeightAndWidth(lastLine)
val left = cursorLeft(1000) // resets the position to the left
val offset = width > 0
val pad = math.max(padding.get - height, 0)
val start = (if (offset) "\n" else "")
val totalSize = currentLength + blankZone + pad
val blank = left + s"\n$DeleteLine" * (totalSize - currentLength)
val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine")
val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0))
val resetCursor = resetCursorUp + left + lastLine
start + blank + lines + resetCursor
} else {
ClearScreenAfterCursor
}
}
}
private[sbt] object ProgressState {
/**
* Receives a new task report and replaces the old one. In the event that the new
* report has fewer lines than the previous report, padding lines are added on top
* so that the console log lines remain contiguous. When a console line is printed
* at the info or greater level, we can decrement the padding because the console
* line will have filled in the blank line.
*/
private[sbt] def updateProgressState(
pe: ProgressEvent,
terminal: Terminal
): Unit = {
val state = terminal.progressState
val isRunning = terminal.prompt == Prompt.Running
val isBatch = terminal.prompt == Prompt.Batch
val isWatch = terminal.prompt == Prompt.Watch
if (terminal.isSupershellEnabled) {
if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) {
terminal.withPrintStream { ps =>
val info = if (isRunning || isBatch && pe.channelName.fold(true)(_ == terminal.name)) {
pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L
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
}
}
val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_))
val previousLines = state.progressLines.getAndSet(info)
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 newPadding = math.max(0, prevSize - currentLength)
state.padding.set(newPadding)
state.printPrompt(terminal, ps)
ps.print(state.printProgress(terminal, lastLine))
ps.flush()
}
} else if (state.progressLines.get.nonEmpty) {
state.progressLines.set(Nil)
terminal.withPrintStream { ps =>
val lastLine = terminal.getLastLine.getOrElse("")
ps.print(lastLine + ClearScreenAfterCursor)
ps.flush()
}
}
}
}
}

View File

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

@ -0,0 +1,194 @@
/*
* 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.PrintStream
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.atomic.{ AtomicInteger, AtomicReference }
import sbt.internal.util.ConsoleAppender.{
ClearScreenAfterCursor,
CursorLeft1000,
DeleteLine,
cursorUp
}
import scala.collection.mutable.ArrayBuffer
import scala.collection.JavaConverters._
private[sbt] final class ProgressState(
val progressLines: AtomicReference[Seq[String]],
val padding: AtomicInteger,
val blankZone: Int,
val currentLineBytes: AtomicReference[ArrayBuffer[Byte]],
) {
def this(blankZone: Int) =
this(
new AtomicReference(Nil),
new AtomicInteger(0),
blankZone,
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 = {
progressLines.set(Nil)
padding.set(0)
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[this] val lineBuffer = new ArrayBlockingQueue[String](300)
private[util] def getLines: Seq[String] = lineBuffer.asScala.toVector
private[this] def appendLine(line: String) = while (!lineBuffer.offer(line)) { lineBuffer.poll }
private[util] def clearBytes(): Unit = {
val pad = padding.get
if (currentLineBytes.get.isEmpty && pad > 0) padding.decrementAndGet()
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[this] val lineSeparatorBytes: Array[Byte] = System.lineSeparator.getBytes("UTF-8")
private[util] def addBytes(terminal: Terminal, bytes: Seq[Byte]): Unit = {
val previous: ArrayBuffer[Byte] = currentLineBytes.get
val padding = this.padding.get
val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0
previous ++= bytes
if (padding > 0) {
val newLineCount = terminal.lineCount(new String(previous.toArray))
val diff = newLineCount - prevLineCount
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)) {
val allLines = lines.split(System.lineSeparator)
allLines.dropRight(1).foreach(appendLine)
allLines.lastOption
.foreach(currentLineBytes.get ++= _.getBytes("UTF-8"))
} else if (lines.contains(System.lineSeparator)) {
lines.split(System.lineSeparator).foreach(appendLine)
}
}
}
private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit =
if (terminal.prompt != Prompt.Running && terminal.prompt != Prompt.Batch) {
val prefix = if (terminal.isAnsiSupported) s"$DeleteLine$CursorLeft1000" else ""
val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes
pmpt.foreach(b => printStream.write(b & 0xFF))
}
private[util] def write(
terminal: Terminal,
bytes: Array[Byte],
printStream: PrintStream,
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(terminal: Terminal, lastLine: String): String = {
val previousLines = progressLines.get
if (previousLines.nonEmpty) {
val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val (height, width) = terminal.getLineHeightAndWidth(lastLine)
val offset = width > 0
val pad = math.max(padding.get - height, 0)
val start = (if (offset) s"\n$CursorLeft1000" else "")
val totalSize = currentLength + blankZone + pad
val blank = CursorLeft1000 + s"\n$DeleteLine" * (totalSize - currentLength)
val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine")
val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0))
val resetCursor = resetCursorUp + CursorLeft1000 + lastLine
start + blank + lines + resetCursor
} else {
ClearScreenAfterCursor
}
}
}
private[sbt] object ProgressState {
/**
* Receives a new task report and replaces the old one. In the event that the new
* report has fewer lines than the previous report, padding lines are added on top
* so that the console log lines remain contiguous. When a console line is printed
* at the info or greater level, we can decrement the padding because the console
* line will have filled in the blank line.
*/
private[sbt] def updateProgressState(
pe: ProgressEvent,
terminal: Terminal
): Unit = {
val state = terminal.progressState
val isAskUser = terminal.prompt.isInstanceOf[Prompt.AskUser]
val isRunning = terminal.prompt == Prompt.Running
val isBatch = terminal.prompt == Prompt.Batch
val isWatch = terminal.prompt == Prompt.Watch
val noPrompt = terminal.prompt == Prompt.NoPrompt
if (terminal.isSupershellEnabled) {
if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) {
terminal.withPrintStream { ps =>
val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name)
val info = if ((isRunning || isBatch || noPrompt) && commandFromThisTerminal) {
pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L
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
}
}
val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_))
val previousLines = state.progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val prevSize = prevLength + state.padding.get
val lastLine =
if (isAskUser) terminal.prompt.render() else terminal.getLastLine.getOrElse("")
state.padding.set(math.max(0, prevSize - currentLength))
state.printPrompt(terminal, ps)
ps.print(state.printProgress(terminal, lastLine))
ps.flush()
}
} else if (state.progressLines.get.nonEmpty) {
state.progressLines.set(Nil)
terminal.withPrintStream { ps =>
val lastLine = terminal.getLastLine.getOrElse("")
ps.print(lastLine + ClearScreenAfterCursor)
ps.flush()
}
}
}
}
}

View File

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

View File

@ -7,19 +7,17 @@
package sbt.internal.util
import java.io.{ InputStream, OutputStream, PrintStream }
import java.io.{ InputStream, InterruptedIOException, OutputStream, PrintStream }
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.{ ConcurrentHashMap, Executors, LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.{ ArrayBlockingQueue, Executors, LinkedBlockingQueue, TimeUnit }
import jline.DefaultTerminal2
import jline.console.ConsoleReader
import sbt.internal.util.ConsoleAppender.{ ClearScreenAfterCursor, CursorLeft1000 }
import scala.annotation.tailrec
import scala.collection.mutable.ArrayBuffer
import scala.util.Try
import scala.util.control.NonFatal
trait Terminal extends AutoCloseable {
@ -57,9 +55,8 @@ trait Terminal extends AutoCloseable {
def inputStream: InputStream
/**
* Gets the input stream for this Terminal. This could be a wrapper around System.in for the
* process or it could be a remote input stream for a network channel.
* @return the input stream.
* Gets the output stream for this Terminal.
* @return the output stream.
*/
def outputStream: OutputStream
@ -112,21 +109,36 @@ trait Terminal extends AutoCloseable {
*/
private[sbt] def getLastLine: Option[String]
private[sbt] def getBooleanCapability(capability: String): Boolean
private[sbt] def getNumericCapability(capability: String): Int
private[sbt] def getStringCapability(capability: String): String
/**
* Returns the buffered lines that have been written to the terminal. The
* main use case is to display the system startup log lines when a client
* connects to a booting server. This could also be used to implement a more
* tmux like experience where multiple clients connect to the same console.
*
* @return the lines
*/
private[sbt] def getLines: Seq[String]
private[sbt] def getBooleanCapability(capability: String, jline3: Boolean): Boolean
private[sbt] def getNumericCapability(capability: String, jline3: Boolean): Integer
private[sbt] def getStringCapability(capability: String, jline3: Boolean): String
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 withRawSystemIn[T](f: => T): T = f
private[sbt] def withRawInput[T](f: => T): T = f
private[sbt] def withCanonicalIn[T](f: => T): T = f
private[sbt] def write(bytes: Int*): Unit
private[sbt] def printStream: PrintStream
private[sbt] def withPrintStream[T](f: PrintStream => T): T
private[sbt] def withRawOutput[R](f: => R): R
private[sbt] def restore(): Unit = {}
private[sbt] val progressState = new ProgressState(1)
private[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Running)
private[sbt] final def prompt: Prompt = promptHolder.get
private[sbt] final def setPrompt(newPrompt: Prompt): Unit = promptHolder.set(newPrompt)
private[sbt] final def setPrompt(newPrompt: Prompt): Unit =
if (prompt != Prompt.NoPrompt) promptHolder.set(newPrompt)
/**
* Returns the number of lines that the input string will cover given the current width of the
@ -142,7 +154,8 @@ trait Terminal extends AutoCloseable {
val len = l.length
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 +201,13 @@ object Terminal {
override def enableInterruptCharacter(): Unit = {}
override def getOutputEncoding: String = null
override def getBooleanCapability(capability: String): Boolean = {
term.getBooleanCapability(capability)
term.getBooleanCapability(capability, jline3 = false)
}
override def getNumericCapability(capability: String): Integer = {
term.getNumericCapability(capability)
term.getNumericCapability(capability, jline3 = false)
}
override def getStringCapability(capability: String): String = {
term.getStringCapability(capability)
term.getStringCapability(capability, jline3 = false)
}
}
}
@ -253,18 +266,45 @@ object Terminal {
*/
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
* @tparam T the result type 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") {
hasProgress.set(isServer)
try withOut(withIn(f))
finally {
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
@ -281,18 +321,26 @@ object Terminal {
override def isEchoEnabled: Boolean = t.isEchoEnabled
override def isSuccessEnabled: Boolean = t.isSuccessEnabled
override def isSupershellEnabled: Boolean = t.isSupershellEnabled
override def getBooleanCapability(capability: String): Boolean =
t.getBooleanCapability(capability)
override def getNumericCapability(capability: String): Int = t.getNumericCapability(capability)
override def getStringCapability(capability: String): String = t.getStringCapability(capability)
override def withRawSystemIn[T](f: => T): T = t.withRawSystemIn(f)
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
t.getBooleanCapability(capability, jline3)
override def getNumericCapability(capability: String, jline3: Boolean): Integer =
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 withRawInput[T](f: => T): T = t.withRawInput(f)
override def withCanonicalIn[T](f: => T): T = t.withCanonicalIn(f)
override def printStream: PrintStream = t.printStream
override def withPrintStream[T](f: PrintStream => T): T = t.withPrintStream(f)
override private[sbt] def withRawOutput[R](f: => R): R = t.withRawOutput(f)
override def restore(): Unit = t.restore()
override def close(): Unit = {}
override private[sbt] def write(bytes: Int*): Unit = t.write(bytes: _*)
override def getLastLine: Option[String] = t.getLastLine
override def getLines: Seq[String] = t.getLines
override private[sbt] def name: String = t.name
}
private[sbt] def get: Terminal = ProxyTerminal
@ -322,18 +370,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[sbt] class WriteableInputStream(in: InputStream, name: String)
extends InputStream
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 =
Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader"))
private[this] val buffer = new LinkedBlockingQueue[Integer]
private[this] val closed = new AtomicBoolean(false)
private[this] val resultQueue = new LinkedBlockingQueue[LinkedBlockingQueue[Int]]
private[this] val waiting = ConcurrentHashMap.newKeySet[LinkedBlockingQueue[Int]]
private[this] val readQueue = new LinkedBlockingQueue[Unit]
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.
* When read is called, we enqueue a `LinkedBlockingQueue[Int]` to which
@ -354,11 +419,14 @@ object Terminal {
*/
private[this] val runnable: Runnable = () => {
@tailrec def impl(): Unit = {
val result = resultQueue.take
val _ = readQueue.take
val b = in.read
// The downstream consumer may have been interrupted. Buffer the result
// 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()
else closed.set(true)
}
@ -370,21 +438,28 @@ object Terminal {
if (closed.get) -1
else
synchronized {
buffer.poll match {
readThread.set(Thread.currentThread)
try buffer.poll match {
case null =>
val result = new LinkedBlockingQueue[Int]
waiting.add(result)
resultQueue.offer(result)
try result.take
val result = new LinkedBlockingQueue[Integer]
waiting.synchronized(waiting.put(result))
readQueue.put(())
try result.take.toInt
catch {
case e: InterruptedException =>
waiting.remove(result)
throw e
-1
}
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 = {
buffer.size
@ -524,7 +599,7 @@ object Terminal {
}
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)"
}
private[this] lazy val isWindows =
@ -592,9 +667,21 @@ object Terminal {
case t: jline.Terminal2 => t
case _ => new DefaultTerminal2(terminal)
}
override def init(): Unit = if (alive) terminal.init()
override def restore(): Unit = if (alive) terminal.restore()
override def reset(): Unit = if (alive) terminal.reset()
override def init(): Unit =
if (alive)
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 getWidth: Int = props.map(_.width).getOrElse(terminal.getWidth)
override def getHeight: Int = props.map(_.height).getOrElse(terminal.getHeight)
@ -650,18 +737,21 @@ object Terminal {
fixTerminalProperty()
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 =
term.withRawSystemIn(super.readLine(prompt, mask))
override def readLine(prompt: String): String = term.withRawSystemIn(super.readLine(prompt))
term.withRawInput(super.readLine(prompt, mask))
override def readLine(prompt: String): String = term.withRawInput(super.readLine(prompt))
}
}
private[sbt] def console: Terminal = consoleTerminalHolder.get match {
def console: Terminal = consoleTerminalHolder.get match {
case null => throw new IllegalStateException("Uninitialized terminal.")
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")
private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine
private class ConsoleTerminal(
@ -669,28 +759,36 @@ object Terminal {
in: InputStream,
out: OutputStream
) 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")
override def getWidth: Int = term.getWidth
override def getHeight: Int = term.getHeight
override def getWidth: Int = system.getSize.getColumns
override def getHeight: Int = system.getSize.getRows
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 getBooleanCapability(capability: String): Boolean =
term.getBooleanCapability(capability)
override def getNumericCapability(capability: String): Int =
term.getNumericCapability(capability)
override def getStringCapability(capability: String): String =
term.getStringCapability(capability)
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
if (jline3) capabilityMap.get(capability).fold(false)(system.getBooleanCapability)
else term.getBooleanCapability(capability)
override def getNumericCapability(capability: String, jline3: Boolean): Integer =
if (jline3) capabilityMap.get(capability).fold(null: Integer)(system.getNumericCapability)
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 def withRawSystemIn[T](f: => T): T = term.synchronized {
try {
term.init()
term.setEchoEnabled(false)
f
} finally {
term.restore()
term.setEchoEnabled(true)
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 withRawInput[T](f: => T): T = term.synchronized {
val prev = JLine3.enterRawMode(system)
try f
catch { case _: InterruptedIOException => throw new InterruptedException } finally {
setAttributes(prev)
}
}
override def isColorEnabled: Boolean =
@ -705,29 +803,35 @@ object Terminal {
case "true" => true
case _ => false
})
override def close(): Unit = {
try system.close()
catch { case NonFatal(_) => }
super.close()
}
}
private[sbt] abstract class TerminalImpl private[sbt] (
val in: InputStream,
val out: OutputStream,
override private[sbt] val name: String
) 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 rawMode = new AtomicBoolean(false)
private[this] val writeLock = new AnyRef
private[this] val writeableInputStream = in match {
case w: WriteableInputStream => w
case _ => new WriteableInputStream(in, name)
}
def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f
override def getLastLine: Option[String] = progressState.currentLine
override def getLines: Seq[String] = progressState.getLines
private val combinedOutputStream = new OutputStream {
override def write(b: Int): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.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 = {
Option(bootOutputStreamHolder.get).foreach(_.write(b, offset, len))
out.write(b, offset, len)
@ -740,54 +844,19 @@ object Terminal {
override val outputStream = new OutputStream {
override def write(b: Int): Unit = throwIfClosed {
writeLock.synchronized {
if (b == Int.MinValue) currentLine.set(new ArrayBuffer[Byte])
else doWrite(Vector((b & 0xFF).toByte))
if (b == 10) combinedOutputStream.flush()
}
write(Array((b & 0xFF).toByte))
}
override def write(b: Array[Byte]): Unit = throwIfClosed(write(b, 0, b.length))
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
throwIfClosed {
writeLock.synchronized {
val lo = math.max(0, off)
val hi = math.min(math.max(off + len, 0), b.length)
doWrite(b.slice(off, off + len).toSeq)
}
}
override def write(b: Array[Byte]): Unit = throwIfClosed {
writeLock.synchronized(doWrite(b))
}
override def write(b: Array[Byte], offset: Int, length: Int): Unit = throwIfClosed {
write(Arrays.copyOfRange(b, offset, offset + length))
}
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 _ =>
}
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 && !rawMode.get)
override private[sbt] val printStream: PrintStream = new LinePrintStream(outputStream)
override def inputStream: InputStream = writeableInputStream
private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*)
@ -801,17 +870,12 @@ object Terminal {
case _ => (0, 0)
}
override def getLastLine: Option[String] = currentLine.get match {
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 = {}
private[sbt] def withRawOutput[R](f: => R): R = {
rawMode.set(true)
try f
finally rawMode.set(false)
}
private[this] val rawPrintStream: PrintStream = new LinePrintStream(combinedOutputStream)
override def withPrintStream[T](f: PrintStream => T): T =
writeLock.synchronized(f(rawPrintStream))
@ -821,12 +885,13 @@ object Terminal {
}
private[sbt] val NullTerminal = new Terminal {
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 getLastLine: Option[String] = None
override def getLines: Seq[String] = Nil
override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0)
override def getNumericCapability(capability: String): Int = -1
override def getStringCapability(capability: String): String = null
override def getNumericCapability(capability: String, jline3: Boolean): Integer = null
override def getStringCapability(capability: String, jline3: Boolean): String = null
override def getWidth: Int = 0
override def inputStream: java.io.InputStream = () => {
try this.synchronized(this.wait)
@ -839,10 +904,14 @@ object Terminal {
override def isSuccessEnabled: Boolean = false
override def isSupershellEnabled: Boolean = false
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] val printStream: java.io.PrintStream =
new PrintStream(outputStream, false)
override private[sbt] def withPrintStream[T](f: java.io.PrintStream => T): T = f(printStream)
override private[sbt] def write(bytes: Int*): Unit = {}
override private[sbt] def withRawOutput[R](f: => R): R = f
}
}

View File

@ -63,4 +63,11 @@ class CleanStringSpec extends FlatSpec {
val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62))
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

@ -66,7 +66,9 @@ final class Console(compiler: AnalyzingCompiler) {
val previous = sys.props.get("scala.color").getOrElse("auto")
try {
sys.props("scala.color") = if (terminal.isColorEnabled) "true" else "false"
terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
terminal.withRawOutput {
terminal.withRawInput(Run.executeTrapExit(console0, log))
}
} finally {
sys.props("scala.color") = previous
}

View File

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

View File

@ -25,6 +25,7 @@ private[sbt] final class ConsoleChannel(
override val userThread: UserThread = new UserThread(this)
private[sbt] def terminal = Terminal.console
if (System.console == null) terminal.setPrompt(Prompt.NoPrompt)
}
private[sbt] object ConsoleChannel {
private[sbt] def defaultName = "console0"

View File

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

View File

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

View File

@ -14,7 +14,7 @@ import java.util.concurrent.Executors
import sbt.State
import sbt.internal.util.{ ConsoleAppender, ProgressEvent, ProgressState, Util }
import sbt.internal.util.Prompt.{ AskUser, Running }
import sbt.internal.util.Prompt.{ AskUser, Loading, Running }
private[sbt] class UserThread(val channel: CommandChannel) extends AutoCloseable {
private[this] val uiThread = new AtomicReference[(UITask, Thread)]
@ -70,8 +70,9 @@ private[sbt] class UserThread(val channel: CommandChannel) extends AutoCloseable
}
val state = consolePromptEvent.state
terminal.prompt match {
case Running => terminal.setPrompt(AskUser(() => UITask.shellPrompt(terminal, state)))
case _ =>
case Loading | Running =>
terminal.setPrompt(AskUser(() => UITask.shellPrompt(terminal, state)))
case _ =>
}
onProgressEvent(ProgressEvent("Info", Vector(), None, None, None))
reset(state)

View File

@ -61,7 +61,7 @@ public final class MetaBuildLoader extends URLClassLoader {
*/
public static MetaBuildLoader makeLoader(final AppProvider appProvider) throws IOException {
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 URL[] interfaceURLs = new URL[3];
final File[] extra =

8
main/src/main/scala/sbt/Defaults.scala Executable file → Normal file
View File

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

View File

@ -78,7 +78,7 @@ private[sbt] object xMain {
BspClient.run(dealiasBaseDirectory(configuration))
} else {
bootServerSocket.foreach(l => Terminal.setBootStreams(l.inputStream, l.outputStream))
Terminal.withStreams {
Terminal.withStreams(true) {
if (clientModByEnv || userCommands.exists(isClient)) {
val args = userCommands.toList.filterNot(isClient)
NetworkClient.run(dealiasBaseDirectory(configuration), args)
@ -114,7 +114,7 @@ private[sbt] object xMain {
case _: ServerAlreadyBootingException
if System.console != null && !Terminal.startedByRemoteClient =>
println("sbt server is already booting. Create a new server? y/n (default y)")
val exit = Terminal.get.withRawSystemIn(System.in.read) match {
val exit = Terminal.get.withRawInput(System.in.read) match {
case 110 => Some(Exit(1))
case _ => None
}
@ -835,7 +835,7 @@ object BuiltinCommands {
@tailrec
private[this] def doLoadFailed(s: State, loadArg: String): State = {
s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)")
val result = try Terminal.get.withRawSystemIn(System.in.read) match {
val result = try Terminal.get.withRawInput(System.in.read) match {
case -1 => 'q'.toInt
case b => b
} catch { case _: ClosedChannelException => 'q' }
@ -913,6 +913,8 @@ object BuiltinCommands {
}
def doLoadProject(s0: State, action: LoadAction.Value): State = {
StandardMain.exchange.unprompt(ConsoleUnpromptEvent(None), force = true)
StandardMain.exchange.channels.foreach(_.terminal.setPrompt(Prompt.Loading))
welcomeBanner(s0)
checkSBTVersionChanged(s0)
val (s1, base) = Project.loadAction(SessionVar.clear(s0), action)
@ -933,7 +935,9 @@ object BuiltinCommands {
SessionSettings.checkSession(session, s2)
val s3 = addCacheStoreFactoryFactory(Project.setProject(session, structure, s2))
val s4 = setupGlobalFileTreeRepository(s3)
CheckBuildSources.init(LintUnused.lintUnusedFunc(s4))
val s5 = CheckBuildSources.init(LintUnused.lintUnusedFunc(s4))
StandardMain.exchange.prompt(ConsolePromptEvent(s5))
s5
}
private val setupGlobalFileTreeRepository: State => State = { state =>
@ -977,9 +981,11 @@ object BuiltinCommands {
val exchange = StandardMain.exchange
if (exchange.channels.exists(ContinuousCommands.isInWatch)) {
val s1 = exchange.run(s0)
def needPrompt(c: CommandChannel) =
ContinuousCommands.isInWatch(c) && !ContinuousCommands.isPending(c)
exchange.channels.foreach {
case c if ContinuousCommands.isPending(c) =>
case c => c.prompt(ConsolePromptEvent(s1))
case c if needPrompt(c) => c.prompt(ConsolePromptEvent(s1))
case _ =>
}
val exec: Exec = getExec(s1, Duration.Inf)
val remaining: List[Exec] =

View File

@ -184,7 +184,8 @@ object MainLoop {
/** This is the main function State transfer function of the sbt command processing. */
def processCommand(exec: Exec, state: State): State = {
val channelName = exec.source map (_.channelName)
StandardMain.exchange notifyStatus
val exchange = StandardMain.exchange
exchange notifyStatus
ExecStatusEvent("Processing", channelName, exec.execId, Vector())
try {
def process(): State = {
@ -197,9 +198,9 @@ object MainLoop {
state.put(sbt.Keys.currentTaskProgress, new Keys.TaskProgress(progress))
} else state
}
StandardMain.exchange.setState(progressState)
StandardMain.exchange.setExec(Some(exec))
StandardMain.exchange.unprompt(ConsoleUnpromptEvent(exec.source))
exchange.setState(progressState)
exchange.setExec(Some(exec))
exchange.unprompt(ConsoleUnpromptEvent(exec.source), force = false)
val newState = Command.process(exec.commandLine, progressState)
if (exec.execId.fold(true)(!_.startsWith(networkExecPrefix)) &&
!exec.commandLine.startsWith(networkExecPrefix)) {
@ -210,26 +211,25 @@ object MainLoop {
newState.remainingCommands.toVector map (_.commandLine),
exitCode(newState, state),
)
StandardMain.exchange.respondStatus(doneEvent)
exchange.respondStatus(doneEvent)
}
StandardMain.exchange.setExec(None)
exchange.setExec(None)
newState.get(sbt.Keys.currentTaskProgress).foreach(_.progress.stop())
newState.remove(sbt.Keys.currentTaskProgress)
}
state.get(CheckBuildSourcesKey) match {
case Some(cbs) =>
if (!cbs.needsReload(state, exec.commandLine)) process()
if (!cbs.needsReload(state, exec)) process()
else {
if (exec.commandLine.startsWith(SetTerminal))
exec +: Exec("reload", None, None) +: state.remove(CheckBuildSourcesKey)
else
Exec("reload", None, None) +: exec +: state.remove(CheckBuildSourcesKey)
val isSetTerminal = exec.commandLine.startsWith(SetTerminal)
if (isSetTerminal) exec +: Exec("reload", None) +: state.remove(CheckBuildSourcesKey)
else Exec("reload", None) +: exec +: state.remove(CheckBuildSourcesKey)
}
case _ => process()
}
} catch {
case err: JsonRpcResponseError =>
StandardMain.exchange.respondError(err, exec.execId, channelName.map(CommandSource(_)))
exchange.respondError(err, exec.execId, channelName.map(CommandSource(_)))
throw err
case err: Throwable =>
val errorEvent = ExecStatusEvent(

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.
impl(interval match {
val exec = impl(interval match {
case d: FiniteDuration => Some(d.fromNow)
case _ => None
}, 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 =
@ -350,7 +357,11 @@ private[sbt] final class CommandExchange {
case c => c.prompt(event)
}
}
def unprompt(event: ConsoleUnpromptEvent): Unit = channels.foreach(_.unprompt(event))
def unprompt(event: ConsoleUnpromptEvent, force: Boolean): Unit = {
if (force)
channels.foreach(c => c.unprompt(event.copy(lastSource = Some(CommandSource(c.name)))))
else channels.foreach(_.unprompt(event))
}
def logMessage(event: LogEvent): Unit = {
channels.foreach {
@ -408,6 +419,10 @@ private[sbt] final class CommandExchange {
case _ =>
}
case _ =>
channels.foreach {
case nc: NetworkChannel => nc.shutdown(true, Some(("", "")))
case c => c.shutdown(false)
}
}
private[sbt] def shutdown(name: String): Unit = {
@ -444,7 +459,9 @@ private[sbt] final class CommandExchange {
case mt: FastTrackTask =>
mt.task match {
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) =>
ContinuousCommands.stopWatchImpl(mt.channel.name)
mt.channel match {
@ -454,6 +471,10 @@ private[sbt] final class CommandExchange {
commandQueue.add(Exec(t, None, None))
case `TerminateAction` => exit(mt)
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 {
case Some(c: NetworkChannel) => c.shutdown(false)
case _ =>

View File

@ -320,7 +320,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
val (nextFileEvent, cleanupFileMonitor): (
Int => Option[(Watch.Event, Watch.Action)],
() => Unit
) = getFileEvents(configs, logger, state, commands, fileStampCache)
) = getFileEvents(configs, logger, state, commands, fileStampCache, channel.name)
val executor = new WatchExecutor(channel.name)
val nextEvent: Int => Watch.Action =
combineInputAndFileEvents(nextInputEvent, nextFileEvent, message, logger, logger, executor)
@ -420,7 +420,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
logger: Logger,
state: State,
commands: Seq[String],
fileStampCache: FileStamp.Cache
fileStampCache: FileStamp.Cache,
channel: String,
)(implicit extracted: Extracted): (Int => Option[(Watch.Event, Watch.Action)], () => Unit) = {
val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild)
val buildGlobs =
@ -465,16 +466,41 @@ private[sbt] object Continuous extends DeprecatedContinuous {
}
}
private[this] val antiEntropyWindow = configs.map(_.watchSettings.antiEntropy).max
private[this] val monitor = FileEventMonitor.antiEntropy(
eventMonitorObservers,
configs.map(_.watchSettings.antiEntropy).max,
antiEntropyWindow,
logger,
quarantinePeriod,
retentionPeriod
)
override def poll(duration: Duration, filter: Event => Boolean): Seq[Event] =
monitor.poll(duration, filter)
private[this] val antiEntropyPollPeriod =
configs.map(_.watchSettings.antiEntropyPollPeriod).max
override def poll(duration: Duration, filter: Event => Boolean): Seq[Event] = {
monitor.poll(duration, filter) match {
case s if s.nonEmpty =>
val limit = antiEntropyWindow.fromNow
/*
* File events may come in bursts so we poll for a short time to see if there
* are other changes detected in the burst. As soon as no changes are detected
* during the polling window, we return all of the detected events. The polling
* period is by default 5 milliseconds which is short enough to detect bursts
* induced by commands like git rebase but fast enough to not lead to a noticable
* increase in latency.
*/
@tailrec def aggregate(res: Seq[Event]): Seq[Event] =
if (limit.isOverdue) res
else {
monitor.poll(antiEntropyPollPeriod) match {
case s if s.nonEmpty => aggregate(res ++ s)
case _ => res
}
}
aggregate(s)
case s => s
}
}
override def close(): Unit = {
configHandle.close()
@ -526,7 +552,16 @@ private[sbt] object Continuous extends DeprecatedContinuous {
}
if (buildGlobs.exists(_.matches(path))) {
getWatchEvent(forceTrigger = false).map(e => e -> Watch.Reload).toSeq
getWatchEvent(forceTrigger = false).flatMap { e =>
state.get(CheckBuildSources.CheckBuildSourcesKey) match {
case Some(cbs) =>
if (cbs.needsReload(state, Exec("", Some(CommandSource(channel)))))
Some(e -> Watch.Reload)
else None
case None =>
Some(e -> Watch.Reload)
}
}.toSeq
} else {
val acceptedConfigParameters = configs.flatMap { config =>
config.inputs().flatMap {
@ -727,7 +762,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
}
}
terminal.withRawSystemIn(impl())
terminal.withRawInput(impl())
}
}
@ -858,6 +893,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
// alternative would be SettingKey[() => InputStream], but that doesn't feel right because
// one might want the InputStream to depend on other tasks.
val inputStream: Option[TaskKey[InputStream]] = key.get(watchInputStream)
val antiEntropyPollPeriod: FiniteDuration =
key.get(watchAntiEntropyPollPeriod).getOrElse(Watch.defaultAntiEntropyPollPeriod)
}
/**
@ -1183,7 +1220,6 @@ private[sbt] object ContinuousCommands {
) extends Thread(s"sbt-${channel.name}-watch-ui-thread")
with UITask {
override private[sbt] def reader: UITask.Reader = () => {
channel.terminal.printStream.write(Int.MinValue)
def stop = Right(s"${ContinuousCommands.stopWatch} ${channel.name}")
val exitAction: Watch.Action = {
Watch.apply(
@ -1202,7 +1238,7 @@ private[sbt] object ContinuousCommands {
case Watch.Trigger => Right(s"$runWatch ${channel.name}")
case Watch.Reload =>
val rewatch = s"$ContinuousExecutePrefix ${ws.count} ${cs.commands mkString "; "}"
stop.map(_ :: "reload" :: rewatch :: Nil mkString "; ")
stop.map(_ :: s"$SetTerminal ${channel.name}" :: "reload" :: rewatch :: Nil mkString "; ")
case Watch.Prompt => stop.map(_ :: s"$PromptChannel ${channel.name}" :: Nil mkString ";")
case Watch.Run(commands) =>
stop.map(_ +: commands.map(_.commandLine).filter(_.nonEmpty) mkString "; ")

View File

@ -53,11 +53,12 @@ private[sbt] class TaskProgress private ()
if (firstTime.compareAndSet(true, activeExceedingThreshold.isEmpty)) threshold
else sleepDuration
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)
while (task != null) {
if (containsSkipTasks(Vector(task)) || lastTaskCount.get == 0) doReport()
task = tasks.poll
tasks.clear()
}
}
} catch {

View File

@ -10,13 +10,13 @@ package internal.nio
import java.nio.file.Path
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import sbt.BasicCommandStrings.{ RebootCommand, Shutdown, TerminateAction }
import sbt.BasicCommandStrings.{ RebootCommand, SetTerminal, Shutdown, TerminateAction }
import sbt.Keys.{ baseDirectory, pollInterval, state }
import sbt.Scope.Global
import sbt.SlashSyntax0._
import sbt.internal.CommandStrings.LoadProject
import sbt.internal.SysProp
import sbt.internal.util.AttributeKey
import sbt.internal.util.{ AttributeKey, Terminal }
import sbt.io.syntax._
import sbt.nio.FileChanges
import sbt.nio.FileStamp
@ -24,9 +24,11 @@ import sbt.nio.Keys._
import sbt.nio.file.{ FileAttributes, FileTreeView, Glob, ** }
import sbt.nio.file.syntax._
import sbt.nio.Settings
import sbt.util.Logger
import scala.annotation.tailrec
import scala.concurrent.duration.{ Deadline => SDeadline, _ }
import scala.io.AnsiColor
/**
* This class is used to determine whether sbt needs to automatically reload
@ -92,19 +94,41 @@ private[sbt] class CheckBuildSources extends AutoCloseable {
val filter = (c: String) =>
c == LoadProject || c == RebootCommand || c == TerminateAction || c == Shutdown ||
c.startsWith("sbtReboot")
val res = !commands.exists(filter)
if (!res) {
val resetState = commands.exists(filter)
if (resetState) {
previousStamps.set(getStamps(force = true))
needUpdate.set(false)
}
res
// We don't need to do a check since we just updated the stamps since
// we are about to perform a reload or reboot.
!resetState
}
@inline private def forceCheck = fileTreeRepository.isEmpty
private[sbt] def needsReload(state: State, cmd: String) = {
private[sbt] def needsReload(
state: State,
exec: Exec
): Boolean = {
val isSetTerminal = exec.commandLine.startsWith(SetTerminal)
val name =
if (isSetTerminal)
exec.commandLine.split(s"$SetTerminal ").lastOption.filterNot(_.isEmpty)
else exec.source.map(_.channelName)
val loggerOrTerminal =
name.flatMap(StandardMain.exchange.channelForName(_).map(_.terminal)) match {
case Some(t) => Right(t)
case _ => Left(state.globalLogging.full)
}
needsReload(state, loggerOrTerminal, exec.commandLine)
}
private def needsReload(
state: State,
loggerOrTerminal: Either[Logger, Terminal],
cmd: String
): Boolean = {
(needCheck(state, cmd) && (forceCheck || needUpdate.compareAndSet(true, false))) && {
val extracted = Project.extract(state)
val onChanges = extracted.get(Global / onChangedBuildSource)
val logger = state.globalLogging.full
val current = getStamps(force = false)
val previous = previousStamps.getAndSet(current)
Settings.changedFiles(previous, current) match {
@ -120,14 +144,24 @@ private[sbt] class CheckBuildSources extends AutoCloseable {
else "")
val prefix = rawPrefix.linesIterator.filterNot(_.trim.isEmpty).mkString("\n")
if (onChanges == ReloadOnSourceChanges) {
logger.info(s"$prefix\nReloading sbt...")
val msg = s"$prefix\nReloading sbt..."
loggerOrTerminal match {
case Right(t) => msg.linesIterator.foreach(l => t.printStream.println(s"[info] $l"))
case Left(l) => l.info(msg)
}
true
} else {
val tail = "Apply these changes by running `reload`.\nAutomatically reload the " +
"build when source changes are detected by setting " +
"`Global / onChangedBuildSource := ReloadOnSourceChanges`.\nDisable this " +
"warning by setting `Global / onChangedBuildSource := IgnoreSourceChanges`."
logger.warn(s"$prefix\n$tail")
val msg = s"$prefix\n$tail"
loggerOrTerminal match {
case Right(t) =>
val prefix = s"[${Def.withColor("warn", Some(AnsiColor.YELLOW), t.isColorEnabled)}]"
msg.linesIterator.foreach(l => t.printStream.println(s"$prefix $l"))
case Left(l) => l.warn(msg)
}
false
}
case _ => false
@ -158,8 +192,9 @@ private[sbt] object CheckBuildSources {
private[sbt] def needReloadImpl: Def.Initialize[Task[StateTransform]] = Def.task {
val st = state.value
st.get(CheckBuildSourcesKey) match {
case Some(cbs) if (cbs.needsReload(st, "")) => StateTransform("reload" :: (_: State))
case _ => StateTransform(identity)
case Some(cbs) if (cbs.needsReload(st, Exec("", None))) =>
StateTransform("reload" :: (_: State))
case _ => StateTransform(identity)
}
}
private[sbt] def buildSourceFileInputs: Def.Initialize[Seq[Glob]] = Def.setting {

View File

@ -12,7 +12,7 @@ package server
import java.io.{ IOException, InputStream, OutputStream }
import java.net.{ Socket, SocketTimeoutException }
import java.nio.channels.ClosedChannelException
import java.util.concurrent.{ ConcurrentHashMap, LinkedBlockingQueue }
import java.util.concurrent.{ ConcurrentHashMap, Executors, LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import sbt.BasicCommandStrings.{ Shutdown, TerminateAction }
@ -99,7 +99,6 @@ final class NetworkChannel(
addFastTrackTask(attach)
}
private[sbt] def prompt(): Unit = {
terminal.setPrompt(Prompt.Running)
interactive.set(true)
jsonRpcNotify(promptChannel, "")
}
@ -575,6 +574,7 @@ final class NetworkChannel(
catch { case _: IOException => }
running.set(false)
out.close()
outputStream.close()
thread.interrupt()
writeThread.interrupt()
}
@ -611,7 +611,7 @@ final class NetworkChannel(
private[sbt] def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit = {
val m =
JsonRpcNotificationMessage("2.0", method, Option(Converter.toJson[A](params).get))
log.debug(s"jsonRpcNotify: $m")
if (method != Serialization.systemOut) log.debug(s"jsonRpcNotify: $m")
val bytes = Serialization.serializeNotificationMessage(m)
publishBytes(bytes)
}
@ -640,28 +640,60 @@ final class NetworkChannel(
case -1 => throw new ClosedChannelException()
case b => b
}
} catch { case _: IOException => -1 }
} catch { case e: IOException => -1 }
}
override def available(): Int = inputBuffer.size
}
import sjsonnew.BasicJsonProtocol._
import scala.collection.JavaConverters._
private[this] lazy val outputStream: OutputStream = new OutputStream {
private[this] val buffer = new LinkedBlockingQueue[Byte]()
override def write(b: Int): Unit = buffer.put(b.toByte)
override def flush(): Unit = {
jsonRpcNotify(Serialization.systemOut, buffer.asScala)
buffer.clear()
private[this] lazy val outputStream: OutputStream with AutoCloseable = new OutputStream
with AutoCloseable {
/*
* We buffer calls to flush to the remote client so that it is called at most
* once every 20 milliseconds. This is done because many terminals seem to flicker
* and display ghost characters if we flush to the remote client too often. The
* json protocol is a bit bulky so this will also reduce the total number of
* bytes that are written to the named pipe or unix domain socket. The buffer
* period of 20 milliseconds was arbitrarily chosen and could be tuned in the future.
* The thinking is that writes tend to be bursty so a twenty millisecond window is
* probably long enough to catch each burst but short enough to not introduce
* noticeable latency.
*/
private[this] val executor =
Executors.newSingleThreadScheduledExecutor(
r => new Thread(r, s"$name-output-buffer-timer-thread")
)
private[this] val buffer = new LinkedBlockingQueue[Byte]
private[this] val future = new AtomicReference[java.util.concurrent.Future[_]]
override def close(): Unit = Util.ignoreResult(executor.shutdownNow())
override def write(b: Int): Unit = buffer.synchronized {
buffer.put(b.toByte)
}
override def write(b: Array[Byte]): Unit = write(b, 0, b.length)
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
var i = off
while (i < len) {
buffer.put(b(i))
i += 1
override def flush(): Unit = {
future.get match {
case null =>
future.set(
executor.schedule(
(() => {
future.set(null)
val list = new java.util.ArrayList[Byte]
buffer.synchronized(buffer.drainTo(list))
jsonRpcNotify(Serialization.systemOut, list.asScala.toSeq)
}): Runnable,
20,
TimeUnit.MILLISECONDS
)
)
case f =>
}
}
override def write(b: Array[Byte]): Unit = buffer.synchronized {
b.foreach(buffer.put)
}
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
write(java.util.Arrays.copyOfRange(b, off, off + len))
}
}
private class NetworkTerminal extends TerminalImpl(inputStream, outputStream, name) {
private[this] val pending = new AtomicBoolean(false)
@ -741,25 +773,82 @@ final class NetworkChannel(
Some(result(queue.take))
}
}
override def getBooleanCapability(capability: String): Boolean =
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
getCapability(
TerminalCapabilitiesQuery(boolean = Some(capability), numeric = None, string = None),
TerminalCapabilitiesQuery(
boolean = Some(capability),
numeric = None,
string = None,
jline3
),
_.boolean.getOrElse(false)
).getOrElse(false)
override def getNumericCapability(capability: String): Int =
override def getNumericCapability(capability: String, jline3: Boolean): Integer =
getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = Some(capability), string = None),
_.numeric.getOrElse(-1)
).getOrElse(-1)
override def getStringCapability(capability: String): String =
TerminalCapabilitiesQuery(
boolean = None,
numeric = Some(capability),
string = None,
jline3
),
(_: TerminalCapabilitiesResponse).numeric.map(Integer.valueOf).getOrElse(-1: Integer)
).getOrElse(-1: Integer)
override def getStringCapability(capability: String, jline3: Boolean): String =
getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = None, string = Some(capability)),
TerminalCapabilitiesQuery(
boolean = None,
numeric = None,
string = Some(capability),
jline3
),
_.string.flatMap {
case "null" => None
case s => Some(s)
}.orNull
).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 close(): Unit = if (closed.compareAndSet(false, true)) {
val threads = blockedThreads.synchronized {

View File

@ -25,16 +25,27 @@ import sbt.protocol.Serialization.{
import sjsonnew.support.scalajson.unsafe.Converter
import sbt.protocol.{
Attach,
TerminalAttributesQuery,
TerminalAttributesResponse,
TerminalCapabilitiesQuery,
TerminalCapabilitiesResponse,
TerminalPropertiesResponse
TerminalPropertiesResponse,
TerminalSetAttributesCommand,
TerminalSetSizeCommand,
}
import sbt.protocol.codec.JsonProtocol._
object VirtualTerminal {
private[this] val pendingTerminalProperties =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalPropertiesResponse]]()
private[this] val pendingTerminalCapabilities =
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(
channelName: String,
jsonRpcRequest: (String, String, String) => Unit
@ -70,6 +81,39 @@ object VirtualTerminal {
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 =>
ServerIntent(requestHandler(cb), responseHandler(cb), notificationHandler(cb))
}
@ -77,7 +121,6 @@ object VirtualTerminal {
private val requestHandler: Handler[JsonRpcRequestMessage] =
callback => {
case r if r.method == attach =>
import sbt.protocol.codec.JsonProtocol.AttachFormat
val isInteractive = r.params
.flatMap(Converter.fromJson[Attach](_).toOption.map(_.interactive))
.exists(identity)
@ -89,7 +132,6 @@ object VirtualTerminal {
private val responseHandler: Handler[JsonRpcResponseMessage] =
callback => {
case r if pendingTerminalProperties.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response =
r.result.flatMap(Converter.fromJson[TerminalPropertiesResponse](_).toOption)
pendingTerminalProperties.remove((callback.name, r.id)) match {
@ -97,7 +139,6 @@ object VirtualTerminal {
case buffer => response.foreach(buffer.put)
}
case r if pendingTerminalCapabilities.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response =
r.result.flatMap(
Converter.fromJson[TerminalCapabilitiesResponse](_).toOption
@ -107,6 +148,24 @@ object VirtualTerminal {
case buffer =>
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] =
callback => {

View File

@ -67,6 +67,9 @@ object Keys {
val watchAntiEntropyRetentionPeriod = settingKey[FiniteDuration](
"Wall clock Duration for which a FileEventMonitor will store anti-entropy events. This prevents spurious triggers when a task takes a long time to run. Higher values will consume more memory but make spurious triggers less likely."
).withRank(BMinusSetting)
val watchAntiEntropyPollPeriod = settingKey[FiniteDuration](
"The duration for which sbt will poll for file events during the window in which sbt is buffering file events"
)
val onChangedBuildSource = settingKey[WatchBuildSourceOption](
"Determines what to do if the sbt meta build sources have changed"
).withRank(DSetting)

View File

@ -450,6 +450,14 @@ object Watch {
*/
final val defaultAntiEntropy: FiniteDuration = 500.milliseconds
/**
* The duration for which we will poll for new file events when we are buffering events
* after an initial event has been detected to avoid spurious rebuilds.
*
* If this value is ever updated, please update the comment in Continuous.getFileEvents.
*/
final val defaultAntiEntropyPollPeriod: FiniteDuration = 5.milliseconds
/**
* The duration in wall clock time for which a FileEventMonitor will retain anti-entropy
* events for files. This is an implementation detail of the FileEventMonitor. It should
@ -613,5 +621,6 @@ object Watch {
watchForceTriggerOnAnyChange :== false,
watchPersistFileStamps := (sbt.Keys.turbo in ThisBuild).value,
watchTriggers :== Nil,
watchAntiEntropyPollPeriod := Watch.defaultAntiEntropyPollPeriod,
)
}

View File

@ -84,10 +84,12 @@ object Dependencies {
val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash")
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 scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0"
val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1"
val specs2 = "org.specs2" %% "specs2-junit" % "4.10.0"
val junit = "junit" % "junit" % "4.11"
val scalaVerify = "com.eed3si9n.verify" %% "verify" % "0.2.0"
val templateResolverApi = "org.scala-sbt" % "template-resolver" % "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 (
val boolean: 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 {
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
}
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 = {
"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 = {
new TerminalCapabilitiesQuery(boolean, numeric, string)
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, jline3)
}
def withBoolean(boolean: Option[String]): TerminalCapabilitiesQuery = {
copy(boolean = boolean)
@ -42,9 +43,12 @@ final class TerminalCapabilitiesQuery private (
def withString(string: String): TerminalCapabilitiesQuery = {
copy(string = Option(string))
}
def withJline3(jline3: Boolean): TerminalCapabilitiesQuery = {
copy(jline3 = jline3)
}
}
object TerminalCapabilitiesQuery {
def apply(boolean: Option[String], numeric: Option[String], string: Option[String]): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string)
def apply(boolean: String, numeric: String, string: String): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(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, 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
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 =>
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")
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] = 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
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 =>
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")
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] = 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.AttachFormats
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.CompletionParamsFormats
with sbt.protocol.codec.ChannelAcceptedEventFormats
@ -20,6 +23,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
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
with sbt.protocol.codec.EventMessageFormats
with sbt.protocol.codec.SettingQueryResponseFormats
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 numeric = unbuilder.readField[Option[String]]("numeric")
val string = unbuilder.readField[Option[String]]("string")
val jline3 = unbuilder.readField[Boolean]("jline3")
unbuilder.endObject()
sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string)
sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string, jline3)
case 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("numeric", obj.numeric)
builder.addField("string", obj.string)
builder.addField("jline3", obj.jline3)
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 {
width: Int!
height: Int!
isAnsiSupported: Boolean!
isColorEnabled: Boolean!
isSupershellEnabled: Boolean!
isEchoEnabled: Boolean!
width: Int!
height: Int!
isAnsiSupported: Boolean!
isColorEnabled: Boolean!
isSupershellEnabled: Boolean!
isEchoEnabled: Boolean!
}
type TerminalCapabilitiesQuery implements CommandMessage {
boolean: String
numeric: String
string: String
boolean: String
numeric: String
string: String
jline3: Boolean!
}
type TerminalCapabilitiesResponse implements EventMessage {
boolean: Boolean
numeric: Int
string: String
boolean: Boolean
numeric: Int
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"
val systemIn = "sbt/systemIn"
val systemOut = "sbt/systemOut"
val systemOutFlush = "sbt/systemOutFlush"
val terminalPropertiesQuery = "sbt/terminalPropertiesQuery"
val terminalPropertiesResponse = "sbt/terminalPropertiesResponse"
val terminalCapabilities = "sbt/terminalCapabilities"
@ -34,6 +35,9 @@ object Serialization {
val attachResponse = "sbt/attachResponse"
val cancelRequest = "sbt/cancelRequest"
val promptChannel = "sbt/promptChannel"
val setTerminalAttributes = "sbt/setTerminalAttributes"
val getTerminalAttributes = "sbt/getTerminalAttributes"
val setTerminalSize = "sbt/setTerminalSize"
val CancelAll = "__CancelAll"
@deprecated("unused", since = "1.4.0")

View File

@ -9,6 +9,7 @@ package sbt
import sbt.internal.util.ConsoleAppender.ClearScreenAfterCursor
import sbt.internal.util.Util.{ AnyOps, none }
import scala.annotation.tailrec
object SelectMainClass {
// Some(SimpleReader.readLine _)
@ -21,14 +22,22 @@ object SelectMainClass {
case head :: Nil => Some(head)
case multiple =>
promptIfMultipleChoices.flatMap { prompt =>
val header = "\nMultiple main classes detected. Select one to run:\n"
val classes = multiple.zipWithIndex
.map { case (className, index) => s" [${index + 1}] $className" }
.mkString("\n")
println(ClearScreenAfterCursor + header + classes + "\n")
val line = trim(prompt("Enter number: "))
toInt(line, multiple.length) map multiple.apply
@tailrec def loop(): Option[String] = {
val header = "\nMultiple main classes detected. Select one to run:\n"
val classes = multiple.zipWithIndex
.map { case (className, index) => s" [${index + 1}] $className" }
.mkString("\n")
println(ClearScreenAfterCursor + header + classes + "\n")
val line = trim(prompt("Enter number: "))
// An empty line usually means the user typed <ctrl+c>
if (line.nonEmpty) {
toInt(line, multiple.length) map multiple.apply match {
case None => loop()
case r => r
}
} else None
}
loop()
}
}
}
@ -44,7 +53,7 @@ object SelectMainClass {
}
} catch {
case nfe: NumberFormatException =>
println("Invalid number: " + nfe.toString)
println(s"Invalid number: '$s'")
none
}
}

View File

@ -16,4 +16,11 @@ failingTask := {
throw new IllegalStateException("failed")
}
onChangedBuildSource := ReloadOnSourceChanges
watchOnIteration := { (count, project, commands) =>
val extra = baseDirectory.value / "extra.sbt"
IO.copyFile(baseDirectory.value / "changes" / "extra.sbt", extra, CopyOptions().withOverwrite(true))
Watch.defaultStartWatch(count, project, commands).foreach(_.linesIterator.foreach(l => println(s"[info] $l")))
Watch.Ignore
}
Global / onChangedBuildSource := ReloadOnSourceChanges

View File

@ -1,7 +1,5 @@
Compile / compile := {
Count.increment()
// Trigger a new build by updating the last modified time
val extra = baseDirectory.value / "extra.sbt"
IO.copyFile(baseDirectory.value / "changes" / "extra.sbt", extra, CopyOptions().withOverwrite(true))
(Compile / compile).value
}