From 90dacc339c0bc5afdfa80b98775f235cff56101f Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 20 Jul 2020 10:12:04 -0700 Subject: [PATCH] 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. --- build.sbt | 7 ++-- .../sbt/internal/util/DeprecatedJLine.java | 21 ++++++++++ .../main/scala/sbt/internal/util/JLine3.scala | 8 ++-- .../scala/sbt/internal/util/Terminal.scala | 36 +++++++++++------ main-actions/src/main/scala/sbt/Console.scala | 4 +- main-command/src/main/scala/sbt/State.scala | 4 ++ .../internal/classpath/ClassLoaderCache.scala | 25 +++++++++--- .../sbt/internal/client/NetworkClient.scala | 11 +++++- .../main/scala/sbt/internal/ui/UITask.scala | 6 ++- .../main/java/sbt/internal/JLineLoader.java | 34 ++++++++++++++++ .../java/sbt/internal/MetaBuildLoader.java | 23 ++++++++--- main/src/main/scala/sbt/Defaults.scala | 39 ++++++++++++------- main/src/main/scala/sbt/Keys.scala | 3 ++ main/src/main/scala/sbt/Main.scala | 3 ++ .../sbt/internal/XMainConfiguration.scala | 7 ++-- .../sbt/internal/server/NetworkChannel.scala | 8 ++++ .../sbt/internal/server/VirtualTerminal.scala | 29 +++++++++++++- project/Dependencies.scala | 6 ++- .../sbt/protocol/TerminalGetSizeQuery.scala | 29 ++++++++++++++ .../protocol/TerminalGetSizeResponse.scala | 36 +++++++++++++++++ .../codec/CommandMessageFormats.scala | 4 +- .../protocol/codec/EventMessageFormats.scala | 4 +- .../sbt/protocol/codec/JsonProtocol.scala | 2 + .../codec/TerminalGetSizeQueryFormats.scala | 27 +++++++++++++ .../TerminalGetSizeResponseFormats.scala | 29 ++++++++++++++ protocol/src/main/contraband/server.contra | 6 +++ .../scala/sbt/protocol/Serialization.scala | 3 +- 27 files changed, 352 insertions(+), 62 deletions(-) create mode 100644 internal/util-logging/src/main/java/sbt/internal/util/DeprecatedJLine.java create mode 100644 main/src/main/java/sbt/internal/JLineLoader.java create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeQuery.scala create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeResponse.scala create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeQueryFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeResponseFormats.scala diff --git a/build.sbt b/build.sbt index 582a7ef93..5b0fcd298 100644 --- a/build.sbt +++ b/build.sbt @@ -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 diff --git a/internal/util-logging/src/main/java/sbt/internal/util/DeprecatedJLine.java b/internal/util-logging/src/main/java/sbt/internal/util/DeprecatedJLine.java new file mode 100644 index 000000000..4e091a2a5 --- /dev/null +++ b/internal/util-logging/src/main/java/sbt/internal/util/DeprecatedJLine.java @@ -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); + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala index 2ef394a54..953042b72 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala @@ -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)) diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala index f612a7730..a440fc77b 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -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 diff --git a/main-actions/src/main/scala/sbt/Console.scala b/main-actions/src/main/scala/sbt/Console.scala index 8e9d40984..43ca79b2a 100644 --- a/main-actions/src/main/scala/sbt/Console.scala +++ b/main-actions/src/main/scala/sbt/Console.scala @@ -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 { diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index 409bac887..32fd3c7a8 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -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 diff --git a/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala b/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala index dd730db87..0ef7d3911 100644 --- a/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala +++ b/main-command/src/main/scala/sbt/internal/classpath/ClassLoaderCache.scala @@ -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 diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index b95b6f55c..46fc1fa31 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -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) diff --git a/main-command/src/main/scala/sbt/internal/ui/UITask.scala b/main-command/src/main/scala/sbt/internal/ui/UITask.scala index a4144b3f8..dce4b1012 100644 --- a/main-command/src/main/scala/sbt/internal/ui/UITask.scala +++ b/main-command/src/main/scala/sbt/internal/ui/UITask.scala @@ -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 => diff --git a/main/src/main/java/sbt/internal/JLineLoader.java b/main/src/main/java/sbt/internal/JLineLoader.java new file mode 100644 index 000000000..098b21bc9 --- /dev/null +++ b/main/src/main/java/sbt/internal/JLineLoader.java @@ -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(); + } +} diff --git a/main/src/main/java/sbt/internal/MetaBuildLoader.java b/main/src/main/java/sbt/internal/MetaBuildLoader.java index a027314b1..2ca2a5088 100644 --- a/main/src/main/java/sbt/internal/MetaBuildLoader.java +++ b/main/src/main/java/sbt/internal/MetaBuildLoader.java @@ -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 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); } } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index cb3c28f05..7fbf1aec9 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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, ) } diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 3609cf245..5da958c30 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 70e291940..c746856fe 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -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)) } diff --git a/main/src/main/scala/sbt/internal/XMainConfiguration.scala b/main/src/main/scala/sbt/internal/XMainConfiguration.scala index e4ebdf6bc..93dcb4e3b 100644 --- a/main/src/main/scala/sbt/internal/XMainConfiguration.scala +++ b/main/src/main/scala/sbt/internal/XMainConfiguration.scala @@ -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) } diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index fd461c06f..ca75b369a 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -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._ diff --git a/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala b/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala index c299c0f4c..497b78c0c 100644 --- a/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala +++ b/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala @@ -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 => { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 63cd768e4..7f1f1c68c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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" diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeQuery.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeQuery.scala new file mode 100644 index 000000000..70fa74148 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeQuery.scala @@ -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() +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeResponse.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeResponse.scala new file mode 100644 index 000000000..d3ebcffe9 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalGetSizeResponse.scala @@ -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) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala index 1ecd02122..56d3b1757 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala @@ -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") } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala index 5475a901b..9aff6f3cf 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala @@ -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") } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala index e3a6e2b99..7c5046c6d 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala @@ -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 diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeQueryFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeQueryFormats.scala new file mode 100644 index 000000000..9989ad96c --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeQueryFormats.scala @@ -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() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeResponseFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeResponseFormats.scala new file mode 100644 index 000000000..c5c489a8d --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalGetSizeResponseFormats.scala @@ -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() + } +} +} diff --git a/protocol/src/main/contraband/server.contra b/protocol/src/main/contraband/server.contra index 2cde1ec82..37c2b7e08 100644 --- a/protocol/src/main/contraband/server.contra +++ b/protocol/src/main/contraband/server.contra @@ -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! diff --git a/protocol/src/main/scala/sbt/protocol/Serialization.scala b/protocol/src/main/scala/sbt/protocol/Serialization.scala index a8f2e54b6..35dd19149 100644 --- a/protocol/src/main/scala/sbt/protocol/Serialization.scala +++ b/protocol/src/main/scala/sbt/protocol/Serialization.scala @@ -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")