Support scala 2.13 console in thin client

In order to make the console task work with scala 2.13 and the thin
client, we need to provide a way for the scala repl to use an sbt
provided jline3 terminal instead of the default terminal typically built
by the repl. We also need to put jline 3 higher up in the classloading
hierarchy to ensure that two versions of jline 3 are not loaded (which
makes it impossible to share the sbt terminal with the scala terminal).

One impact of this change is the decoupling of the version of
jline-terminal used by the in process scala console and the version
of jline-terminal specified by the scala version itself. It is possible
to override this by setting the `useScalaReplJLine` flag to true. When
that is set, the scala REPL will run in a fully isolated classloader. That
will ensure that the versions are consistent. It will, however, for sure
break the thin client and may interfere with the embedded shell ui.

As part of this work, I also discovered that jline 3 Terminal.getSize is
very slow. In jline 2, the terminal attributes were automatically cached with a
timeout of, I think, 1 second so it wasn't a big deal to call
Terminal.getAttributes. The getSize method in jline 3 is not cached and
it shells out to run a tty command. This caused a significant
performance regression in sbt because when progress is enabled, we call
Terminal.getSize whenever we log any messages. I added caching of
getSize at the TerminalImpl level to address this. The timeout is 1
second, which seems responsive enough for most use cases. We could also
move the calculation onto a background thread and have it periodically
updated, but that seems like overkill.
This commit is contained in:
Ethan Atkins 2020-07-20 10:12:04 -07:00
parent 6dd69a54ae
commit 90dacc339c
27 changed files with 352 additions and 62 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
@ -1103,7 +1105,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

@ -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

@ -7,7 +7,7 @@
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, AtomicReference }
@ -22,7 +22,7 @@ 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 =>
@ -109,18 +109,18 @@ private[util] object JLine3 {
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))

View File

@ -16,6 +16,7 @@ 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
@ -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
@ -500,7 +498,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
@ -674,13 +671,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()
@ -767,10 +764,12 @@ object Terminal {
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 def isEchoEnabled: Boolean = system.echo()
override def isSuccessEnabled: Boolean = true
override def getBooleanCapability(capability: String, jline3: Boolean): Boolean =
@ -785,7 +784,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 =
@ -825,6 +824,19 @@ object Terminal {
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
def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f

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

@ -49,6 +49,8 @@ private[sbt] object UITask {
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
@ -59,9 +61,9 @@ private[sbt] object UITask {
val clear = terminal.ansi(ClearPromptLine, "")
@tailrec def impl(): Either[String, String] = {
val thread = Thread.currentThread
if (thread.isInterrupted || closed.get) throw new InterruptedException
if (thread.isInterrupted || closed.get) throw interrupted
val reader = LineReader.createReader(history(state), parser, terminal)
if (thread.isInterrupted || closed.get) throw new InterruptedException
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 =>

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,
@ -386,6 +387,11 @@ 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 },
progressReports := {
val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector
@ -888,8 +894,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 +944,8 @@ object Defaults extends BuildCommon {
allJars,
Array(libraryJar),
compilerJar,
state.value.classLoaderCache
state.value.extendedClassLoaderCache,
scalaInstanceTopLoader.value,
)
}
private[this] def mkScalaInstance(
@ -940,15 +954,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 +980,8 @@ object Defaults extends BuildCommon {
dummy.allJars,
dummy.libraryJars,
dummy.compilerJar,
state.value.classLoaderCache
state.value.extendedClassLoaderCache,
scalaInstanceTopLoader.value,
)
}

View File

@ -570,6 +570,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

@ -932,6 +932,9 @@ 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)
// 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))
CheckBuildSources.init(LintUnused.lintUnusedFunc(s5))
}

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

@ -875,6 +875,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")