Merge pull request #5711 from eatkins/jline3-console

Support scala 2.13 and dotty console in thin client
This commit is contained in:
eugene yokota 2020-08-09 23:33:58 -04:00 committed by GitHub
commit 31bea086aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 829 additions and 375 deletions

View File

@ -304,7 +304,7 @@ val completeProj = (project in file("internal") / "util-complete")
testedBaseSettings,
name := "Completion",
libraryDependencies += jline,
libraryDependencies += jline3,
libraryDependencies += jline3Reader,
mimaSettings,
// Parser is used publicly, so we can't break bincompat.
mimaBinaryIssueFilters := Seq(
@ -366,7 +366,8 @@ lazy val utilLogging = (project in file("internal") / "util-logging")
libraryDependencies ++=
Seq(
jline,
jline3,
jline3Terminal,
jline3Jansi,
log4jApi,
log4jCore,
disruptor,
@ -661,6 +662,7 @@ lazy val actionsProj = (project in file("main-actions"))
testedBaseSettings,
name := "Actions",
libraryDependencies += sjsonNewScalaJson.value,
libraryDependencies += jline3Terminal,
mimaSettings,
mimaBinaryIssueFilters ++= Seq(
// Removed unused private[sbt] nested class
@ -1017,6 +1019,7 @@ lazy val mainProj = (project in file("main"))
// internal logging apis,
exclude[IncompatibleSignatureProblem]("sbt.internal.LogManager*"),
exclude[MissingTypesProblem]("sbt.internal.RelayAppender"),
exclude[MissingClassProblem]("sbt.internal.TaskProgress$ProgressThread")
)
)
.configure(
@ -1103,7 +1106,6 @@ lazy val sbtClientProj = (project in file("client"))
crossPaths := false,
exportJars := true,
libraryDependencies += jansi,
libraryDependencies += jline3Jansi,
libraryDependencies += scalatest % "test",
/*
* On windows, the raw classpath is too large to be a command argument to an

View File

@ -73,7 +73,6 @@ object LineReader {
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

View File

@ -0,0 +1,21 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util;
import org.jline.terminal.TerminalBuilder;
/**
* This exists to a provide a wrapper to TerminalBuilder.setTerminalOverride that will not emit a
* deprecation warning when called from scala.
*/
public class DeprecatedJLine {
@SuppressWarnings("deprecation")
public static void setTerminalOverride(final org.jline.terminal.Terminal terminal) {
TerminalBuilder.setTerminalOverride(terminal);
}
}

View File

@ -33,6 +33,7 @@ object ConsoleOut {
override def println(s: String): Unit = get.println(s)
override def println(): Unit = get.println()
override def flush(): Unit = get.flush()
override def toString: String = s"ProxyConsoleOut"
}
def overwriteContaining(s: String): (String, String) => Boolean =
@ -70,6 +71,7 @@ object ConsoleOut {
last = Some(s)
current.setLength(0)
}
override def toString: String = s"SystemOutOverwrite@${System.identityHashCode(this)}"
}
def terminalOut: ConsoleOut = new ConsoleOut {
@ -78,6 +80,7 @@ object ConsoleOut {
override def println(s: String): Unit = Terminal.get.printStream.println(s)
override def println(): Unit = Terminal.get.printStream.println()
override def flush(): Unit = Terminal.get.printStream.flush()
override def toString: String = s"TerminalOut"
}
private[this] val consoleOutPerTerminal = new ConcurrentHashMap[Terminal, ConsoleOut]
@ -89,6 +92,7 @@ object ConsoleOut {
override def println(s: String): Unit = terminal.printStream.println(s)
override def println(): Unit = terminal.printStream.println()
override def flush(): Unit = terminal.printStream.flush()
override def toString: String = s"TerminalOut($terminal)"
}
consoleOutPerTerminal.put(terminal, res)
res
@ -100,6 +104,7 @@ object ConsoleOut {
def println(s: String) = out.println(s)
def println() = out.println()
def flush() = out.flush()
override def toString: String = s"PrintStreamConsoleOut($out)"
}
def printWriterOut(out: PrintWriter): ConsoleOut = new ConsoleOut {
val lockObject = out
@ -107,6 +112,7 @@ object ConsoleOut {
def println(s: String) = { out.println(s); flush() }
def println() = { out.println(); flush() }
def flush() = { out.flush() }
override def toString: String = s"PrintWriterConsoleOut($out)"
}
def bufferedWriterOut(out: BufferedWriter): ConsoleOut = new ConsoleOut {
val lockObject = out
@ -114,5 +120,6 @@ object ConsoleOut {
def println(s: String) = { out.write(s); println() }
def println() = { out.newLine(); flush() }
def flush() = { out.flush() }
override def toString: String = s"BufferedWriterConsoleOut($out)"
}
}

View File

@ -7,12 +7,12 @@
package sbt.internal.util
import java.io.{ EOFException, InputStream, OutputStream, PrintWriter }
import java.io.{ InputStream, OutputStream, PrintWriter }
import java.nio.charset.Charset
import java.util.{ Arrays, EnumSet }
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import org.jline.utils.InfoCmp.Capability
import org.jline.utils.{ NonBlocking, OSUtils }
import org.jline.utils.{ ClosedException, NonBlockingReader, OSUtils }
import org.jline.terminal.{ Attributes, Size, Terminal => JTerminal }
import org.jline.terminal.Terminal.SignalHandler
import org.jline.terminal.impl.AbstractTerminal
@ -20,8 +20,9 @@ import org.jline.terminal.impl.jansi.JansiSupportImpl
import org.jline.terminal.impl.jansi.win.JansiWinSysTerminal
import scala.collection.JavaConverters._
import scala.util.Try
import java.util.concurrent.LinkedBlockingQueue
private[util] object JLine3 {
private[sbt] object JLine3 {
private val capabilityMap = Capability
.values()
.map { c =>
@ -77,6 +78,8 @@ private[util] object JLine3 {
new AbstractTerminal(term.name, "ansi", Charset.forName("UTF-8"), SignalHandler.SIG_DFL) {
val closed = new AtomicBoolean(false)
setOnClose { () =>
doClose()
reader.close()
if (closed.compareAndSet(false, true)) {
// This is necessary to shutdown the non blocking input reader
// so that it doesn't keep blocking
@ -89,29 +92,76 @@ private[util] object JLine3 {
parseInfoCmp()
override val input: InputStream = new InputStream {
override def read: Int = {
val res = try term.inputStream.read
catch { case _: InterruptedException => -2 }
val res = term.inputStream match {
case w: Terminal.WriteableInputStream =>
val result = new LinkedBlockingQueue[Integer]
try {
w.read(result)
result.poll match {
case null => throw new ClosedException
case i => i.toInt
}
} catch {
case _: InterruptedException =>
w.cancel()
throw new ClosedException
}
case _ => throw new ClosedException
}
if (res == 4 && term.prompt.render().endsWith(term.prompt.mkPrompt()))
throw new EOFException
throw new ClosedException
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 =>
ps.write(b)
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 reader = new NonBlockingReader {
val buffer = new LinkedBlockingQueue[Integer]
val thread = new AtomicReference[Thread]
private def fillBuffer(): Unit = thread.synchronized {
thread.set(Thread.currentThread)
buffer.put(
try input.read()
catch { case _: InterruptedException => -3 }
)
}
override def close(): Unit = thread.get match {
case null =>
case t => t.interrupt()
}
override def read(timeout: Long, peek: Boolean) = {
if (buffer.isEmpty && !peek) fillBuffer()
(if (peek) buffer.peek else buffer.take) match {
case null => -2
case i => if (i == -3) throw new ClosedException else i
}
}
override def peek(timeout: Long): Int = buffer.peek() match {
case null => -1
case i => i.toInt
}
override def readBuffered(buf: Array[Char]): Int = {
if (buffer.isEmpty) fillBuffer()
buffer.take match {
case i if i == -1 => -1
case i =>
buf(0) = i.toChar
1
}
}
}
override val writer: PrintWriter = new PrintWriter(output, true)
/*
* For now assume that the terminal capabilities for client and server

View File

@ -26,14 +26,16 @@ private[sbt] final class ProgressState(
val padding: AtomicInteger,
val blankZone: Int,
val currentLineBytes: AtomicReference[ArrayBuffer[Byte]],
val maxItems: Int,
) {
def this(blankZone: Int) =
this(
new AtomicReference(Nil),
new AtomicInteger(0),
blankZone,
new AtomicReference(new ArrayBuffer[Byte]),
)
def this(blankZone: Int, maxItems: Int) = this(
new AtomicReference(Nil),
new AtomicInteger(0),
blankZone,
new AtomicReference(new ArrayBuffer[Byte]),
maxItems,
)
def this(blankZone: Int) = this(blankZone, 8)
def currentLine: Option[String] =
new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption
.map(EscHelpers.stripColorsAndMoves)
@ -78,7 +80,7 @@ private[sbt] final class ProgressState(
}
private[util] def getPrompt(terminal: Terminal): Array[Byte] = {
if (terminal.prompt != Prompt.Running && terminal.prompt != Prompt.Batch) {
if (terminal.prompt.isInstanceOf[Prompt.AskUser]) {
val prefix = if (terminal.isAnsiSupported) s"$DeleteLine$CursorLeft1000" else ""
prefix.getBytes ++ terminal.prompt.render().getBytes("UTF-8")
} else Array.empty
@ -108,8 +110,8 @@ private[sbt] final class ProgressState(
val lines = printProgress(terminal, lastLine)
toWrite ++= (ClearScreenAfterCursor + lines).getBytes("UTF-8")
}
toWrite ++= getPrompt(terminal)
}
toWrite ++= getPrompt(terminal)
printStream.write(toWrite.toArray)
printStream.flush()
} else printStream.write(bytes)
@ -136,6 +138,9 @@ private[sbt] final class ProgressState(
}
private[sbt] object ProgressState {
private val MIN_COMMAND_WIDTH = 10
private val SERVER_IS_RUNNING = "sbt server is running "
private val SERVER_IS_RUNNING_LENGTH = SERVER_IS_RUNNING.length + 2
/**
* Receives a new task report and replaces the old one. In the event that the new
@ -158,15 +163,24 @@ private[sbt] object ProgressState {
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 info = if (commandFromThisTerminal) {
val base = pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L
s" | => ${item.name} ${elapsed}s"
}
val limit = state.maxItems
if (base.size > limit)
s" | ... (${base.size - limit} other tasks)" +: base.takeRight(limit)
else base
} else {
pe.command.toSeq.flatMap { cmd =>
val width = terminal.getWidth
val sanitized = if ((cmd.length + SERVER_IS_RUNNING_LENGTH) > width) {
if (SERVER_IS_RUNNING_LENGTH + cmd.length < width) cmd
else cmd.take(MIN_COMMAND_WIDTH) + "..."
} else cmd
val tail = if (isWatch) Nil else "enter 'cancel' to stop evaluation" :: Nil
s"sbt server is running '$cmd'" :: tail
s"$SERVER_IS_RUNNING '$sanitized'" :: tail
}
}

View File

@ -34,5 +34,6 @@ private[sbt] object Prompt {
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 Pending extends NoPrompt
private[sbt] case object NoPrompt extends NoPrompt
}

View File

@ -11,11 +11,12 @@ import java.io.{ InputStream, InterruptedIOException, IOException, OutputStream,
import java.nio.channels.ClosedChannelException
import java.util.{ Arrays, Locale }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.{ ArrayBlockingQueue, Executors, LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.{ Executors, LinkedBlockingQueue, TimeUnit }
import jline.DefaultTerminal2
import jline.console.ConsoleReader
import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.Try
import scala.util.control.NonFatal
@ -140,8 +141,8 @@ trait Terminal extends AutoCloseable {
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] def progressState: ProgressState
private[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Pending)
private[sbt] final def prompt: Prompt = promptHolder.get
private[sbt] final def setPrompt(newPrompt: Prompt): Unit =
if (prompt != Prompt.NoPrompt) promptHolder.set(newPrompt)
@ -174,10 +175,7 @@ object Terminal {
try Terminal.console.printStream.println(s"[info] $string")
catch { case _: IOException => }
}
private[sbt] def set(terminal: Terminal): Terminal = {
jline.TerminalFactory.set(terminal.toJLine)
activeTerminal.getAndSet(terminal)
}
private[sbt] def set(terminal: Terminal): Terminal = activeTerminal.getAndSet(terminal)
implicit class TerminalOps(private val term: Terminal) extends AnyVal {
def ansi(richString: => String, string: => String): String =
if (term.isAnsiSupported) richString else string
@ -317,6 +315,7 @@ object Terminal {
private[this] object ProxyTerminal extends Terminal {
private def t: Terminal = activeTerminal.get
override private[sbt] def progressState: ProgressState = t.progressState
override def getWidth: Int = t.getWidth
override def getHeight: Int = t.getHeight
override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line)
@ -350,6 +349,7 @@ object Terminal {
override def getLastLine: Option[String] = t.getLastLine
override def getLines: Seq[String] = t.getLines
override private[sbt] def name: String = t.name
override def toString: String = s"ProxyTerminal(current = $t)"
}
private[sbt] def get: Terminal = ProxyTerminal
@ -395,50 +395,31 @@ object Terminal {
private[sbt] class WriteableInputStream(in: InputStream, name: String)
extends InputStream
with AutoCloseable {
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))
}
final def write(bytes: Int*): Unit = readThread.synchronized {
bytes.foreach(b => buffer.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 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
* the runnable can return a byte from stdin. If the read caller is interrupted,
* they remove the result from the waiting set and any byte read will be
* enqueued in the buffer. It is done this way so that we only read from
* System.in when a caller actually asks for bytes. If we constantly poll
* from System.in, then when the user calls reboot from the console, the
* first character they type after reboot is swallowed by the previous
* sbt main program. If the user calls reboot from a remote client, we
* can't avoid losing the first byte inputted in the console. A more
* robust fix would be to override System.in at the launcher level instead
* of at the sbt level. At the moment, the use case of a user calling
* reboot from a network client and the adding input at the server console
* seems pathological enough that it isn't worth putting more effort into
* fixing.
*
* Starts a loop that fills a buffer with bytes from stdin. We only read from
* the underlying stream when the buffer is empty and there is an active reader.
* If the reader detaches without consuming any bytes, we just buffer the
* next byte that we read from the stream. One known issue with this approach
* is that if a remote client triggers a reboot, we cannot necessarily stop this
* loop from consuming the next byte from standard in even if sbt has fully
* rebooted and the byte will never be consumed. We try to fix this in withStreams
* by setting the terminal to raw mode, which the input stream makes it non blocking,
* but this approach only works on posix platforms.
*/
private[this] val runnable: Runnable = () => {
@tailrec def impl(): Unit = {
val _ = readQueue.take
val b = in.read
// The downstream consumer may have been interrupted. Buffer the result
// when that hapens.
waiting.poll match {
case null => buffer.put(b)
case q => q.put(b)
}
buffer.put(b)
if (b != -1 && !Thread.interrupted()) impl()
else closed.set(true)
}
@ -446,30 +427,28 @@ object Terminal {
catch { case _: InterruptedException => closed.set(true) }
}
executor.submit(runnable)
override def read(): Int =
if (closed.get) -1
else
synchronized {
def read(result: LinkedBlockingQueue[Integer]): Unit =
if (!closed.get)
readThread.synchronized {
readThread.set(Thread.currentThread)
try buffer.poll match {
case null =>
val result = new LinkedBlockingQueue[Integer]
waiting.synchronized(waiting.put(result))
readQueue.put(())
try result.take.toInt
catch {
case e: InterruptedException =>
waiting.remove(result)
-1
}
result.put(buffer.take)
case b if b == -1 => throw new ClosedChannelException
case b => b.toInt
case b => result.put(b)
} finally readThread.set(null)
}
def cancel(): Unit = waiting.synchronized {
override def read(): Int = {
val result = new LinkedBlockingQueue[Integer]
read(result)
result.poll match {
case null => -1
case i => i.toInt
}
}
def cancel(): Unit = readThread.synchronized {
Option(readThread.getAndSet(null)).foreach(_.interrupt())
waiting.forEach(_.put(-2))
waiting.clear()
readQueue.clear()
}
@ -489,7 +468,7 @@ object Terminal {
try {
System.setOut(proxyPrintStream)
System.setErr(proxyErrorStream)
scala.Console.withErr(proxyErrorStream)(scala.Console.withOut(proxyOutputStream)(f))
scala.Console.withErr(proxyErrorStream)(scala.Console.withOut(proxyPrintStream)(f))
} finally {
System.setOut(originalOut)
System.setErr(originalErr)
@ -520,7 +499,6 @@ object Terminal {
* System.out through the terminal's input and output streams.
*/
private[this] val activeTerminal = new AtomicReference[Terminal](consoleTerminalHolder.get)
jline.TerminalFactory.set(consoleTerminalHolder.get.toJLine)
/**
* The boot input stream allows a remote client to forward input to the sbt process while
@ -694,13 +672,13 @@ object Terminal {
if (alive)
try terminal.init()
catch {
case _: InterruptedException =>
case _: InterruptedException | _: java.io.IOError =>
}
override def restore(): Unit =
if (alive)
try terminal.restore()
catch {
case _: InterruptedException =>
case _: InterruptedException | _: java.io.IOError =>
}
override def reset(): Unit =
try terminal.reset()
@ -731,7 +709,11 @@ object Terminal {
}
term.restore()
term.setEchoEnabled(true)
new ConsoleTerminal(term, nonBlockingIn, originalOut)
new ConsoleTerminal(
term,
if (System.console == null) nullWriteableInputStream else nonBlockingIn,
originalOut
)
}
private[sbt] def reset(): Unit = {
@ -774,19 +756,25 @@ object Terminal {
private val capabilityMap =
org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap
private val consoleProgressState = new AtomicReference[ProgressState](new ProgressState(1))
private[sbt] def setConsoleProgressState(progressState: ProgressState): Unit =
consoleProgressState.set(progressState)
@deprecated("For compatibility only", "1.4.0")
private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine
private class ConsoleTerminal(
val term: jline.Terminal with jline.Terminal2,
in: InputStream,
in: WriteableInputStream,
out: OutputStream
) extends TerminalImpl(in, out, originalErr, "console0") {
private[util] lazy val system = JLine3.system
private[this] def isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")
override def getWidth: Int = system.getSize.getColumns
override def getHeight: Int = system.getSize.getRows
override def isAnsiSupported: Boolean = term.isAnsiSupported && !isCI
override private[sbt] def getSizeImpl: (Int, Int) = {
val size = system.getSize
(size.getColumns, size.getRows)
}
private[this] val isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")
override lazy val isAnsiSupported: Boolean = term.isAnsiSupported && !isCI
override private[sbt] def progressState: ProgressState = consoleProgressState.get
override def isEchoEnabled: Boolean = system.echo()
override def isSuccessEnabled: Boolean = true
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
@ -801,7 +789,7 @@ object Terminal {
override private[sbt] def restore(): Unit = term.restore()
override private[sbt] def getAttributes: Map[String, String] =
JLine3.toMap(system.getAttributes)
Try(JLine3.toMap(system.getAttributes)).getOrElse(Map.empty)
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 =
@ -836,17 +824,26 @@ object Terminal {
}
}
private[sbt] abstract class TerminalImpl private[sbt] (
val in: InputStream,
val in: WriteableInputStream,
val out: OutputStream,
override val errorStream: OutputStream,
override private[sbt] val name: String
) extends Terminal {
private[sbt] def getSizeImpl: (Int, Int)
private[this] val sizeRefreshPeriod = 1.second
private[this] val size =
new AtomicReference[((Int, Int), Deadline)](((1, 1), Deadline.now - 1.day))
private[this] def setSize() = size.set((Try(getSizeImpl).getOrElse((1, 1)), Deadline.now))
private[this] def getSize = size.get match {
case (s, d) if (d + sizeRefreshPeriod).isOverdue =>
setSize()
size.get._1
case (s, _) => s
}
override def getWidth: Int = getSize._1
override def getHeight: Int = getSize._2
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
@ -881,12 +878,13 @@ object Terminal {
}
override def flush(): Unit = combinedOutputStream.flush()
}
private def doWrite(bytes: Array[Byte]): Unit =
progressState.write(TerminalImpl.this, bytes, rawPrintStream, hasProgress.get && !rawMode.get)
private def doWrite(bytes: Array[Byte]): Unit = withPrintStream { ps =>
progressState.write(TerminalImpl.this, bytes, ps, hasProgress.get && !rawMode.get)
}
override private[sbt] val printStream: PrintStream = new LinePrintStream(outputStream)
override def inputStream: InputStream = writeableInputStream
override def inputStream: InputStream = in
private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*)
private[sbt] def write(bytes: Int*): Unit = in.write(bytes: _*)
private[this] val isStopped = new AtomicBoolean(false)
override def getLineHeightAndWidth(line: String): (Int, Int) = getWidth match {
@ -907,11 +905,19 @@ object Terminal {
writeLock.synchronized(f(rawPrintStream))
override def close(): Unit = if (isStopped.compareAndSet(false, true)) {
writeableInputStream.close()
in.close()
}
}
private lazy val nullInputStream: InputStream = () => {
try this.synchronized(this.wait)
catch { case _: InterruptedException => }
-1
}
private lazy val nullWriteableInputStream =
new WriteableInputStream(nullInputStream, "null-writeable-input-stream")
private[sbt] val NullTerminal = new Terminal {
override def close(): Unit = {}
override private[sbt] def progressState: ProgressState = new ProgressState(1)
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false
override def getHeight: Int = 0
override def getLastLine: Option[String] = None
@ -920,11 +926,7 @@ object Terminal {
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)
catch { case _: InterruptedException => }
-1
}
override def inputStream: java.io.InputStream = nullInputStream
override def isAnsiSupported: Boolean = false
override def isColorEnabled: Boolean = false
override def isEchoEnabled: Boolean = false

View File

@ -10,7 +10,7 @@ package sbt
import java.io.File
import java.nio.channels.ClosedChannelException
import sbt.internal.inc.{ AnalyzingCompiler, PlainVirtualFile }
import sbt.internal.util.Terminal
import sbt.internal.util.{ DeprecatedJLine, Terminal }
import sbt.util.Logger
import xsbti.compile.{ Compilers, Inputs }
@ -67,6 +67,8 @@ final class Console(compiler: AnalyzingCompiler) {
try {
sys.props("scala.color") = if (terminal.isColorEnabled) "true" else "false"
terminal.withRawOutput {
jline.TerminalFactory.set(terminal.toJLine)
DeprecatedJLine.setTerminalOverride(sbt.internal.util.JLine3(terminal))
terminal.withRawInput(Run.executeTrapExit(console0, log))
}
} finally {

View File

@ -389,6 +389,10 @@ object State {
s get BasicKeys.classLoaderCache getOrElse (throw new IllegalStateException(
"Tried to get classloader cache for uninitialized state."
))
private[sbt] def extendedClassLoaderCache: ClassLoaderCache =
s get BasicKeys.extendedClassLoaderCache getOrElse (throw new IllegalStateException(
"Tried to get extended classloader cache for uninitialized state."
))
def initializeClassLoaderCache: State = {
s.get(BasicKeys.extendedClassLoaderCache).foreach(_.close())
val cache = newClassLoaderCache

View File

@ -11,7 +11,7 @@ import java.io.File
import java.lang.management.ManagementFactory
import java.lang.ref.{ Reference, ReferenceQueue, SoftReference }
import java.net.URLClassLoader
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.{ AtomicInteger, AtomicReference }
import sbt.internal.inc.classpath.{
AbstractClassLoaderCache,
@ -30,9 +30,12 @@ private object ClassLoaderCache {
private def threadID = new AtomicInteger(0)
}
private[sbt] class ClassLoaderCache(
override val commonParent: ClassLoader,
val parent: ClassLoader,
private val miniProvider: Option[(File, ClassLoader)]
) extends AbstractClassLoaderCache {
private[this] val parentHolder = new AtomicReference(parent)
def commonParent = parentHolder.get()
def setParent(parent: ClassLoader): Unit = parentHolder.set(parent)
def this(commonParent: ClassLoader) = this(commonParent, None)
def this(scalaProvider: ScalaProvider) =
this(scalaProvider.launcher.topLoader, {
@ -51,8 +54,9 @@ private[sbt] class ClassLoaderCache(
}
}
private class Key(val fileStamps: Seq[(File, Long)], val parent: ClassLoader) {
def this(files: List[File]) =
this(files.map(f => f -> IO.getModifiedTimeOrZero(f)), commonParent)
def this(files: List[File], parent: ClassLoader) =
this(files.map(f => f -> IO.getModifiedTimeOrZero(f)), parent)
def this(files: List[File]) = this(files, commonParent)
lazy val files: Seq[File] = fileStamps.map(_._1)
lazy val maxStamp: Long = fileStamps.maxBy(_._2)._2
class CachedClassLoader
@ -169,10 +173,19 @@ private[sbt] class ClassLoaderCache(
val key = new Key(files, parent)
get(key, mkLoader)
}
override def apply(files: List[File]): ClassLoader = {
val key = new Key(files)
def apply(files: List[File], parent: ClassLoader): ClassLoader = {
val key = new Key(files, parent)
get(key, () => key.toClassLoader)
}
override def apply(files: List[File]): ClassLoader = {
files match {
case d :: s :: Nil if d.getName.startsWith("dotty-library") =>
apply(files, classOf[org.jline.terminal.Terminal].getClassLoader)
case _ =>
val key = new Key(files)
get(key, () => key.toClassLoader)
}
}
override def cachedCustomClassloader(
files: List[File],
mkLoader: () => ClassLoader

View File

@ -47,11 +47,12 @@ import Serialization.{
systemErrFlush,
terminalCapabilities,
terminalCapabilitiesResponse,
terminalGetSize,
terminalPropertiesQuery,
terminalPropertiesResponse,
terminalSetSize,
getTerminalAttributes,
setTerminalAttributes,
setTerminalSize,
}
import NetworkClient.Arguments
@ -657,7 +658,13 @@ class NetworkClient(
cchars = attrs.getOrElse("cchars", ""),
)
sendCommandResponse("", response, msg.id)
case (`setTerminalSize`, Some(json)) =>
case (`terminalGetSize`, _) =>
val response = TerminalGetSizeResponse(
Terminal.console.getWidth,
Terminal.console.getHeight,
)
sendCommandResponse("", response, msg.id)
case (`terminalSetSize`, Some(json)) =>
Converter.fromJson[TerminalSetSizeCommand](json) match {
case Success(size) =>
Terminal.console.setSize(size.width, size.height)

View File

@ -11,7 +11,6 @@ import java.io.File
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicBoolean
//import jline.console.history.PersistentHistory
import sbt.BasicCommandStrings.{ Cancel, TerminateAction, Shutdown }
import sbt.BasicKeys.{ historyPath, terminalShellPrompt }
import sbt.State
@ -23,55 +22,71 @@ import sbt.internal.util.complete.{ Parser }
import scala.annotation.tailrec
private[sbt] trait UITask extends Runnable with AutoCloseable {
private[sbt] def channel: CommandChannel
private[sbt] def reader: UITask.Reader
private[sbt] val channel: CommandChannel
private[sbt] val reader: UITask.Reader
private[this] final def handleInput(s: Either[String, String]): Boolean = s match {
case Left(m) => channel.onFastTrackTask(m)
case Right(cmd) => channel.onCommand(cmd)
}
private[this] val isStopped = new AtomicBoolean(false)
override def run(): Unit = {
@tailrec def impl(): Unit = {
@tailrec def impl(): Unit = if (!isStopped.get) {
val res = reader.readLine()
if (!handleInput(res) && !isStopped.get) impl()
}
try impl()
catch { case _: InterruptedException | _: ClosedChannelException => isStopped.set(true) }
}
override def close(): Unit = isStopped.set(true)
override def close(): Unit = {
isStopped.set(true)
reader.close()
}
}
private[sbt] object UITask {
trait Reader { def readLine(): Either[String, String] }
trait Reader extends AutoCloseable {
def readLine(): Either[String, String]
override def close(): Unit = {}
}
object Reader {
// Avoid filling the stack trace since it isn't helpful here
object interrupted extends InterruptedException
def terminalReader(parser: Parser[_])(
terminal: Terminal,
state: State
): Reader = { () =>
try {
val clear = terminal.ansi(ClearPromptLine, "")
@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)
}
): Reader = new Reader {
val closed = new AtomicBoolean(false)
def readLine(): Either[String, String] =
try {
val clear = terminal.ansi(ClearPromptLine, "")
@tailrec def impl(): Either[String, String] = {
val thread = Thread.currentThread
if (thread.isInterrupted || closed.get) throw interrupted
val reader = LineReader.createReader(history(state), parser, terminal)
if (thread.isInterrupted || closed.get) throw interrupted
(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 e: InterruptedException => Right("") }
val res = impl()
terminal.setPrompt(Prompt.Pending)
res
} catch { case e: InterruptedException => Right("") }
override def close(): Unit = closed.set(true)
}
}
private[this] def history(s: State): Option[File] =
@ -87,7 +102,7 @@ private[sbt] object UITask {
state: State,
override val channel: CommandChannel,
) extends UITask {
override private[sbt] def reader: UITask.Reader = {
override private[sbt] lazy val reader: UITask.Reader = {
UITask.Reader.terminalReader(state.combinedParser)(channel.terminal, state)
}
}

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
private[sbt] class UserThread(val channel: CommandChannel) extends AutoCloseable {
private[this] val uiThread = new AtomicReference[(UITask, Thread)]
@ -31,15 +31,21 @@ private[sbt] class UserThread(val channel: CommandChannel) extends AutoCloseable
uiThread.synchronized {
val task = channel.makeUIThread(state)
def submit(): Thread = {
val thread = new Thread(() => {
task.run()
uiThread.set(null)
}, s"sbt-$name-ui-thread")
thread.setDaemon(true)
thread.start()
val thread: Thread = new Thread(s"sbt-$name-ui-thread") {
setDaemon(true)
override def run(): Unit =
try task.run()
finally uiThread.get match {
case (_, t) if t == this => uiThread.set(null)
case _ =>
}
}
uiThread.getAndSet((task, thread)) match {
case null =>
case (_, t) => t.interrupt()
case null => thread.start()
case (task, t) if t.getClass != task.getClass =>
stopThreadImpl()
thread.start()
case t => uiThread.set(t)
}
thread
}
@ -47,39 +53,52 @@ private[sbt] class UserThread(val channel: CommandChannel) extends AutoCloseable
case null => uiThread.set((task, submit()))
case (t, _) if t.getClass == task.getClass =>
case (t, thread) =>
thread.interrupt()
stopThreadImpl()
uiThread.set((task, submit()))
}
}
Option(lastProgressEvent.get).foreach(onProgressEvent)
}
private[sbt] def stopThread(): Unit = uiThread.synchronized {
private[sbt] def stopThreadImpl(): Unit = uiThread.synchronized {
uiThread.getAndSet(null) match {
case null =>
case (t, thread) =>
t.close()
Util.ignoreResult(thread.interrupt())
}
}
try thread.join(1000)
catch { case _: InterruptedException => }
private[sbt] def onConsolePromptEvent(consolePromptEvent: ConsolePromptEvent): Unit = {
channel.terminal.withPrintStream { ps =>
ps.print(ConsoleAppender.ClearScreenAfterCursor)
ps.flush()
// This join should always work, but if it doesn't log an error because
// it can cause problems if the thread isn't joined
if (thread.isAlive) System.err.println(s"Unable to join thread $thread")
()
}
val state = consolePromptEvent.state
terminal.prompt match {
case Running => terminal.setPrompt(AskUser(() => UITask.shellPrompt(terminal, state)))
case _ =>
}
onProgressEvent(ProgressEvent("Info", Vector(), None, None, None))
reset(state)
}
private[sbt] def stopThread(): Unit = uiThread.synchronized(stopThreadImpl())
private[sbt] def onConsolePromptEvent(consolePromptEvent: ConsolePromptEvent): Unit =
// synchronize to ensure that the state isn't modified during the call to reset
// at the bottom
synchronized {
channel.terminal.withPrintStream { ps =>
ps.print(ConsoleAppender.ClearScreenAfterCursor)
ps.flush()
}
val state = consolePromptEvent.state
terminal.prompt match {
case Prompt.Running | Prompt.Pending =>
terminal.setPrompt(Prompt.AskUser(() => UITask.shellPrompt(terminal, state)))
case _ =>
}
onProgressEvent(ProgressEvent("Info", Vector(), None, None, None))
reset(state)
}
private[sbt] def onConsoleUnpromptEvent(
consoleUnpromptEvent: ConsoleUnpromptEvent
): Unit = {
terminal.setPrompt(Prompt.Pending)
if (consoleUnpromptEvent.lastSource.fold(true)(_.channelName != name)) {
terminal.progressState.reset()
} else stopThread()

View File

@ -0,0 +1,34 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal;
import java.net.URL;
import java.net.URLClassLoader;
class JLineLoader extends URLClassLoader {
JLineLoader(final URL[] urls, final ClassLoader parent) {
super(urls, parent);
}
@Override
public String toString() {
final StringBuilder result = new StringBuilder();
result.append("JLineLoader(");
final URL[] urls = getURLs();
for (int i = 0; i < urls.length; ++i) {
result.append(urls[i].toString());
if (i < urls.length - 1) result.append(", ");
}
result.append(")");
return result.toString();
}
static {
registerAsParallelCapable();
}
}

View File

@ -22,16 +22,19 @@ public final class MetaBuildLoader extends URLClassLoader {
private final URLClassLoader fullScalaLoader;
private final URLClassLoader libraryLoader;
private final URLClassLoader interfaceLoader;
private final URLClassLoader jlineLoader;
MetaBuildLoader(
final URL[] urls,
final URLClassLoader fullScalaLoader,
final URLClassLoader libraryLoader,
final URLClassLoader interfaceLoader) {
final URLClassLoader interfaceLoader,
final URLClassLoader jlineLoader) {
super(urls, fullScalaLoader);
this.fullScalaLoader = fullScalaLoader;
this.libraryLoader = libraryLoader;
this.interfaceLoader = interfaceLoader;
this.jlineLoader = jlineLoader;
}
@Override
@ -45,6 +48,7 @@ public final class MetaBuildLoader extends URLClassLoader {
fullScalaLoader.close();
libraryLoader.close();
interfaceLoader.close();
jlineLoader.close();
}
static {
@ -61,20 +65,26 @@ 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-(terminal-)?[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar");
final File[] cp = appProvider.mainClasspath();
final URL[] interfaceURLs = new URL[3];
final URL[] interfaceURLs = new URL[1];
final URL[] jlineURLs = new URL[3];
final File[] extra =
appProvider.id().classpathExtra() == null ? new File[0] : appProvider.id().classpathExtra();
final Set<File> bottomClasspath = new LinkedHashSet<>();
{
int interfaceIndex = 0;
int jlineIndex = 0;
for (final File file : cp) {
final String name = file.getName();
if (pattern.matcher(name).find()) {
if (name.contains("test-interface") && pattern.matcher(name).find()) {
interfaceURLs[interfaceIndex] = file.toURI().toURL();
interfaceIndex += 1;
} else if (pattern.matcher(name).find()) {
jlineURLs[jlineIndex] = file.toURI().toURL();
jlineIndex += 1;
} else {
bottomClasspath.add(file);
}
@ -108,6 +118,7 @@ public final class MetaBuildLoader extends URLClassLoader {
if (topLoader == null) topLoader = scalaProvider.launcher().topLoader();
final TestInterfaceLoader interfaceLoader = new TestInterfaceLoader(interfaceURLs, topLoader);
final JLineLoader jlineLoader = new JLineLoader(jlineURLs, interfaceLoader);
final File[] siJars = scalaProvider.jars();
final URL[] lib = new URL[1];
int scalaRestCount = siJars.length - 1;
@ -131,8 +142,8 @@ public final class MetaBuildLoader extends URLClassLoader {
}
}
assert lib[0] != null : "no scala-library.jar";
final ScalaLibraryClassLoader libraryLoader = new ScalaLibraryClassLoader(lib, interfaceLoader);
final ScalaLibraryClassLoader libraryLoader = new ScalaLibraryClassLoader(lib, jlineLoader);
final FullScalaLoader fullScalaLoader = new FullScalaLoader(scalaRest, libraryLoader);
return new MetaBuildLoader(rest, fullScalaLoader, libraryLoader, interfaceLoader);
return new MetaBuildLoader(rest, fullScalaLoader, libraryLoader, interfaceLoader, jlineLoader);
}
}

View File

@ -8,7 +8,7 @@
package sbt
import java.io.{ File, PrintWriter }
import java.net.{ URI, URL, URLClassLoader }
import java.net.{ URI, URL }
import java.nio.file.{ Paths, Path => NioPath }
import java.util.Optional
import java.util.concurrent.TimeUnit
@ -34,9 +34,8 @@ import sbt.Scope.{ GlobalScope, ThisScope, fillTaskAxis }
import sbt.coursierint._
import sbt.internal.CommandStrings.ExportStream
import sbt.internal._
import sbt.internal.classpath.AlternativeZincUtil
import sbt.internal.classpath.{ AlternativeZincUtil, ClassLoaderCache }
import sbt.internal.inc.JavaInterfaceUtil._
import sbt.internal.inc.classpath.{ ClassLoaderCache, ClasspathFilter, ClasspathUtil }
import sbt.internal.inc.{
CompileOutput,
MappedFileConverter,
@ -45,6 +44,8 @@ import sbt.internal.inc.{
ZincLmUtil,
ZincUtil
}
import sbt.internal.inc.classpath.{ ClasspathFilter, ClasspathUtil }
import sbt.internal.inc.{ MappedFileConverter, PlainVirtualFile, Stamps, ZincLmUtil, ZincUtil }
import sbt.internal.io.{ Source, WatchState }
import sbt.internal.librarymanagement.mavenint.{
PomExtraDependencyAttributes,
@ -96,7 +97,7 @@ import sjsonnew._
import sjsonnew.support.scalajson.unsafe.Converter
import scala.collection.immutable.ListMap
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.duration._
import scala.util.control.NonFatal
import scala.xml.NodeSeq
@ -386,12 +387,21 @@ object Defaults extends BuildCommon {
},
turbo :== SysProp.turbo,
usePipelining :== SysProp.pipelining,
useScalaReplJLine :== false,
scalaInstanceTopLoader := {
if (!useScalaReplJLine.value) classOf[org.jline.terminal.Terminal].getClassLoader
else appConfiguration.value.provider.scalaProvider.launcher.topLoader.getParent
},
useSuperShell := { if (insideCI.value) false else Terminal.console.isSupershellEnabled },
superShellThreshold :== SysProp.supershellThreshold,
superShellMaxTasks :== SysProp.supershellMaxTasks,
superShellSleep :== SysProp.supershellSleep.millis,
progressReports := {
val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector
rs map { Keys.TaskProgress(_) }
},
progressState := Some(new ProgressState(SysProp.supershellBlankZone)),
// progressState is deprecated
SettingKey[Option[ProgressState]]("progressState") := None,
Previous.cache := new Previous(
Def.streamsManagerKey.value,
Previous.references.value.getReferences
@ -888,8 +898,15 @@ object Defaults extends BuildCommon {
val libraryJars = allJars.filter(_.getName == "scala-library.jar")
allJars.filter(_.getName == "scala-compiler.jar") match {
case Array(compilerJar) if libraryJars.nonEmpty =>
val cache = state.value.classLoaderCache
mkScalaInstance(version, allJars, libraryJars, compilerJar, cache)
val cache = state.value.extendedClassLoaderCache
mkScalaInstance(
version,
allJars,
libraryJars,
compilerJar,
cache,
scalaInstanceTopLoader.value
)
case _ => ScalaInstance(version, scalaProvider)
}
} else
@ -931,7 +948,8 @@ object Defaults extends BuildCommon {
allJars,
Array(libraryJar),
compilerJar,
state.value.classLoaderCache
state.value.extendedClassLoaderCache,
scalaInstanceTopLoader.value,
)
}
private[this] def mkScalaInstance(
@ -940,15 +958,11 @@ object Defaults extends BuildCommon {
libraryJars: Array[File],
compilerJar: File,
classLoaderCache: ClassLoaderCache,
topLoader: ClassLoader,
): ScalaInstance = {
val allJarsDistinct = allJars.distinct
val libraryLoader = classLoaderCache(libraryJars.toList)
class ScalaLoader
extends URLClassLoader(allJarsDistinct.map(_.toURI.toURL).toArray, libraryLoader)
val fullLoader = classLoaderCache.cachedCustomClassloader(
allJarsDistinct.toList,
() => new ScalaLoader
)
val libraryLoader = classLoaderCache(libraryJars.toList, topLoader)
val fullLoader = classLoaderCache(allJarsDistinct.toList, libraryLoader)
new ScalaInstance(
version,
fullLoader,
@ -970,7 +984,8 @@ object Defaults extends BuildCommon {
dummy.allJars,
dummy.libraryJars,
dummy.compilerJar,
state.value.classLoaderCache
state.value.extendedClassLoaderCache,
scalaInstanceTopLoader.value,
)
}

View File

@ -255,7 +255,7 @@ object EvaluateTask {
extracted,
structure
)
val reporters = maker.map(_.progress) ++ Some(TaskProgress) ++
val reporters = maker.map(_.progress) ++ state.get(Keys.taskProgress) ++
(if (SysProp.taskTimings)
new TaskTimings(reportOnShutdown = false, state.globalLogging.full) :: Nil
else Nil)

View File

@ -555,10 +555,15 @@ object Keys {
def apply(progress: ExecuteProgress[Task]): TaskProgress = new TaskProgress(progress)
}
private[sbt] val currentTaskProgress = AttributeKey[TaskProgress]("current-task-progress")
private[sbt] val taskProgress = AttributeKey[sbt.internal.TaskProgress]("active-task-progress")
val useSuperShell = settingKey[Boolean]("Enables (true) or disables the super shell.")
val superShellMaxTasks = settingKey[Int]("The max number of tasks to display in the supershell progress report")
val superShellSleep = settingKey[FiniteDuration]("The minimum duration to sleep between progress reports")
val superShellThreshold = settingKey[FiniteDuration]("The minimum amount of time a task must be running to appear in the supershell progress report")
val turbo = settingKey[Boolean]("Enables (true) or disables optional performance features.")
// This key can be used to add custom ExecuteProgress instances
val progressReports = settingKey[Seq[TaskProgress]]("A function that returns a list of progress reporters.").withRank(DTask)
@deprecated("unused", "1.4.0")
private[sbt] val progressState = settingKey[Option[ProgressState]]("The optional progress state if supershell is enabled.").withRank(Invisible)
private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask)
@deprecated("No longer used", "1.3.0")
@ -570,6 +575,9 @@ object Keys {
val includeLintKeys = settingKey[Set[Def.KeyedInitialize[_]]]("Task keys that are included into lintUnused task")
val lintUnusedKeysOnLoad = settingKey[Boolean]("Toggles whether or not to check for unused keys during startup")
val useScalaReplJLine = settingKey[Boolean]("Toggles whether or not to use sbt's forked jline in the scala repl. Enabling this flag may break the thin client in the scala console.").withRank(KeyRanks.Invisible)
val scalaInstanceTopLoader = settingKey[ClassLoader]("The top classloader for the scala instance").withRank(KeyRanks.Invisible)
val stateStreams = AttributeKey[Streams]("stateStreams", "Streams manager, which provides streams for different contexts. Setting this on State will override the default Streams implementation.")
val resolvedScoped = Def.resolvedScoped
val pluginData = taskKey[PluginData]("Information from the plugin build needed in the main build definition.").withRank(DTask)

View File

@ -15,7 +15,7 @@ import java.util.Properties
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.atomic.AtomicBoolean
import sbt.BasicCommandStrings.{ Shell, Shutdown, TemplateCommand, networkExecPrefix }
import sbt.BasicCommandStrings.{ Shell, Shutdown, TemplateCommand }
import sbt.Project.LoadAction
import sbt.compiler.EvalImports
import sbt.internal.Aggregation.AnyKeys
@ -932,13 +932,28 @@ object BuiltinCommands {
val s3 = addCacheStoreFactoryFactory(Project.setProject(session, structure, s2))
val s4 = s3.put(Keys.useLog4J.key, Project.extract(s3).get(Keys.useLog4J))
val s5 = setupGlobalFileTreeRepository(s4)
CheckBuildSources.init(LintUnused.lintUnusedFunc(s5))
// This is a workaround for the console task in dotty which uses the classloader cache.
// We need to override the top loader in that case so that it gets the forked jline.
s5.extendedClassLoaderCache.setParent(Project.extract(s5).get(Keys.scalaInstanceTopLoader))
addSuperShellParams(CheckBuildSources.init(LintUnused.lintUnusedFunc(s5)))
}
private val setupGlobalFileTreeRepository: State => State = { state =>
state.get(sbt.nio.Keys.globalFileTreeRepository).foreach(_.close())
state.put(sbt.nio.Keys.globalFileTreeRepository, FileTreeRepository.default)
}
private val addSuperShellParams: State => State = (s: State) => {
val extracted = Project.extract(s)
import scala.concurrent.duration._
val sleep = extracted.getOpt(Keys.superShellSleep).getOrElse(SysProp.supershellSleep.millis)
val threshold =
extracted.getOpt(Keys.superShellThreshold).getOrElse(SysProp.supershellThreshold)
val maxItems = extracted.getOpt(Keys.superShellMaxTasks).getOrElse(SysProp.supershellMaxTasks)
Terminal.setConsoleProgressState(new ProgressState(1, maxItems))
s.put(Keys.superShellSleep.key, sleep)
.put(Keys.superShellThreshold.key, threshold)
.put(Keys.superShellMaxTasks.key, maxItems)
}
private val addCacheStoreFactoryFactory: State => State = (s: State) => {
val size = Project
.extract(s)
@ -996,13 +1011,7 @@ object BuiltinCommands {
}
private def getExec(state: State, interval: Duration): Exec = {
val exec: Exec =
StandardMain.exchange.blockUntilNextExec(interval, Some(state), state.globalLogging.full)
if (exec.source.fold(true)(_.channelName != ConsoleChannel.defaultName) &&
!exec.commandLine.startsWith(networkExecPrefix)) {
Terminal.consoleLog(s"received remote command: ${exec.commandLine}")
}
exec
StandardMain.exchange.blockUntilNextExec(interval, Some(state), state.globalLogging.full)
}
def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 =>

View File

@ -15,15 +15,17 @@ import sbt.internal.ShutdownHooks
import sbt.internal.langserver.ErrorCodes
import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.nio.CheckBuildSources.CheckBuildSourcesKey
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Terminal }
import sbt.internal.{ ConsoleUnpromptEvent, ShutdownHooks }
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Prompt, Terminal }
import sbt.internal.{ ShutdownHooks, TaskProgress }
import sbt.io.{ IO, Using }
import sbt.protocol._
import sbt.util.{ Logger, LoggerContext }
import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.control.NonFatal
import sbt.internal.FastTrackCommands
import sbt.internal.SysProp
object MainLoop {
@ -150,9 +152,17 @@ object MainLoop {
def next(state: State): State = {
val context = LoggerContext(useLog4J = state.get(Keys.useLog4J.key).getOrElse(false))
val superShellSleep =
state.get(Keys.superShellSleep.key).getOrElse(SysProp.supershellSleep.millis)
val superShellThreshold =
state.get(Keys.superShellThreshold.key).getOrElse(SysProp.supershellThreshold)
val taskProgress = new TaskProgress(superShellSleep, superShellThreshold)
try {
ErrorHandling.wideConvert {
state.put(Keys.loggerContext, context).process(processCommand)
state
.put(Keys.loggerContext, context)
.put(Keys.taskProgress, taskProgress)
.process(processCommand)
} match {
case Right(s) => s.remove(Keys.loggerContext)
case Left(t: xsbti.FullReload) => throw t
@ -186,7 +196,10 @@ object MainLoop {
state.log.error(msg)
state.log.error("\n")
state.handleError(oom)
} finally context.close()
} finally {
context.close()
taskProgress.close()
}
}
/** This is the main function State transfer function of the sbt command processing. */
@ -206,14 +219,18 @@ 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))
val restoreTerminal = channelName.flatMap(exchange.channelForName) match {
case Some(c) =>
val prevTerminal = Terminal.set(c.terminal)
val prevPrompt = c.terminal.prompt
// temporarily set the prompt to running during task evaluation
c.terminal.setPrompt(Prompt.Running)
() => {
c.terminal.setPrompt(prevPrompt)
Terminal.set(prevTerminal)
c.terminal.setPrompt(prevPrompt)
c.terminal.flush()
}
case _ => () => ()

View File

@ -9,8 +9,11 @@ package sbt
package internal
import java.util.concurrent.ConcurrentHashMap
import scala.collection.concurrent.TrieMap
import java.util.concurrent.atomic.AtomicLong
import scala.collection.JavaConverters._
import scala.collection.mutable
import scala.collection.immutable.VectorBuilder
import scala.concurrent.duration._
private[sbt] abstract class AbstractTaskExecuteProgress extends ExecuteProgress[Task] {
import AbstractTaskExecuteProgress.Timer
@ -18,10 +21,51 @@ private[sbt] abstract class AbstractTaskExecuteProgress extends ExecuteProgress[
private[this] val showScopedKey = Def.showShortKey(None)
private[this] val anonOwners = new ConcurrentHashMap[Task[_], Task[_]]
private[this] val calledBy = new ConcurrentHashMap[Task[_], Task[_]]
private[this] val activeTasksMap = new ConcurrentHashMap[Task[_], Unit]
protected val timings = new ConcurrentHashMap[Task[_], Timer]
private[this] val timings = new ConcurrentHashMap[Task[_], Timer]
private[sbt] def timingsByName: mutable.Map[String, AtomicLong] = {
val result = new ConcurrentHashMap[String, AtomicLong]
timings.forEach { (task, timing) =>
val duration = timing.durationNanos
result.putIfAbsent(taskName(task), new AtomicLong(duration)) match {
case null =>
case t => t.getAndAdd(duration); ()
}
}
result.asScala
}
private[sbt] def anyTimings = !timings.isEmpty
def currentTimings: Iterator[(Task[_], Timer)] = timings.asScala.iterator
def activeTasks: Set[Task[_]] = activeTasksMap.keySet.asScala.toSet
private[internal] def exceededThreshold(task: Task[_], threshold: FiniteDuration): Boolean =
timings.get(task) match {
case null => false
case t => t.durationMicros > threshold.toMicros
}
private[internal] def timings(
tasks: java.util.Set[Task[_]],
thresholdMicros: Long
): Vector[(Task[_], Long)] = {
val result = new VectorBuilder[(Task[_], Long)]
val now = System.nanoTime
tasks.forEach { t =>
timings.get(t) match {
case null =>
case timing =>
if (timing.isActive) {
val elapsed = (now - timing.startNanos) / 1000
if (elapsed > thresholdMicros) result += t -> elapsed
}
}
}
result.result()
}
def activeTasks(now: Long) = {
val result = new VectorBuilder[(Task[_], FiniteDuration)]
timings.forEach { (task, timing) =>
if (timing.isActive) result += task -> (now - timing.startNanos).nanos
}
result.result
}
override def afterRegistered(
task: Task[_],
@ -38,15 +82,17 @@ private[sbt] abstract class AbstractTaskExecuteProgress extends ExecuteProgress[
override def beforeWork(task: Task[_]): Unit = {
timings.put(task, new Timer)
activeTasksMap.put(task, ())
()
}
protected def clearTimings: Boolean = false
override def afterWork[A](task: Task[A], result: Either[Task[A], Result[A]]): Unit = {
timings.get(task) match {
case null =>
case t => t.stop()
}
activeTasksMap.remove(task)
if (clearTimings) timings.remove(task)
else
timings.get(task) match {
case null =>
case t => t.stop()
}
// we need this to infer anonymous task names
result.left.foreach { t =>
@ -54,14 +100,14 @@ private[sbt] abstract class AbstractTaskExecuteProgress extends ExecuteProgress[
}
}
protected def reset(): Unit = {
activeTasksMap.clear()
timings.clear()
private[this] val taskNameCache = new ConcurrentHashMap[Task[_], String]
protected def taskName(t: Task[_]): String = taskNameCache.get(t) match {
case null =>
val name = taskName0(t)
taskNameCache.putIfAbsent(t, name)
name
case name => name
}
private[this] val taskNameCache = TrieMap.empty[Task[_], String]
protected def taskName(t: Task[_]): String =
taskNameCache.getOrElseUpdate(t, taskName0(t))
private[this] def taskName0(t: Task[_]): String = {
def definedName(node: Task[_]): Option[String] =
node.info.name orElse TaskName.transformNode(node).map(showScopedKey.show)
@ -80,6 +126,7 @@ object AbstractTaskExecuteProgress {
def stop(): Unit = {
endNanos = System.nanoTime()
}
def isActive = endNanos == 0L
def durationNanos: Long = endNanos - startNanos
def startMicros: Long = (startNanos.toDouble / 1000).toLong
def durationMicros: Long = (durationNanos.toDouble / 1000).toLong

View File

@ -133,17 +133,10 @@ private[sbt] final class CommandExchange {
}
}
// Do not manually run GC until the user has been idling for at least the min gc interval.
val exec = impl(interval match {
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 =
@ -208,7 +201,8 @@ private[sbt] final class CommandExchange {
instance,
handlers,
s.log,
mkAskUser(name)
mkAskUser(name),
Option(lastState.get),
)
subscribe(channel)
}

View File

@ -1231,7 +1231,7 @@ private[sbt] object ContinuousCommands {
state: State
) extends Thread(s"sbt-${channel.name}-watch-ui-thread")
with UITask {
override private[sbt] def reader: UITask.Reader = () => {
override private[sbt] lazy val reader: UITask.Reader = () => {
def stop = Right(s"${ContinuousCommands.stopWatch} ${channel.name}")
val exitAction: Watch.Action = {
Watch.apply(

View File

@ -11,6 +11,7 @@ package internal
import java.util.Locale
import scala.util.control.NonFatal
import scala.concurrent.duration._
import sbt.internal.util.ConsoleAppender
import sbt.internal.util.complete.SizeParser
@ -103,7 +104,9 @@ object SysProp {
def dumbTerm: Boolean = sys.env.get("TERM").contains("dumb")
def supershell: Boolean = booleanOpt("sbt.supershell").getOrElse(!dumbTerm && color)
def supershellSleep: Long = long("sbt.supershell.sleep", 100L)
def supershellMaxTasks: Int = int("sbt.supershell.maxitems", 8)
def supershellSleep: Long = long("sbt.supershell.sleep", 500.millis.toMillis)
def supershellThreshold: FiniteDuration = long("sbt.supershell.threshold", 100L).millis
def supershellBlankZone: Int = int("sbt.supershell.blankzone", 1)
def defaultUseCoursier: Boolean = {

View File

@ -9,99 +9,100 @@ package sbt
package internal
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, AtomicReference }
import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.TimeUnit
import sbt.internal.util._
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.concurrent.duration._
object TaskProgress extends TaskProgress
import java.util.concurrent.{ ConcurrentHashMap, Executors, TimeoutException }
/**
* implements task progress display on the shell.
*/
private[sbt] class TaskProgress private ()
private[sbt] class TaskProgress(sleepDuration: FiniteDuration, threshold: FiniteDuration)
extends AbstractTaskExecuteProgress
with ExecuteProgress[Task] {
with ExecuteProgress[Task]
with AutoCloseable {
private[this] val lastTaskCount = new AtomicInteger(0)
private[this] val currentProgressThread = new AtomicReference[Option[ProgressThread]](None)
private[this] val sleepDuration = SysProp.supershellSleep.millis
private[this] val threshold = 10.millis
private[this] val tasks = new LinkedBlockingQueue[Task[_]]
private[this] final class ProgressThread
extends Thread("task-progress-report-thread")
with AutoCloseable {
private[this] val isClosed = new AtomicBoolean(false)
private[this] val firstTime = new AtomicBoolean(true)
private[this] val hasReported = new AtomicBoolean(false)
private[this] def doReport(): Unit = { hasReported.set(true); report() }
setDaemon(true)
start()
private def resetThread(): Unit =
currentProgressThread.synchronized {
currentProgressThread.getAndSet(None) match {
case Some(t) if t != this => currentProgressThread.set(Some(t))
case _ =>
}
}
@tailrec override def run(): Unit = {
if (!isClosed.get() && (!hasReported.get || active.nonEmpty)) {
try {
if (activeExceedingThreshold.nonEmpty) doReport()
val duration =
if (firstTime.compareAndSet(true, activeExceedingThreshold.isEmpty)) threshold
else sleepDuration
val limit = duration.fromNow
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 {
case _: InterruptedException =>
isClosed.set(true)
// One last report after close in case the last one hadn't gone through yet.
doReport()
}
run()
} else {
resetThread()
private[this] val reportLoop = new AtomicReference[AutoCloseable]
private[this] val active = new ConcurrentHashMap[Task[_], AutoCloseable]
private[this] val nextReport = new AtomicReference(Deadline.now)
private[this] val scheduler =
Executors.newSingleThreadScheduledExecutor(r => new Thread(r, "sbt-progress-report-scheduler"))
private[this] val pending = new java.util.Vector[java.util.concurrent.Future[_]]
private def schedule[R](duration: FiniteDuration, recurring: Boolean)(f: => R): AutoCloseable = {
val cancelled = new AtomicBoolean(false)
val runnable: Runnable = () => {
if (!cancelled.get) {
try Util.ignoreResult(f)
catch { case _: InterruptedException => }
}
}
def addTask(task: Task[_]): Unit = tasks.put(task)
override def close(): Unit = {
isClosed.set(true)
interrupt()
report()
appendProgress(ProgressEvent("Info", Vector(), None, None, None))
resetThread()
val delay = duration.toMillis
val future =
if (recurring) scheduler.schedule(runnable, delay, TimeUnit.MILLISECONDS)
else scheduler.scheduleAtFixedRate(runnable, delay, delay, TimeUnit.MILLISECONDS)
pending.add(future)
() => Util.ignoreResult(future.cancel(true))
}
private[this] val executor =
Executors.newSingleThreadExecutor(r => new Thread(r, "sbt-task-progress-report-thread"))
override def close(): Unit = {
Option(reportLoop.get).foreach(_.close())
pending.forEach(f => Util.ignoreResult(f.cancel(true)))
pending.clear()
scheduler.shutdownNow()
executor.shutdownNow()
if (!executor.awaitTermination(1, TimeUnit.SECONDS) ||
!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
throw new TimeoutException
}
}
override protected def clearTimings: Boolean = true
override def initial(): Unit = ()
private[this] def doReport(): Unit = {
val runnable: Runnable = () => {
if (nextReport.get.isOverdue) {
report()
}
}
Util.ignoreResult(pending.add(executor.submit(runnable)))
}
override def beforeWork(task: Task[_]): Unit = {
maybeStartThread()
super.beforeWork(task)
tasks.put(task)
reportLoop.get match {
case null =>
val loop = schedule(sleepDuration, recurring = true)(doReport())
reportLoop.getAndSet(loop) match {
case null =>
case l =>
reportLoop.set(l)
loop.close()
}
case s =>
}
}
override def afterReady(task: Task[_]): Unit = maybeStartThread()
override def afterCompleted[A](task: Task[A], result: Result[A]): Unit = maybeStartThread()
override def afterReady(task: Task[_]): Unit =
Util.ignoreResult(active.put(task, schedule(threshold, recurring = false)(doReport())))
override def stop(): Unit = {}
override def stop(): Unit = currentProgressThread.synchronized {
currentProgressThread.getAndSet(None).foreach(_.close())
}
override def afterCompleted[A](task: Task[A], result: Result[A]): Unit =
active.remove(task) match {
case null =>
case a =>
a.close()
if (exceededThreshold(task, threshold)) report()
}
override def afterAllCompleted(results: RMap[Task, Result]): Unit = {
reset()
reportLoop.getAndSet(null) match {
case null =>
case l => l.close()
}
// send an empty progress report to clear out the previous report
appendProgress(ProgressEvent("Info", Vector(), Some(lastTaskCount.get), None, None))
}
@ -117,51 +118,39 @@ private[sbt] class TaskProgress private ()
"consoleQuick",
"state"
)
private[this] def maybeStartThread(): Unit = {
currentProgressThread.get() match {
case None =>
currentProgressThread.synchronized {
currentProgressThread.get() match {
case None => currentProgressThread.set(Some(new ProgressThread))
case _ =>
}
}
case _ =>
}
}
private[this] def appendProgress(event: ProgressEvent): Unit =
StandardMain.exchange.updateProgress(event)
private[this] def active: Vector[Task[_]] = activeTasks.toVector.filterNot(Def.isDummy)
private[this] def activeExceedingThreshold: Vector[(Task[_], Long)] = active.flatMap { task =>
timings.get(task) match {
case null => None
case t =>
val elapsed = t.currentElapsedMicros
if (elapsed.micros > threshold) Some[(Task[_], Long)](task -> elapsed) else None
private[this] def report(): Unit = {
val currentTasks = timings(active.keySet, threshold.toMicros)
val ltc = lastTaskCount.get
if (currentTasks.nonEmpty || ltc != 0) {
val currentTasksCount = currentTasks.size
def event(tasks: Vector[(Task[_], Long)]): ProgressEvent = {
if (tasks.nonEmpty) nextReport.set(Deadline.now + sleepDuration)
val toWrite = tasks.sortBy(_._2)
val distinct = new java.util.LinkedHashMap[String, ProgressItem]
toWrite.foreach {
case (task, elapsed) =>
val name = taskName(task)
distinct.put(name, ProgressItem(name, elapsed))
}
ProgressEvent(
"Info",
distinct.values.asScala.toVector,
Some(ltc),
None,
None,
None,
Some(containsSkipTasks(active.keySet))
)
}
lastTaskCount.set(currentTasksCount)
appendProgress(event(currentTasks))
}
}
private[this] def report(): Unit = {
val currentTasks = activeExceedingThreshold
val ltc = lastTaskCount.get
val currentTasksCount = currentTasks.size
def event(tasks: Vector[(Task[_], Long)]): ProgressEvent = ProgressEvent(
"Info",
tasks
.map { case (task, elapsed) => ProgressItem(taskName(task), elapsed) }
.sortBy(_.elapsedMicros),
Some(ltc),
None,
None,
None,
Some(containsSkipTasks(active))
)
if (active.nonEmpty) maybeStartThread()
lastTaskCount.set(currentTasksCount)
appendProgress(event(currentTasks))
}
private[this] def containsSkipTasks(tasks: Vector[Task[_]]): Boolean = {
tasks.map(taskName).exists { n =>
private[this] def containsSkipTasks(tasks: java.util.Set[Task[_]]): Boolean = {
tasks.iterator.asScala.map(taskName).exists { n =>
val shortName = n.lastIndexOf('/') match {
case -1 => n
case i =>

View File

@ -31,7 +31,6 @@ private[sbt] final class TaskTimings(reportOnShutdown: Boolean, logger: Logger)
override def log(level: Level.Value, message: => String): Unit =
ConsoleOut.systemOut.println(message)
})
import AbstractTaskExecuteProgress.Timer
private[this] var start = 0L
private[this] val threshold = SysProp.taskTimingsThreshold
private[this] val omitPaths = SysProp.taskTimingsOmitPaths
@ -61,15 +60,12 @@ private[sbt] final class TaskTimings(reportOnShutdown: Boolean, logger: Logger)
private[this] def report() = {
val total = divide(System.nanoTime - start)
logger.info(s"Total time: $total $unit")
import collection.JavaConverters._
def sumTimes(in: Seq[(Task[_], Timer)]) = in.map(_._2.durationNanos).sum
val timingsByName = timings.asScala.toSeq.groupBy { case (t, _) => taskName(t) } mapValues (sumTimes)
val times = timingsByName.toSeq
.sortBy(_._2)
.sortBy(_._2.get)
.reverse
.map {
case (name, time) =>
(if (omitPaths) reFilePath.replaceFirstIn(name, "") else name, divide(time))
(if (omitPaths) reFilePath.replaceFirstIn(name, "") else name, divide(time.get))
}
.filter { _._2 > threshold }
if (times.size > 0) {

View File

@ -13,7 +13,6 @@ import java.nio.file.Files
import sbt.internal.util.{ RMap, ConsoleOut }
import sbt.io.IO
import sbt.io.syntax._
import scala.collection.JavaConverters._
import sjsonnew.shaded.scalajson.ast.unsafe.JString
import sjsonnew.support.scalajson.unsafe.CompactPrinter
@ -39,7 +38,7 @@ private[sbt] final class TaskTraceEvent
ShutdownHooks.add(() => report())
private[this] def report() = {
if (timings.asScala.nonEmpty) {
if (anyTimings) {
writeTraceEvent()
}
}
@ -63,10 +62,10 @@ private[sbt] final class TaskTraceEvent
CompactPrinter.print(new JString(name), sb)
s"""{"name": ${sb.toString}, "cat": "$cat", "ph": "X", "ts": ${(t.startMicros)}, "dur": ${(t.durationMicros)}, "pid": 0, "tid": ${t.threadId}}"""
}
val entryIterator = timings.entrySet().iterator()
val entryIterator = currentTimings
while (entryIterator.hasNext) {
val entry = entryIterator.next()
trace.append(durationEvent(taskName(entry.getKey), "task", entry.getValue))
val (key, value) = entryIterator.next()
trace.append(durationEvent(taskName(key), "task", value))
if (entryIterator.hasNext) trace.append(",")
}
trace.append("]}")

View File

@ -59,17 +59,16 @@ private[sbt] class XMainConfiguration {
val topLoader = configuration.provider.scalaProvider.launcher.topLoader
val updatedConfiguration =
try {
val method = topLoader.getClass.getMethod("getEarlyJars")
val method = topLoader.getClass.getMethod("getJLineJars")
val jars = method.invoke(topLoader).asInstanceOf[Array[URL]]
var canReuseConfiguration = jars.length == 3
var j = 0
while (j < jars.length && canReuseConfiguration) {
val s = jars(j).toString
canReuseConfiguration =
s.contains("jline") || s.contains("test-interface") || s.contains("jansi")
canReuseConfiguration = s.contains("jline") || s.contains("jansi")
j += 1
}
if (canReuseConfiguration) configuration else makeConfiguration(configuration)
if (canReuseConfiguration && j == 3) configuration else makeConfiguration(configuration)
} catch {
case _: NoSuchMethodException => makeConfiguration(configuration)
}

View File

@ -49,6 +49,7 @@ import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter }
import BasicJsonProtocol._
import Serialization.{ attach, promptChannel }
import sbt.internal.util.ProgressState
final class NetworkChannel(
val name: String,
@ -58,7 +59,8 @@ final class NetworkChannel(
instance: ServerInstance,
handlers: Seq[ServerHandler],
val log: Logger,
mkUIThreadImpl: (State, CommandChannel) => UITask
mkUIThreadImpl: (State, CommandChannel) => UITask,
state: Option[State],
) extends CommandChannel { self =>
def this(
name: String,
@ -77,7 +79,8 @@ final class NetworkChannel(
instance,
handlers,
log,
new UITask.AskUserTask(_, _)
new UITask.AskUserTask(_, _),
None
)
private val running = new AtomicBoolean(true)
@ -110,7 +113,7 @@ final class NetworkChannel(
}
private[sbt] def write(byte: Byte) = inputBuffer.add(byte)
private[this] val terminalHolder = new AtomicReference(Terminal.NullTerminal)
private[this] val terminalHolder = new AtomicReference[Terminal](Terminal.NullTerminal)
override private[sbt] def terminal: Terminal = terminalHolder.get
override val userThread: UserThread = new UserThread(this)
@ -152,8 +155,8 @@ final class NetworkChannel(
if (interactive.get || ContinuousCommands.isInWatch(state, this)) mkUIThreadImpl(state, command)
else
new UITask {
override private[sbt] def channel = NetworkChannel.this
override def reader: UITask.Reader = () => {
override private[sbt] val channel = NetworkChannel.this
override private[sbt] lazy val reader: UITask.Reader = () => {
try {
this.synchronized(this.wait)
Left(TerminateAction)
@ -650,6 +653,8 @@ final class NetworkChannel(
}
override def available(): Int = inputBuffer.size
}
private[this] lazy val writeableInputStream: Terminal.WriteableInputStream =
new Terminal.WriteableInputStream(inputStream, name)
import sjsonnew.BasicJsonProtocol._
import scala.collection.JavaConverters._
@ -726,7 +731,8 @@ final class NetworkChannel(
write(java.util.Arrays.copyOfRange(b, off, off + len))
}
}
private class NetworkTerminal extends TerminalImpl(inputStream, outputStream, errorStream, name) {
private class NetworkTerminal
extends TerminalImpl(writeableInputStream, outputStream, errorStream, name) {
private[this] val pending = new AtomicBoolean(false)
private[this] val closed = new AtomicBoolean(false)
private[this] val properties = new AtomicReference[TerminalPropertiesResponse]
@ -784,6 +790,10 @@ final class NetworkChannel(
)
}
private[this] val blockedThreads = ConcurrentHashMap.newKeySet[Thread]
override private[sbt] val progressState: ProgressState = new ProgressState(
1,
state.flatMap(_.get(Keys.superShellMaxTasks.key)).getOrElse(SysProp.supershellMaxTasks)
)
override def getWidth: Int = getProperty(_.width, 0).getOrElse(0)
override def getHeight: Int = getProperty(_.height, 0).getOrElse(0)
override def isAnsiSupported: Boolean = getProperty(_.isAnsiSupported, false).getOrElse(false)
@ -872,6 +882,14 @@ final class NetworkChannel(
try queue.take
catch { case _: InterruptedException => }
}
override private[sbt] def getSizeImpl: (Int, Int) =
if (!closed.get) {
import sbt.protocol.codec.JsonProtocol._
val queue = VirtualTerminal.getTerminalSize(name, jsonRpcRequest)
val res = try queue.take
catch { case _: InterruptedException => TerminalGetSizeResponse(1, 1) }
(res.width, res.height)
} else (1, 1)
override def setSize(width: Int, height: Int): Unit =
if (!closed.get) {
import sbt.protocol.codec.JsonProtocol._

View File

@ -20,7 +20,9 @@ import sbt.protocol.Serialization.{
attach,
systemIn,
terminalCapabilities,
terminalGetSize,
terminalPropertiesQuery,
terminalSetSize,
}
import sjsonnew.support.scalajson.unsafe.Converter
import sbt.protocol.{
@ -30,10 +32,13 @@ import sbt.protocol.{
TerminalCapabilitiesQuery,
TerminalCapabilitiesResponse,
TerminalPropertiesResponse,
TerminalGetSizeQuery,
TerminalGetSizeResponse,
TerminalSetAttributesCommand,
TerminalSetSizeCommand,
}
import sbt.protocol.codec.JsonProtocol._
import sbt.protocol.TerminalGetSizeResponse
object VirtualTerminal {
private[this] val pendingTerminalProperties =
@ -46,6 +51,8 @@ object VirtualTerminal {
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]]
private[this] val pendingTerminalSetSize =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]]
private[this] val pendingTerminalGetSize =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalGetSizeResponse]]
private[sbt] def sendTerminalPropertiesQuery(
channelName: String,
jsonRpcRequest: (String, String, String) => Unit
@ -111,9 +118,22 @@ object VirtualTerminal {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[Unit](1)
pendingTerminalSetSize.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, query)
jsonRpcRequest(id, terminalSetSize, query)
queue
}
private[sbt] def getTerminalSize(
channelName: String,
jsonRpcRequest: (String, String, TerminalGetSizeQuery) => Unit,
): ArrayBlockingQueue[TerminalGetSizeResponse] = {
val id = UUID.randomUUID.toString
val query = TerminalGetSizeQuery()
val queue = new ArrayBlockingQueue[TerminalGetSizeResponse](1)
pendingTerminalGetSize.put((channelName, id), queue)
jsonRpcRequest(id, terminalGetSize, query)
queue
}
val handler = ServerHandler { cb =>
ServerIntent(requestHandler(cb), responseHandler(cb), notificationHandler(cb))
}
@ -166,6 +186,13 @@ object VirtualTerminal {
case null =>
case buffer => buffer.put(())
}
case r if pendingTerminalGetSize.get((callback.name, r.id)) != null =>
val response =
r.result.flatMap(Converter.fromJson[TerminalGetSizeResponse](_).toOption)
pendingTerminalGetSize.remove((callback.name, r.id)) match {
case null =>
case buffer => buffer.put(response.getOrElse(TerminalGetSizeResponse(1, 1)))
}
}
private val notificationHandler: Handler[JsonRpcNotificationMessage] =
callback => {

View File

@ -84,8 +84,10 @@ object Dependencies {
val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash")
val jline = "org.scala-sbt.jline" % "jline" % "2.14.7-sbt-5e51b9d4f9631ebfa29753ce4accc57808e7fd6b"
val jline3 = "org.jline" % "jline" % "3.15.0"
val jline3Jansi = "org.jline" % "jline-terminal-jansi" % "3.15.0"
val jline3Version = "3.16.0" // Once the base jline version is upgraded, we can use the official jline-terminal
val jline3Terminal = "org.scala-sbt.jline3" % "jline-terminal" % s"$jline3Version-sbt-211a082ed6326908dc84ca017ce4430728f18a8a"
val jline3Jansi = "org.jline" % "jline-terminal-jansi" % jline3Version
val jline3Reader = "org.jline" % "jline-reader" % jline3Version
val jansi = "org.fusesource.jansi" % "jansi" % "1.18"
val scalatest = "org.scalatest" %% "scalatest" % "3.0.8"
val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0"

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 TerminalGetSizeQuery private () extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case _: TerminalGetSizeQuery => true
case _ => false
}
override def hashCode: Int = {
37 * (17 + "sbt.protocol.TerminalGetSizeQuery".##)
}
override def toString: String = {
"TerminalGetSizeQuery()"
}
private[this] def copy(): TerminalGetSizeQuery = {
new TerminalGetSizeQuery()
}
}
object TerminalGetSizeQuery {
def apply(): TerminalGetSizeQuery = new TerminalGetSizeQuery()
}

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 TerminalGetSizeResponse private (
val width: Int,
val height: Int) extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalGetSizeResponse => (this.width == x.width) && (this.height == x.height)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (17 + "sbt.protocol.TerminalGetSizeResponse".##) + width.##) + height.##)
}
override def toString: String = {
"TerminalGetSizeResponse(" + width + ", " + height + ")"
}
private[this] def copy(width: Int = width, height: Int = height): TerminalGetSizeResponse = {
new TerminalGetSizeResponse(width, height)
}
def withWidth(width: Int): TerminalGetSizeResponse = {
copy(width = width)
}
def withHeight(height: Int): TerminalGetSizeResponse = {
copy(height = height)
}
}
object TerminalGetSizeResponse {
def apply(width: Int, height: Int): TerminalGetSizeResponse = new TerminalGetSizeResponse(width, height)
}

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 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")
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.TerminalGetSizeQueryFormats with sbt.protocol.codec.TerminalSetSizeCommandFormats =>
implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat9[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.TerminalGetSizeQuery, 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 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")
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.TerminalGetSizeResponseFormats with sbt.protocol.codec.TerminalSetSizeResponseFormats =>
implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat11[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.TerminalGetSizeResponse, sbt.protocol.TerminalSetSizeResponse]("type")
}

View File

@ -12,6 +12,7 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.protocol.codec.TerminalCapabilitiesQueryFormats
with sbt.protocol.codec.TerminalSetAttributesCommandFormats
with sbt.protocol.codec.TerminalAttributesQueryFormats
with sbt.protocol.codec.TerminalGetSizeQueryFormats
with sbt.protocol.codec.TerminalSetSizeCommandFormats
with sbt.protocol.codec.CommandMessageFormats
with sbt.protocol.codec.CompletionParamsFormats
@ -25,6 +26,7 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.protocol.codec.TerminalCapabilitiesResponseFormats
with sbt.protocol.codec.TerminalSetAttributesResponseFormats
with sbt.protocol.codec.TerminalAttributesResponseFormats
with sbt.protocol.codec.TerminalGetSizeResponseFormats
with sbt.protocol.codec.TerminalSetSizeResponseFormats
with sbt.protocol.codec.EventMessageFormats
with sbt.protocol.codec.SettingQueryResponseFormats

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 TerminalGetSizeQueryFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalGetSizeQueryFormat: JsonFormat[sbt.protocol.TerminalGetSizeQuery] = new JsonFormat[sbt.protocol.TerminalGetSizeQuery] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalGetSizeQuery = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
unbuilder.endObject()
sbt.protocol.TerminalGetSizeQuery()
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalGetSizeQuery, 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 TerminalGetSizeResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalGetSizeResponseFormat: JsonFormat[sbt.protocol.TerminalGetSizeResponse] = new JsonFormat[sbt.protocol.TerminalGetSizeResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalGetSizeResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val width = unbuilder.readField[Int]("width")
val height = unbuilder.readField[Int]("height")
unbuilder.endObject()
sbt.protocol.TerminalGetSizeResponse(width, height)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalGetSizeResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("width", obj.width)
builder.addField("height", obj.height)
builder.endObject()
}
}
}

View File

@ -126,6 +126,12 @@ type TerminalAttributesResponse implements EventMessage {
cchars: String!,
}
type TerminalGetSizeQuery implements CommandMessage {}
type TerminalGetSizeResponse implements EventMessage {
width: Int!
height: Int!
}
type TerminalSetSizeCommand implements CommandMessage {
width: Int!
height: Int!

View File

@ -39,7 +39,8 @@ object Serialization {
val promptChannel = "sbt/promptChannel"
val setTerminalAttributes = "sbt/setTerminalAttributes"
val getTerminalAttributes = "sbt/getTerminalAttributes"
val setTerminalSize = "sbt/setTerminalSize"
val terminalGetSize = "sbt/terminalGetSize"
val terminalSetSize = "sbt/terminalSetSize"
val CancelAll = "__CancelAll"
@deprecated("unused", since = "1.4.0")