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")