Merge pull request #5620 from eatkins/wip-sbt-instant-startup

Nearly instantaneous sbt startup with remote client
This commit is contained in:
Ethan Atkins 2020-07-02 15:26:59 -07:00 committed by GitHub
commit 0941415420
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
118 changed files with 6004 additions and 1862 deletions

View File

@ -1,29 +1,158 @@
image: Visual Studio 2017
image:
- MacOS
- Visual Studio 2015
- Visual Studio 2019
- Ubuntu
build: off
init:
- git config --global core.autocrlf input
install:
- SET JAVA_HOME=C:\Program Files\Java\jdk1.8.0
- SET PATH=%JAVA_HOME%\bin;%PATH%
- SET CI=true
for:
-
matrix:
only:
- image: Ubuntu
- ps: |
Add-Type -AssemblyName System.IO.Compression.FileSystem
if (!(Test-Path -Path "C:\sbt" )) {
(new-object System.Net.WebClient).DownloadFile(
'https://github.com/sbt/sbt/releases/download/v1.0.4/sbt-1.0.4.zip',
'C:\sbt-bin.zip'
)
[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\sbt-bin.zip", "C:\sbt")
}
- SET PATH=C:\sbt\sbt\bin;%PATH%
- SET SBT_OPTS=-XX:MaxPermSize=2g -Xmx4g -Dsbt.supershell=never -Dfile.encoding=UTF8
test_script:
- sbt "scripted actions/* classloader-cache/* nio/* watch/*" "testOnly sbt.ServerSpec"
branches:
only:
- build-graal
artifacts:
- path: client/target/bin/sbtc
name: sbtc
cache:
- '%LOCALAPPDATA%\Coursier\Cache\v1'
- '%USERPROFILE%\.ivy2\cache'
- '%USERPROFILE%\.sbt'
install:
- curl -sL https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.tgz > ~/sbt-bin.tgz
- mkdir ~/sbt
- tar -xf ~/sbt-bin.tgz --directory ~/sbt
- curl -sL https://raw.githubusercontent.com/shyiko/jabba/0.11.0/install.sh | bash && . ~/.jabba/jabba.sh
- jabba install adopt@1.8.0-222
- jabba use adopt@1.8.0-222
- curl -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java8-linux-amd64-20.1.0.tar.gz > graalvm.tar.gz
- tar -xf graalvm.tar.gz
- export PATH="~/sbt/sbt/bin:$PATH"
- export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-222/bin"
- export JAVA_HOME="~/.jabba/jdk/adopt@1.8.0-222"
test_script:
- export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-222/bin"
- export PATH="$PATH:graalvm-ce-java8-20.1.0/bin"
- gu install native-image
- sbt -Dsbt.native-image=$(pwd)/graalvm-ce-java8-20.1.0/bin/native-image "sbtClientProj/genNativeExecutable"
-
matrix:
only:
- image: MacOS
branches:
only:
- build-graal
artifacts:
- path: client/target/bin/sbtc
name: mac-native-sbt-client
install:
- curl -sL https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.tgz > ~/sbt-bin.tgz
- mkdir ~/sbt
- tar -xf ~/sbt-bin.tgz --directory ~/sbt
- curl -sL https://raw.githubusercontent.com/shyiko/jabba/0.11.0/install.sh | bash && . ~/.jabba/jabba.sh
- jabba install adopt@1.8.0-222
- jabba use adopt@1.8.0-222
- curl -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java8-darwin-amd64-20.1.0.tar.gz > graalvm.tar.gz
- tar -xf graalvm.tar.gz
- export PATH="~/sbt/sbt/bin:$PATH"
- export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-222/bin"
- export JAVA_HOME="~/.jabba/jdk/adopt@1.8.0-222"
test_script:
- export PATH="$PATH:~/.jabba/jdk/adopt@1.8.0-222/Contents/Home/bin"
- export PATH="$PATH:graalvm-ce-java8-20.1.0/Contents/Home/bin"
- gu install native-image
- sbt -Dsbt.native-image=$(pwd)/graalvm-ce-java8-20.1.0/Contents/Home/bin/native-image "sbtClientProj/genNativeExecutable"
-
matrix:
only:
- image: Visual Studio 2015
branches:
only:
- build-graal
artifacts:
- path: client\target\bin\sbtc.exe
name: sbtc.exe
install:
- cinst jdk8 -params 'installdir=C:\\jdk8'
- SET CI=true
#- choco install windows-sdk-7.1 kb2519277
- call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd"
- ps: |
Add-Type -AssemblyName System.IO.Compression.FileSystem
if (!(Test-Path -Path "C:\sbt" )) {
(new-object System.Net.WebClient).DownloadFile(
'https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.zip',
'C:\sbt-bin.zip'
)
[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\sbt-bin.zip", "C:\sbt")
}
if (!(Test-Path -Path "C:\graalvm-ce-java8-20.2.0-dev" )) {
(new-object System.Net.WebClient).DownloadFile(
'https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java8-windows-amd64-20.1.0.zip',
'C:\graalvm-ce-java8-20.1.0.zip'
)
[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\graalvm-ce-java8-20.1.0.zip", "C:\")
}
if (!(Test-Path -Path "C:\zulu-jdk7" )) {
(new-object System.Net.WebClient).DownloadFile(
'https://cdn.azul.com/zulu/bin/zulu7.38.0.11-ca-jdk7.0.262-win_x64.zip',
'C:\zulu-jdk7.zip'
)
[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\zulu-jdk7.zip", "C:\")
}
- SET PATH=C:\graalvm-ce-java8-20.1.0\bin;%PATH%
- SET PATH=C:\sbt\sbt\bin;%PATH%
- SET JAVA_HOME=C:\jdk8
- gu install native-image
cache:
- '%USERPROFILE%\.ivy2\cache'
- '%LOCALAPPDATA%\Coursier\Cache\v1'
- '%USERPROFILE%\.sbt'
test_script:
- sbt "-Dsbt.native-image=C:\graalvm-ce-java8-20.1.0\bin\native-image.cmd" "sbtClientProj/genNativeExecutable"
-
matrix:
only:
- image: Visual Studio 2019
branches:
except:
- build-graal
install:
- cinst jdk8 -params 'installdir=C:\\jdk8'
- SET JAVA_HOME=C:\jdk8
- SET PATH=C:\jdk8\bin;%PATH%
- SET CI=true
- ps: |
Add-Type -AssemblyName System.IO.Compression.FileSystem
if (!(Test-Path -Path "C:\sbt" )) {
(new-object System.Net.WebClient).DownloadFile(
'https://github.com/sbt/sbt/releases/download/v1.3.10/sbt-1.3.10.zip',
'C:\sbt-bin.zip'
)
[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\sbt-bin.zip", "C:\sbt")
}
- SET PATH=C:\sbt\sbt\bin;%PATH%
- SET SBT_OPTS=-Xmx4g -Dsbt.supershell=never -Dfile.encoding=UTF8
cache:
- '%USERPROFILE%\.ivy2\cache'
- '%LOCALAPPDATA%\Coursier\Cache\v1'
- '%USERPROFILE%\.sbt'
test_script:
- sbt "scripted actions/* classloader-cache/* nio/* watch/*" "serverTestProj/test"

120
build.sbt
View File

@ -4,6 +4,7 @@ import Util._
import com.typesafe.tools.mima.core.ProblemFilters._
import com.typesafe.tools.mima.core._
import local.Scripted
import java.nio.file.{ Files, Path => JPath }
import scala.util.Try
@ -213,6 +214,8 @@ lazy val sbtRoot: Project = (project in file("."))
.single("sbtOn")((state, dir) => s"sbtProj/test:runMain sbt.RunFromSourceMain $dir" :: state),
mimaSettings,
mimaPreviousArtifacts := Set.empty,
genExecutable := (sbtClientProj / genExecutable).evaluated,
genNativeExecutable := (sbtClientProj / genNativeExecutable).value,
)
// This is used to configure an sbt-launcher for this version of sbt.
@ -955,6 +958,11 @@ lazy val mainProj = (project in file("main"))
exclude[DirectMissingMethodProblem]("sbt.Classpaths.warnInsecureProtocol"),
exclude[DirectMissingMethodProblem]("sbt.Classpaths.warnInsecureProtocolInModules"),
exclude[MissingClassProblem]("sbt.internal.ExternalHooks*"),
// This seems to be a mima problem. The older constructor still exists but
// mima seems to incorrectly miss the secondary constructor that provides
// the binary compatible version.
exclude[IncompatibleMethTypeProblem]("sbt.internal.server.NetworkChannel.this"),
exclude[IncompatibleSignatureProblem]("sbt.internal.DeprecatedContinuous.taskDefinitions"),
)
)
.configure(
@ -1001,6 +1009,7 @@ lazy val serverTestProj = (project in file("server-test"))
crossScalaVersions := Seq(baseScalaVersion),
publish / skip := true,
// make server tests serial
Test / watchTriggers += baseDirectory.value.toGlob / "src" / "server-test" / **,
Test / parallelExecution := false,
Test / run / connectInput := true,
Test / run / outputStrategy := Some(StdoutOutput),
@ -1011,11 +1020,117 @@ lazy val serverTestProj = (project in file("server-test"))
List(
s"-Dsbt.server.classpath=$cp",
s"-Dsbt.server.version=${version.value}",
s"-Dsbt.server.scala.version=${scalaVersion.value}"
s"-Dsbt.server.scala.version=${scalaVersion.value}",
s"-Dsbt.supershell=false",
)
},
)
val isWin = scala.util.Properties.isWin
val generateReflectionConfig = taskKey[Unit]("generate the graalvm reflection config")
val genExecutable =
inputKey[JPath]("generate a java implementation of the thin client")
val graalClasspath = taskKey[String]("Generate the classpath for graal (compacted for windows)")
val graalNativeImageCommand = taskKey[String]("The native image command")
val graalNativeImageOptions = settingKey[Seq[String]]("The native image options")
val graalNativeImageClass = settingKey[String]("The class for the native image")
val genNativeExecutable = taskKey[JPath]("Generate a native executable")
val nativeExecutablePath = settingKey[JPath]("The location of the native executable")
lazy val sbtClientProj = (project in file("client"))
.dependsOn(commandProj)
.settings(
commonBaseSettings,
publish / skip := true,
name := "sbt-client",
mimaPreviousArtifacts := Set.empty,
crossPaths := false,
exportJars := true,
libraryDependencies += jansi,
libraryDependencies += "net.java.dev.jna" % "jna" % "5.5.0",
libraryDependencies += "net.java.dev.jna" % "jna-platform" % "5.5.0",
libraryDependencies += scalatest % "test",
/*
* On windows, the raw classpath is too large to be a command argument to an
* external process so we create symbolic links with short names to get the
* classpath length under the limit.
*/
graalClasspath := {
val original = (Compile / fullClasspathAsJars).value.map(_.data)
val outputDir = target.value / "graalcp"
IO.createDirectory(outputDir)
Files.walk(outputDir.toPath).forEach {
case f if f.getFileName.toString.endsWith(".jar") => Files.deleteIfExists(f)
case _ =>
}
original.zipWithIndex
.map {
case (f, i) =>
Files.createSymbolicLink(outputDir.toPath / s"$i.jar", f.toPath)
s"$i.jar"
}
.mkString(java.io.File.pathSeparator)
},
graalNativeImageCommand := System.getProperty("sbt.native-image", "native-image").toString,
genNativeExecutable / name := s"sbtc${if (isWin) ".exe" else ""}",
nativeExecutablePath := target.value.toPath / "bin" / (genNativeExecutable / name).value,
graalNativeImageClass := "sbt.client.Client",
genNativeExecutable := {
val prefix = Seq(graalNativeImageCommand.value, "-cp", graalClasspath.value)
val full = prefix ++ graalNativeImageOptions.value :+ graalNativeImageClass.value
val pb = new java.lang.ProcessBuilder(full: _*)
pb.directory(target.value / "graalcp")
val proc = pb.start()
val thread = new Thread {
setDaemon(true)
val is = proc.getInputStream
val es = proc.getErrorStream
override def run(): Unit = {
Thread.sleep(100)
while (proc.isAlive) {
if (is.available > 0 || es.available > 0) {
while (is.available > 0) System.out.print(is.read.toChar)
while (es.available > 0) System.err.print(es.read.toChar)
}
if (proc.isAlive) Thread.sleep(10)
}
}
}
thread.start()
proc.waitFor(5, java.util.concurrent.TimeUnit.MINUTES)
nativeExecutablePath.value
file("").toPath
},
graalNativeImageOptions := Seq(
"--no-fallback",
s"--initialize-at-run-time=sbt.client",
"--verbose",
"-H:IncludeResourceBundles=jline.console.completer.CandidateListCompletionHandler",
"-H:+ReportExceptionStackTraces",
"-H:-ParseRuntimeOptions",
s"-H:Name=${target.value / "bin" / "sbtc"}",
),
genExecutable := {
val isFish = Def.spaceDelimited("").parsed.headOption.fold(false)(_ == "--fish")
val ext = if (isWin) ".bat" else if (isFish) ".fish" else ".sh"
val output = target.value.toPath / "bin" / s"${if (isFish) "fish-" else ""}client$ext"
java.nio.file.Files.createDirectories(output.getParent)
val cp = (Compile / fullClasspathAsJars).value.map(_.data)
val args =
if (isWin) "%*" else if (isFish) s"$$argv" else s"$$*"
java.nio.file.Files.write(
output,
s"""
|${if (isWin) "@echo off" else s"#!/usr/bin/env ${if (isFish) "fish" else "sh"}"}
|
|java -cp ${cp.mkString(java.io.File.pathSeparator)} sbt.client.Client --jna $args
""".stripMargin.linesIterator.toSeq.tail.mkString("\n").getBytes
)
output.toFile.setExecutable(true)
output
},
)
/*
lazy val sbtBig = (project in file(".big"))
.dependsOn(sbtProj)
@ -1190,7 +1305,8 @@ def allProjects =
mainProj,
sbtProj,
bundledLauncherProj,
coreMacrosProj
coreMacrosProj,
sbtClientProj,
) ++ lowerUtilProjects
lazy val lowerUtilProjects =

5
client/completions/_sbtc Executable file
View File

@ -0,0 +1,5 @@
#compdef sbtc
COMPLETE="--completions=${words[@]}"
COMPLETIONS=($(sbtc --no-tab ${COMPLETE}))
_alternative 'arguments:custom arg:($COMPLETIONS)'

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
_do_sbtc_completions() {
COMPREPLY=($(sbtc "--completions=${COMP_LINE}"))
}
complete -F _do_sbtc_completions sbtc

4
client/completions/sbtc.fish Executable file
View File

@ -0,0 +1,4 @@
function __sbtcomp
sbtc --completions="$argv"
end
complete --command sbtc -f --arguments '(__sbtcomp (commandline -cp))'

View File

@ -0,0 +1,10 @@
$scriptblock = {
param($commandName, $line, $position)
$len = $line.ToString().length
$spaces = " " * ($position - $len)
$arg="--completions=$line$spaces"
& 'sbtc.exe' @('--no-tab', '--no-stderr', $arg)
}
Set-Alias -Name sbtc -Value sbtc.exe
Register-ArgumentCompleter -CommandName sbtc.exe -ScriptBlock $scriptBlock
Register-ArgumentCompleter -CommandName sbtc -ScriptBlock $scriptBlock

View File

@ -0,0 +1,26 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.client;
import sbt.internal.client.NetworkClient;
import java.nio.file.Paths;
import org.fusesource.jansi.AnsiConsole;
public class Client {
public static void main(final String[] args) {
boolean isWin = System.getProperty("os.name").toLowerCase().startsWith("win");
try {
if (isWin) AnsiConsole.systemInstall();
NetworkClient.main(args);
} catch (final Throwable t) {
t.printStackTrace();
} finally {
if (isWin) AnsiConsole.systemUninstall();
}
}
}

View File

@ -0,0 +1,18 @@
[
{
"name":"java.lang.ProcessBuilder",
"methods":[{"name":"redirectInput","parameterTypes":["java.lang.ProcessBuilder$Redirect"] }]
},
{
"name":"java.lang.ProcessBuilder$Redirect",
"fields":[{"name":"INHERIT"}]
},
{
"name":"java.lang.System",
"methods":[{"name":"console","parameterTypes":[] }]
},
{
"name":"jline.UnixTerminal",
"methods":[{"name":"<init>","parameterTypes":[] }]
}
]

View File

@ -0,0 +1,9 @@
{
"resources":[
{"pattern":"jline/console/completer/CandidateListCompletionHandler.properties"},
{"pattern":"library.properties"},
{"pattern":"darwin/x86_64/libsbtipcsocket.dylib"},
{"pattern":"linux/x86_64/libsbtipcsocket.so"},
{"pattern":"win32/x86_64/sbtipcsocket.dll"}
]
}

View File

@ -7,6 +7,8 @@
package sbt.internal.util
import sun.misc.{ Signal, SignalHandler }
object Signals {
val CONT = "CONT"
val INT = "INT"
@ -36,24 +38,14 @@ object Signals {
def register(handler: () => Unit, signal: String = INT): Registration =
// TODO - Maybe we can just ignore things if not is-supported.
if (supported(signal)) {
import sun.misc.{ Signal, SignalHandler }
val intSignal = new Signal(signal)
val newHandler = new SignalHandler {
def handle(sig: Signal): Unit = { handler() }
}
val oldHandler = Signal.handle(intSignal, newHandler)
object unregisterNewHandler extends Registration {
override def remove(): Unit = {
Signal.handle(intSignal, oldHandler)
()
}
}
unregisterNewHandler
new UnregisterNewHandler(intSignal, oldHandler)
} else {
// TODO - Maybe we should just throw an exception if we don't support signals...
object NullUnregisterNewHandler extends Registration {
override def remove(): Unit = ()
}
NullUnregisterNewHandler
}
@ -64,6 +56,17 @@ object Signals {
} catch { case _: LinkageError => false }
}
private class UnregisterNewHandler(intSignal: Signal, oldHandler: SignalHandler)
extends Signals.Registration {
override def remove(): Unit = {
Signal.handle(intSignal, oldHandler)
()
}
}
private object NullUnregisterNewHandler extends Signals.Registration {
override def remove(): Unit = ()
}
// Must only be referenced using a
// try { } catch { case _: LinkageError => ... }
// block to

View File

@ -9,6 +9,9 @@ package sbt.internal.util
import java.util.Locale
import scala.reflect.macros.blackbox
import scala.language.experimental.macros
object Util {
def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value)
@ -42,6 +45,8 @@ object Util {
def quoteIfKeyword(s: String): String = if (ScalaKeywords.values(s)) '`' + s + '`' else s
def ignoreResult[T](f: => T): Unit = macro Macro.ignore
lazy val isWindows: Boolean =
System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")
@ -63,4 +68,7 @@ object Util {
implicit class AnyOps[A](private val value: A) extends AnyVal {
def some: Option[A] = (Some(value): Option[A])
}
class Macro(val c: blackbox.Context) {
def ignore(f: c.Tree): c.Expr[Unit] = c.universe.reify({ c.Expr[Any](f).splice; () })
}
}

View File

@ -18,7 +18,6 @@ import scala.concurrent.duration._
trait LineReader {
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
def redraw(): Unit = ()
}
object LineReader {
@ -26,8 +25,12 @@ object LineReader {
!java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT)
val MaxHistorySize = 500
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = {
val cr = Terminal.createReader(in)
def createReader(
historyPath: Option[File],
terminal: Terminal,
prompt: Prompt = Prompt.Running,
): ConsoleReader = {
val cr = Terminal.createReader(terminal, prompt)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
@ -36,9 +39,11 @@ object LineReader {
}
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr.setHistoryEnabled(true)
cr
}
def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal)
def simple(
historyPath: Option[File],
handleCONT: Boolean = HandleCONT,
@ -56,18 +61,13 @@ abstract class JLine extends LineReader {
override def readLine(prompt: String, mask: Option[Char] = None): Option[String] =
try {
Terminal.withRawSystemIn(unsynchronizedReadLine(prompt, mask))
unsynchronizedReadLine(prompt, mask)
} catch {
case _: InterruptedException =>
// println("readLine: InterruptedException")
Option("")
}
override def redraw(): Unit = {
reader.drawLine()
reader.flush()
}
private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] =
readLineWithHistory(prompt, mask) map { x =>
x.trim
@ -144,16 +144,19 @@ private[sbt] object JLine {
* For accessing the JLine Terminal object.
* This ensures synchronized access as well as re-enabling echo after getting the Terminal.
*/
@deprecated("Don't use jline.Terminal directly. Use Terminal.withCanonicalIn instead.", "1.4.0")
@deprecated(
"Don't use jline.Terminal directly. Use Terminal.get.withCanonicalIn instead.",
"1.4.0"
)
def usingTerminal[T](f: jline.Terminal => T): T =
Terminal.withCanonicalIn(f(Terminal.deprecatedTeminal))
Terminal.get.withCanonicalIn(f(Terminal.get.toJLine))
@deprecated("unused", "1.4.0")
def createReader(): ConsoleReader = createReader(None, Terminal.wrappedSystemIn)
@deprecated("Use LineReader.createReader", "1.4.0")
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = {
val cr = Terminal.createReader(in)
val cr = Terminal.createReader(Terminal.console, Prompt.Running)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
@ -165,8 +168,8 @@ private[sbt] object JLine {
cr
}
@deprecated("Avoid referencing JLine directly. Use Terminal.withRawSystemIn instead.", "1.4.0")
def withJLine[T](action: => T): T = Terminal.withRawSystemIn(action)
@deprecated("Avoid referencing JLine directly.", "1.4.0")
def withJLine[T](action: => T): T = Terminal.get.withRawSystemIn(action)
@deprecated("Use LineReader.simple instead", "1.4.0")
def simple(
@ -211,7 +214,7 @@ final class FullReader(
historyPath: Option[File],
complete: Parser[_],
val handleCONT: Boolean,
inputStream: InputStream,
terminal: Terminal
) extends JLine {
@deprecated("Use the constructor with no injectThreadSleep parameter", "1.4.0")
def this(
@ -219,9 +222,15 @@ final class FullReader(
complete: Parser[_],
handleCONT: Boolean = LineReader.HandleCONT,
injectThreadSleep: Boolean = false
) = this(historyPath, complete, handleCONT, JLine.makeInputStream(injectThreadSleep))
) =
this(
historyPath,
complete,
handleCONT,
Terminal.console
)
protected[this] val reader: ConsoleReader = {
val cr = LineReader.createReader(historyPath, inputStream)
val cr = LineReader.createReader(historyPath, terminal)
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
cr
}
@ -230,12 +239,15 @@ final class FullReader(
class SimpleReader private[sbt] (
historyPath: Option[File],
val handleCONT: Boolean,
inputStream: InputStream
terminal: Terminal
) extends JLine {
def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
this(historyPath, handleCONT, Terminal.wrappedSystemIn)
this(historyPath, handleCONT, Terminal.console)
protected[this] val reader: ConsoleReader =
LineReader.createReader(historyPath, inputStream)
LineReader.createReader(historyPath, terminal)
}
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false)
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) {
def apply(terminal: Terminal): SimpleReader =
new SimpleReader(None, LineReader.HandleCONT, terminal)
}

View File

@ -98,7 +98,7 @@ object JLineCompletion {
f: (String, Int) => (Seq[String], Seq[String])
): (ConsoleReader, Int) => Boolean =
(reader, level) => {
val success = complete(beforeCursor(reader), reader => f(reader, level), reader)
val success = complete(beforeCursor(reader), string => f(string, level), reader)
reader.flush()
success
}

View File

@ -10,22 +10,24 @@ final class ProgressEvent private (
val items: Vector[sbt.internal.util.ProgressItem],
val lastTaskCount: Option[Int],
channelName: Option[String],
execId: Option[String]) extends sbt.internal.util.AbstractEntry(channelName, execId) with Serializable {
execId: Option[String],
val command: Option[String],
val skipIfActive: Option[Boolean]) extends sbt.internal.util.AbstractEntry(channelName, execId) with Serializable {
private def this(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Option[Int], channelName: Option[String], execId: Option[String]) = this(level, items, lastTaskCount, channelName, execId, None, None)
override def equals(o: Any): Boolean = o match {
case x: ProgressEvent => (this.level == x.level) && (this.items == x.items) && (this.lastTaskCount == x.lastTaskCount) && (this.channelName == x.channelName) && (this.execId == x.execId)
case x: ProgressEvent => (this.level == x.level) && (this.items == x.items) && (this.lastTaskCount == x.lastTaskCount) && (this.channelName == x.channelName) && (this.execId == x.execId) && (this.command == x.command) && (this.skipIfActive == x.skipIfActive)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.ProgressEvent".##) + level.##) + items.##) + lastTaskCount.##) + channelName.##) + execId.##)
37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.util.ProgressEvent".##) + level.##) + items.##) + lastTaskCount.##) + channelName.##) + execId.##) + command.##) + skipIfActive.##)
}
override def toString: String = {
"ProgressEvent(" + level + ", " + items + ", " + lastTaskCount + ", " + channelName + ", " + execId + ")"
"ProgressEvent(" + level + ", " + items + ", " + lastTaskCount + ", " + channelName + ", " + execId + ", " + command + ", " + skipIfActive + ")"
}
private[this] def copy(level: String = level, items: Vector[sbt.internal.util.ProgressItem] = items, lastTaskCount: Option[Int] = lastTaskCount, channelName: Option[String] = channelName, execId: Option[String] = execId): ProgressEvent = {
new ProgressEvent(level, items, lastTaskCount, channelName, execId)
private[this] def copy(level: String = level, items: Vector[sbt.internal.util.ProgressItem] = items, lastTaskCount: Option[Int] = lastTaskCount, channelName: Option[String] = channelName, execId: Option[String] = execId, command: Option[String] = command, skipIfActive: Option[Boolean] = skipIfActive): ProgressEvent = {
new ProgressEvent(level, items, lastTaskCount, channelName, execId, command, skipIfActive)
}
def withLevel(level: String): ProgressEvent = {
copy(level = level)
@ -51,9 +53,23 @@ final class ProgressEvent private (
def withExecId(execId: String): ProgressEvent = {
copy(execId = Option(execId))
}
def withCommand(command: Option[String]): ProgressEvent = {
copy(command = command)
}
def withCommand(command: String): ProgressEvent = {
copy(command = Option(command))
}
def withSkipIfActive(skipIfActive: Option[Boolean]): ProgressEvent = {
copy(skipIfActive = skipIfActive)
}
def withSkipIfActive(skipIfActive: Boolean): ProgressEvent = {
copy(skipIfActive = Option(skipIfActive))
}
}
object ProgressEvent {
def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Option[Int], channelName: Option[String], execId: Option[String]): ProgressEvent = new ProgressEvent(level, items, lastTaskCount, channelName, execId)
def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Int, channelName: String, execId: String): ProgressEvent = new ProgressEvent(level, items, Option(lastTaskCount), Option(channelName), Option(execId))
def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Option[Int], channelName: Option[String], execId: Option[String], command: Option[String], skipIfActive: Option[Boolean]): ProgressEvent = new ProgressEvent(level, items, lastTaskCount, channelName, execId, command, skipIfActive)
def apply(level: String, items: Vector[sbt.internal.util.ProgressItem], lastTaskCount: Int, channelName: String, execId: String, command: String, skipIfActive: Boolean): ProgressEvent = new ProgressEvent(level, items, Option(lastTaskCount), Option(channelName), Option(execId), Option(command), Option(skipIfActive))
}

View File

@ -16,8 +16,10 @@ implicit lazy val ProgressEventFormat: JsonFormat[sbt.internal.util.ProgressEven
val lastTaskCount = unbuilder.readField[Option[Int]]("lastTaskCount")
val channelName = unbuilder.readField[Option[String]]("channelName")
val execId = unbuilder.readField[Option[String]]("execId")
val command = unbuilder.readField[Option[String]]("command")
val skipIfActive = unbuilder.readField[Option[Boolean]]("skipIfActive")
unbuilder.endObject()
sbt.internal.util.ProgressEvent(level, items, lastTaskCount, channelName, execId)
sbt.internal.util.ProgressEvent(level, items, lastTaskCount, channelName, execId, command, skipIfActive)
case None =>
deserializationError("Expected JsObject but found None")
}
@ -29,6 +31,8 @@ implicit lazy val ProgressEventFormat: JsonFormat[sbt.internal.util.ProgressEven
builder.addField("lastTaskCount", obj.lastTaskCount)
builder.addField("channelName", obj.channelName)
builder.addField("execId", obj.execId)
builder.addField("command", obj.command)
builder.addField("skipIfActive", obj.skipIfActive)
builder.endObject()
}
}

View File

@ -29,6 +29,8 @@ type ProgressEvent implements sbt.internal.util.AbstractEntry {
lastTaskCount: Int
channelName: String
execId: String
command: String @since("1.4.0")
skipIfActive: Boolean @since("1.4.0")
}
## used by super shell

View File

@ -100,13 +100,11 @@ class ConsoleLogger private[ConsoleLogger] (
override def trace(t: => Throwable): Unit =
appender.trace(t, getTrace)
override def logAll(events: Seq[LogEvent]) =
out.lockObject.synchronized { events.foreach(log) }
override def logAll(events: Seq[LogEvent]) = events.foreach(log)
}
object ConsoleAppender {
private[sbt] def cursorLeft(n: Int): String = s"\u001B[${n}D"
private[sbt] def cursorRight(n: Int): String = s"\u001B[${n}C"
private[sbt] def cursorUp(n: Int): String = s"\u001B[${n}A"
private[sbt] def cursorDown(n: Int): String = s"\u001B[${n}B"
private[sbt] def scrollUp(n: Int): String = s"\u001B[${n}S"
@ -116,9 +114,27 @@ object ConsoleAppender {
private[sbt] final val ClearScreenAfterCursor = clearScreen(0)
private[sbt] final val CursorLeft1000 = cursorLeft(1000)
private[sbt] final val CursorDown1 = cursorDown(1)
private[sbt] final val ClearPromptLine = CursorLeft1000 + ClearScreenAfterCursor
private[this] val showProgressHolder: AtomicBoolean = new AtomicBoolean(false)
def setShowProgress(b: Boolean): Unit = showProgressHolder.set(b)
def showProgress: Boolean = showProgressHolder.get
private[ConsoleAppender] trait Properties {
def isAnsiSupported: Boolean
def isColorEnabled: Boolean
def out: ConsoleOut
}
object Properties {
def from(terminal: Terminal): Properties = new Properties {
override def isAnsiSupported: Boolean = terminal.isAnsiSupported
override def isColorEnabled: Boolean = terminal.isColorEnabled
override def out = ConsoleOut.terminalOut(terminal)
}
def from(o: ConsoleOut, ansi: Boolean, color: Boolean): Properties = new Properties {
override def isAnsiSupported: Boolean = ansi
override def isColorEnabled: Boolean = color
override def out = o
}
}
/** Hide stack trace altogether. */
val noSuppressedMessage = (_: SuppressedTraceContext) => None
@ -130,7 +146,7 @@ object ConsoleAppender {
* 3. -Dsbt.colour=always/auto/never/true/false
* 4. -Dsbt.log.format=always/auto/never/true/false
*/
val formatEnabledInEnv: Boolean = {
lazy val formatEnabledInEnv: Boolean = {
def useColorDefault: Boolean = {
// This approximates that both stdin and stdio are connected,
// so by default color will be turned off for pipes and redirects.
@ -239,7 +255,38 @@ object ConsoleAppender {
* @return A new `ConsoleAppender` that writes to `out`.
*/
def apply(name: String, out: ConsoleOut, useFormat: Boolean): ConsoleAppender =
apply(name, out, formatEnabledInEnv, useFormat, noSuppressedMessage)
apply(name, out, useFormat || formatEnabledInEnv, useFormat, noSuppressedMessage)
/**
* A new `ConsoleAppender` identified by `name`, and that writes to `out`.
*
* @param name An identifier for the `ConsoleAppender`.
* @param terminal The terminal to which this appender corresponds
* @return A new `ConsoleAppender` that writes to `out`.
*/
def apply(name: String, terminal: Terminal): ConsoleAppender = {
val appender = new ConsoleAppender(name, Properties.from(terminal), noSuppressedMessage)
appender.start()
appender
}
/**
* A new `ConsoleAppender` identified by `name`, and that writes to `out`.
*
* @param name An identifier for the `ConsoleAppender`.
* @param terminal The terminal to which this appender corresponds
* @param suppressedMessage How to handle stack traces.
* @return A new `ConsoleAppender` that writes to `out`.
*/
def apply(
name: String,
terminal: Terminal,
suppressedMessage: SuppressedTraceContext => Option[String]
): ConsoleAppender = {
val appender = new ConsoleAppender(name, Properties.from(terminal), suppressedMessage)
appender.start()
appender
}
/**
* A new `ConsoleAppender` identified by `name`, and that writes to `out`.
@ -296,7 +343,7 @@ object ConsoleAppender {
private[sbt] def generateName(): String = "out-" + generateId.incrementAndGet
private[this] def ansiSupported: Boolean = Terminal.isAnsiSupported
private[this] def ansiSupported: Boolean = Terminal.console.isAnsiSupported
}
// See http://stackoverflow.com/questions/24205093/how-to-create-a-custom-appender-in-log4j2
@ -312,14 +359,23 @@ object ConsoleAppender {
*/
class ConsoleAppender private[ConsoleAppender] (
name: String,
out: ConsoleOut,
ansiCodesSupported: Boolean,
useFormat: Boolean,
properties: Properties,
suppressedMessage: SuppressedTraceContext => Option[String]
) extends AbstractAppender(name, null, LogExchange.dummyLayout, true, Array.empty) {
def this(
name: String,
out: ConsoleOut,
ansiCodesSupported: Boolean,
useFormat: Boolean,
suppressedMessage: SuppressedTraceContext => Option[String]
) = this(name, Properties.from(out, ansiCodesSupported, useFormat), suppressedMessage)
import scala.Console.{ BLUE, GREEN, RED, YELLOW }
private val reset: String = {
private[util] def out: ConsoleOut = properties.out
private[util] def ansiCodesSupported: Boolean = properties.isAnsiSupported
private[util] def useFormat: Boolean = properties.isColorEnabled
private def reset: String = {
if (ansiCodesSupported && useFormat) scala.Console.RESET
else ""
}
@ -352,16 +408,15 @@ class ConsoleAppender private[ConsoleAppender] (
* @param t The `Throwable` whose stack trace to log.
* @param traceLevel How to shorten the stack trace.
*/
def trace(t: => Throwable, traceLevel: Int): Unit =
out.lockObject.synchronized {
if (traceLevel >= 0)
write(StackTrace.trimmed(t, traceLevel))
if (traceLevel <= 2) {
val ctx = new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat)
for (msg <- suppressedMessage(ctx))
appendLog(NO_COLOR, "trace", NO_COLOR, msg)
}
def trace(t: => Throwable, traceLevel: Int): Unit = {
if (traceLevel >= 0)
write(StackTrace.trimmed(t, traceLevel))
if (traceLevel <= 2) {
val ctx = new SuppressedTraceContext(traceLevel, ansiCodesSupported && useFormat)
for (msg <- suppressedMessage(ctx))
appendLog(NO_COLOR, "trace", NO_COLOR, msg)
}
}
/**
* Logs a `ControlEvent` to the log.
@ -382,18 +437,6 @@ class ConsoleAppender private[ConsoleAppender] (
appendLog(labelColor(level), level.toString, NO_COLOR, message)
}
/**
* Formats `msg` with `format, wrapped between `RESET`s
*
* @param format The format to use
* @param msg The message to format
* @return The formatted message.
*/
private def formatted(format: String, msg: String): String = {
val builder = new java.lang.StringBuilder(reset.length * 2 + format.length + msg.length)
builder.append(reset).append(format).append(msg).append(reset).toString
}
/**
* Select the right color for the label given `level`.
*
@ -424,22 +467,24 @@ class ConsoleAppender private[ConsoleAppender] (
messageColor: String,
message: String
): Unit =
out.lockObject.synchronized {
val builder: StringBuilder =
new StringBuilder(labelColor.length + label.length + messageColor.length + reset.length * 3)
try {
val len =
labelColor.length + label.length + messageColor.length + reset.length * 3 + ClearScreenAfterCursor.length
val builder: StringBuilder = new StringBuilder(len)
message.linesIterator.foreach { line =>
builder.ensureCapacity(
labelColor.length + label.length + messageColor.length + line.length + reset.length * 3 + 3
)
builder.ensureCapacity(len + line.length + 4)
builder.setLength(0)
def fmted(a: String, b: String) = builder.append(reset).append(a).append(b).append(reset)
builder.append(reset).append('[')
fmted(labelColor, label)
builder.append("] ")
fmted(messageColor, line)
builder.append(ClearScreenAfterCursor)
write(builder.toString)
}
}
} catch { case _: InterruptedException => }
// success is called by ConsoleLogger.
private[sbt] def success(message: => String): Unit = {
@ -483,12 +528,8 @@ class ConsoleAppender private[ConsoleAppender] (
def appendEvent(oe: ObjectEvent[_]): Unit = {
val contentType = oe.contentType
contentType match {
case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent])
case "sbt.internal.util.TraceEvent" => appendTraceEvent(oe.message.asInstanceOf[TraceEvent])
case "sbt.internal.util.ProgressEvent" =>
oe.message match {
case pe: ProgressEvent => ProgressState.updateProgressState(pe)
case _ =>
}
case _ =>
LogExchange.stringCodec[AnyRef](contentType) match {
case Some(codec) if contentType == "sbt.internal.util.SuccessEvent" =>
@ -523,45 +564,70 @@ private[sbt] final class ProgressState(
new AtomicReference(Nil),
new AtomicInteger(0),
blankZone,
new AtomicReference(new ArrayBuffer[Byte])
new AtomicReference(new ArrayBuffer[Byte]),
)
def reset(): Unit = {
progressLines.set(Nil)
padding.set(0)
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[util] def clearBytes(): Unit = {
val pad = padding.get
if (currentLineBytes.get.isEmpty && pad > 0) padding.decrementAndGet()
currentLineBytes.set(new ArrayBuffer[Byte])
}
private[util] def addBytes(terminal: Terminal, bytes: ArrayBuffer[Byte]): Unit = {
val previous = currentLineBytes.get
val padding = this.padding.get
val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0
previous ++= bytes
if (padding > 0) {
val newLineCount = terminal.lineCount(new String(previous.toArray))
val diff = newLineCount - prevLineCount
this.padding.set(math.max(padding - diff, 0))
}
}
private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit =
if (terminal.prompt != Prompt.Running && terminal.prompt != Prompt.Batch) {
val prefix = if (terminal.isAnsiSupported) s"$DeleteLine$CursorLeft1000" else ""
val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes
pmpt.foreach(b => printStream.write(b & 0xFF))
}
private[util] def reprint(terminal: Terminal, printStream: PrintStream): Unit = {
printPrompt(terminal, printStream)
if (progressLines.get.nonEmpty) {
val lines = printProgress(terminal, terminal.getLastLine.getOrElse(""))
printStream.print(ClearScreenAfterCursor + lines)
}
}
private[util] def printProgress(
terminal: Terminal,
lastLine: String
): String = {
val previousLines = progressLines.get
if (previousLines.nonEmpty) {
val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val (height, width) = terminal.getLineHeightAndWidth(lastLine)
val left = cursorLeft(1000) // resets the position to the left
val offset = width > 0
val pad = math.max(padding.get - height, 0)
val start = (if (offset) "\n" else "")
val totalSize = currentLength + blankZone + pad
val blank = left + s"\n$DeleteLine" * (totalSize - currentLength)
val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine")
val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0))
val resetCursor = resetCursorUp + left + lastLine
start + blank + lines + resetCursor
} else {
ClearScreenAfterCursor
}
}
}
private[sbt] object ProgressState {
private val progressState: AtomicReference[ProgressState] = new AtomicReference(null)
private[util] def clearBytes(): Unit = progressState.get match {
case null =>
case state =>
val pad = state.padding.get
if (state.currentLineBytes.get.isEmpty && pad > 0) state.padding.decrementAndGet()
state.currentLineBytes.set(new ArrayBuffer[Byte])
}
private[util] def addBytes(bytes: ArrayBuffer[Byte]): Unit = progressState.get match {
case null =>
case state =>
val previous = state.currentLineBytes.get
val padding = state.padding.get
val prevLineCount = if (padding > 0) Terminal.lineCount(new String(previous.toArray)) else 0
previous ++= bytes
if (padding > 0) {
val newLineCount = Terminal.lineCount(new String(previous.toArray))
val diff = newLineCount - prevLineCount
state.padding.set(math.max(padding - diff, 0))
}
}
private[util] def reprint(printStream: PrintStream): Unit = progressState.get match {
case null => printStream.write('\n')
case state =>
if (state.progressLines.get.nonEmpty) {
val lines = printProgress(0, 0)
printStream.print(ClearScreenAfterCursor + "\n" + lines)
} else printStream.write('\n')
}
/**
* Receives a new task report and replaces the old one. In the event that the new
@ -570,51 +636,52 @@ private[sbt] object ProgressState {
* at the info or greater level, we can decrement the padding because the console
* line will have filled in the blank line.
*/
private[util] def updateProgressState(pe: ProgressEvent): Unit = Terminal.withPrintStream { ps =>
progressState.get match {
case null =>
case state =>
val info = pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L
s" | => ${item.name} ${elapsed}s"
private[sbt] def updateProgressState(
pe: ProgressEvent,
terminal: Terminal
): Unit = {
val state = terminal.progressState
val isRunning = terminal.prompt == Prompt.Running
val isBatch = terminal.prompt == Prompt.Batch
val isWatch = terminal.prompt == Prompt.Watch
if (terminal.isSupershellEnabled) {
if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) {
terminal.withPrintStream { ps =>
val info = if (isRunning || isBatch && pe.channelName.fold(true)(_ == terminal.name)) {
pe.items.map { item =>
val elapsed = item.elapsedMicros / 1000000L
s" | => ${item.name} ${elapsed}s"
}
} else {
pe.command.toSeq.flatMap { cmd =>
val tail = if (isWatch) Nil else "enter 'cancel' to stop evaluation" :: Nil
s"sbt server is running '$cmd'" :: tail
}
}
val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_))
val previousLines = state.progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_))
val lastLine = terminal.prompt match {
case Prompt.Running | Prompt.Batch => terminal.getLastLine.getOrElse("")
case a => a.render()
}
val prevSize = prevLength + state.padding.get
val newPadding = math.max(0, prevSize - currentLength)
state.padding.set(newPadding)
state.printPrompt(terminal, ps)
ps.print(state.printProgress(terminal, lastLine))
ps.flush()
}
val currentLength = info.foldLeft(0)(_ + Terminal.lineCount(_))
val previousLines = state.progressLines.getAndSet(info)
val prevLength = previousLines.foldLeft(0)(_ + Terminal.lineCount(_))
val (height, width) = Terminal.getLineHeightAndWidth
val prevSize = prevLength + state.padding.get
val newPadding = math.max(0, prevSize - currentLength)
state.padding.set(newPadding)
ps.print(printProgress(height, width))
ps.flush()
} else if (state.progressLines.get.nonEmpty) {
state.progressLines.set(Nil)
terminal.withPrintStream { ps =>
val lastLine = terminal.getLastLine.getOrElse("")
ps.print(lastLine + ClearScreenAfterCursor)
ps.flush()
}
}
}
}
private[sbt] def set(state: ProgressState): Unit = progressState.set(state)
private[util] def printProgress(height: Int, width: Int): String = progressState.get match {
case null => ""
case state =>
val previousLines = state.progressLines.get
if (previousLines.nonEmpty) {
val currentLength = previousLines.foldLeft(0)(_ + Terminal.lineCount(_))
val left = cursorLeft(1000) // resets the position to the left
val offset = width > 0
val pad = math.max(state.padding.get - height, 0)
val start = ClearScreenAfterCursor + (if (offset) "\n" else "")
val totalSize = currentLength + state.blankZone + pad
val blank = left + s"\n$DeleteLine" * (totalSize - currentLength)
val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine")
val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0))
val resetCursorRight = left + (if (offset) cursorRight(width) else "")
val resetCursor = resetCursorUp + resetCursorRight
start + blank + lines + resetCursor
} else {
ClearScreenAfterCursor
}
}
}

View File

@ -8,6 +8,8 @@
package sbt.internal.util
import java.io.{ BufferedWriter, PrintStream, PrintWriter }
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference
sealed trait ConsoleOut {
val lockObject: AnyRef
@ -18,7 +20,20 @@ sealed trait ConsoleOut {
}
object ConsoleOut {
def systemOut: ConsoleOut = printStreamOut(System.out)
def systemOut: ConsoleOut = terminalOut
private[sbt] def globalProxy: ConsoleOut = Proxy
private[sbt] def setGlobalProxy(out: ConsoleOut): Unit = Proxy.set(out)
private[sbt] def getGlobalProxy: ConsoleOut = Proxy.proxy.get
private object Proxy extends ConsoleOut {
private[ConsoleOut] val proxy = new AtomicReference[ConsoleOut](systemOut)
private[this] def get: ConsoleOut = proxy.get
def set(proxy: ConsoleOut): Unit = this.proxy.set(proxy)
override val lockObject: AnyRef = proxy
override def print(s: String): Unit = get.print(s)
override def println(s: String): Unit = get.println(s)
override def println(): Unit = get.println()
override def flush(): Unit = get.flush()
}
def overwriteContaining(s: String): (String, String) => Boolean =
(cur, prev) => cur.contains(s) && prev.contains(s)
@ -57,6 +72,28 @@ object ConsoleOut {
}
}
def terminalOut: ConsoleOut = new ConsoleOut {
override val lockObject: AnyRef = System.out
override def print(s: String): Unit = Terminal.get.printStream.print(s)
override def println(s: String): Unit = Terminal.get.printStream.println(s)
override def println(): Unit = Terminal.get.printStream.println()
override def flush(): Unit = Terminal.get.printStream.flush()
}
private[this] val consoleOutPerTerminal = new ConcurrentHashMap[Terminal, ConsoleOut]
def terminalOut(terminal: Terminal): ConsoleOut = consoleOutPerTerminal.get(terminal) match {
case null =>
val res = new ConsoleOut {
override val lockObject: AnyRef = terminal
override def print(s: String): Unit = terminal.printStream.print(s)
override def println(s: String): Unit = terminal.printStream.println(s)
override def println(): Unit = terminal.printStream.println()
override def flush(): Unit = terminal.printStream.flush()
}
consoleOutPerTerminal.put(terminal, res)
res
case c => c
}
def printStreamOut(out: PrintStream): ConsoleOut = new ConsoleOut {
val lockObject = out
def print(s: String) = out.print(s)

View File

@ -7,6 +7,9 @@
package sbt.internal.util
import scala.collection.mutable.ArrayBuffer
import scala.util.Try
object EscHelpers {
/** Escape character, used to introduce an escape sequence. */
@ -84,6 +87,110 @@ object EscHelpers {
nextESC(s, next, sb)
}
}
private[this] val esc = 1
private[this] val csi = 2
def cursorPosition(s: String): Int = {
val bytes = s.getBytes
var i = 0
var index = 0
var state = 0
val digit = new ArrayBuffer[Byte]
var leftDigit = -1
while (i < bytes.length) {
bytes(i) match {
case 27 => state = esc
case b if (state == esc || state == csi) && b >= 48 && b < 58 =>
state = csi
digit += b
case '[' if state == esc => state = csi
case 8 =>
state = 0
index = index - 1
case b if state == csi =>
leftDigit = Try(new String(digit.toArray).toInt).getOrElse(0)
state = 0
b.toChar match {
case 'D' => index = math.max(index - leftDigit, 0)
case 'C' => index += leftDigit
case 'K' =>
case 'J' => if (leftDigit == 2) index = 0
case 'm' =>
case ';' => state = csi
case _ =>
}
digit.clear()
case _ =>
index += 1
}
i += 1
}
index
}
def stripMoves(s: String): String = {
val bytes = s.getBytes
val res = Array.fill[Byte](bytes.length)(0)
var index = 0
var lastEscapeIndex = -1
var state = 0
def set(b: Byte) = {
res(index) = b
index += 1
}
bytes.foreach { b =>
set(b)
b match {
case 27 =>
state = esc
lastEscapeIndex = math.max(0, index)
case b if b == '[' && state == esc => state = csi
case 'm' => state = 0
case b if state == csi && (b < 48 || b >= 58) && b != ';' =>
state = 0
index = math.max(0, lastEscapeIndex - 1)
case b =>
}
}
new String(res, 0, index)
}
def stripColorsAndMoves(s: String): String = {
val bytes = s.getBytes
val res = Array.fill[Byte](bytes.length)(0)
var i = 0
var index = 0
var state = 0
var limit = 0
val digit = new ArrayBuffer[Byte]
var leftDigit = -1
bytes.foreach {
case 27 => state = esc
case b if (state == esc || state == csi) && b >= 48 && b < 58 =>
state = csi
digit += b
case '[' if state == esc => state = csi
case 8 =>
state = 0
index = math.max(index - 1, 0)
case b if state == csi =>
leftDigit = Try(new String(digit.toArray).toInt).getOrElse(0)
state = 0
b.toChar match {
case 'D' => index = math.max(index - leftDigit, 0)
case 'C' => index = math.min(limit, math.min(index + leftDigit, res.length - 1))
case 'K' | 'J' =>
if (leftDigit > 0) (0 until index).foreach(res(_) = 32)
else res(index) = 32
case 'm' =>
case ';' => state = csi
case _ =>
}
digit.clear()
case b =>
res(index) = b
index += 1
limit = math.max(limit, index)
}
new String(res, 0, limit)
}
/** Skips the escape sequence starting at `i-1`. `i` should be positioned at the character after the ESC that starts the sequence. */
private[this] def skipESC(s: String, i: Int): Int = {

View File

@ -75,8 +75,13 @@ object MainAppender {
def defaultScreen(
console: ConsoleOut,
suppressedMessage: SuppressedTraceContext => Option[String]
): Appender =
ConsoleAppender(ConsoleAppender.generateName, console, suppressedMessage = suppressedMessage)
): Appender = {
ConsoleAppender(
ConsoleAppender.generateName,
Terminal.get,
suppressedMessage = suppressedMessage
)
}
def defaultScreen(
name: String,

View File

@ -21,8 +21,11 @@ class ManagedLogger(
val name: String,
val channelName: Option[String],
val execId: Option[String],
xlogger: XLogger
xlogger: XLogger,
terminal: Option[Terminal]
) extends Logger {
def this(name: String, channelName: Option[String], execId: Option[String], xlogger: XLogger) =
this(name, channelName, execId, xlogger, None)
override def trace(t: => Throwable): Unit =
logEvent(Level.Error, TraceEvent("Error", t, channelName, execId))
override def log(level: Level.Value, message: => String): Unit = {
@ -35,10 +38,12 @@ class ManagedLogger(
private lazy val SuccessEventTag = scala.reflect.runtime.universe.typeTag[SuccessEvent]
// send special event for success since it's not a real log level
override def success(message: => String): Unit = {
infoEvent[SuccessEvent](SuccessEvent(message))(
implicitly[JsonFormat[SuccessEvent]],
SuccessEventTag
)
if (terminal.fold(true)(_.isSuccessEnabled)) {
infoEvent[SuccessEvent](SuccessEvent(message))(
implicitly[JsonFormat[SuccessEvent]],
SuccessEventTag
)
}
}
def registerStringCodec[A: ShowLines: TypeTag]: Unit = {

View File

@ -0,0 +1,47 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import java.io.OutputStream
import java.util.concurrent.LinkedBlockingQueue
import scala.collection.JavaConverters._
private[sbt] sealed trait Prompt {
def mkPrompt: () => String
def render(): String
def wrappedOutputStream(terminal: Terminal): OutputStream
}
private[sbt] object Prompt {
private[sbt] case class AskUser(override val mkPrompt: () => String) extends Prompt {
private[this] val bytes = new LinkedBlockingQueue[Int]
override def wrappedOutputStream(terminal: Terminal): OutputStream = new OutputStream {
override def write(b: Int): Unit = {
if (b == 10) bytes.clear()
else bytes.put(b)
terminal.withPrintStream { p =>
p.write(b)
p.flush()
}
}
override def flush(): Unit = terminal.withPrintStream(_.flush())
}
override def render(): String =
EscHelpers.stripMoves(new String(bytes.asScala.toArray.map(_.toByte)))
}
private[sbt] trait NoPrompt extends Prompt {
override val mkPrompt: () => String = () => ""
override def render(): String = ""
override def wrappedOutputStream(terminal: Terminal): OutputStream = terminal.outputStream
}
private[sbt] case object Running extends NoPrompt
private[sbt] case object Batch extends NoPrompt
private[sbt] case object Watch extends NoPrompt
}

View File

@ -10,17 +10,18 @@ package sbt.internal.util
import java.io.{ InputStream, OutputStream, PrintStream }
import java.nio.channels.ClosedChannelException
import java.util.Locale
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.{ ConcurrentHashMap, Executors, LinkedBlockingQueue, TimeUnit }
import jline.DefaultTerminal2
import jline.console.ConsoleReader
import sbt.internal.util.ConsoleAppender.{ ClearScreenAfterCursor, CursorLeft1000 }
import scala.annotation.tailrec
import scala.collection.mutable.ArrayBuffer
import scala.util.control.NonFatal
import scala.util.Try
object Terminal {
trait Terminal extends AutoCloseable {
/**
* Gets the current width of the terminal. The implementation reads a property from the jline
@ -29,7 +30,7 @@ object Terminal {
*
* @return the terminal width.
*/
def getWidth: Int = terminal.getWidth
def getWidth: Int
/**
* Gets the current height of the terminal. The implementation reads a property from the jline
@ -38,7 +39,7 @@ object Terminal {
*
* @return the terminal height.
*/
def getHeight: Int = terminal.getHeight
def getHeight: Int
/**
* Returns the height and width of the current line that is displayed on the terminal. If the
@ -46,14 +47,86 @@ object Terminal {
*
* @return the (height, width) pair
*/
def getLineHeightAndWidth: (Int, Int) = currentLine.get.toArray match {
case bytes if bytes.isEmpty => (0, 0)
case bytes =>
val width = getWidth
val line = EscHelpers.removeEscapeSequences(new String(bytes))
val count = lineCount(line)
(count, line.length - ((count - 1) * width))
}
def getLineHeightAndWidth(line: String): (Int, Int)
/**
* Gets the input stream for this Terminal. This could be a wrapper around System.in for the
* process or it could be a remote input stream for a network channel.
* @return the input stream.
*/
def inputStream: InputStream
/**
* Gets the input stream for this Terminal. This could be a wrapper around System.in for the
* process or it could be a remote input stream for a network channel.
* @return the input stream.
*/
def outputStream: OutputStream
/**
* Returns true if the terminal supports ansi characters.
*
* @return true if the terminal supports ansi escape codes.
*/
def isAnsiSupported: Boolean
/**
* Returns true if color is enabled for this terminal.
*
* @return true if color is enabled for this terminal.
*/
def isColorEnabled: Boolean
/**
* Returns true if the terminal has echo enabled.
*
* @return true if the terminal has echo enabled.
*/
def isEchoEnabled: Boolean
/**
* Returns true if the terminal has success enabled, which it may not if it is for batch
* commands because the client will print the success results when received from the
* server.
*
* @return true if the terminal has success enabled
*/
def isSuccessEnabled: Boolean
/**
* Returns true if the terminal has supershell enabled.
*
* @return true if the terminal has supershell enabled.
*/
def isSupershellEnabled: Boolean
/*
* The methods below this comment are implementation details that are in
* some cases specific to jline2. These methods may need to change or be
* removed if/when sbt upgrades to jline 3.
*/
/**
* Returns the last line written to the terminal's output stream.
* @return the last line
*/
private[sbt] def getLastLine: Option[String]
private[sbt] def getBooleanCapability(capability: String): Boolean
private[sbt] def getNumericCapability(capability: String): Int
private[sbt] def getStringCapability(capability: String): String
private[sbt] def name: String
private[sbt] def withRawSystemIn[T](f: => T): T = f
private[sbt] def withCanonicalIn[T](f: => T): T = f
private[sbt] def write(bytes: Int*): Unit
private[sbt] def printStream: PrintStream
private[sbt] def withPrintStream[T](f: PrintStream => T): T
private[sbt] def restore(): Unit = {}
private[sbt] val progressState = new ProgressState(1)
private[this] val promptHolder: AtomicReference[Prompt] = new AtomicReference(Prompt.Running)
private[sbt] final def prompt: Prompt = promptHolder.get
private[sbt] final def setPrompt(newPrompt: Prompt): Unit = promptHolder.set(newPrompt)
/**
* Returns the number of lines that the input string will cover given the current width of the
@ -62,9 +135,9 @@ object Terminal {
* @param line the input line
* @return the number of lines that the line will cover on the terminal
*/
def lineCount(line: String): Int = {
private[sbt] def lineCount(line: String): Int = {
val lines = EscHelpers.stripColorsAndMoves(line).split('\n')
val width = getWidth
val lines = EscHelpers.removeEscapeSequences(line).split('\n')
def count(l: String): Int = {
val len = l.length
if (width > 0 && len > 0) (len - 1 + width) / width else 0
@ -72,14 +145,72 @@ object Terminal {
lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_))
}
/**
* Returns true if the current terminal supports ansi characters.
*
* @return true if the current terminal supports ansi escape codes.
}
object Terminal {
// Disable noisy jline log spam
if (System.getProperty("sbt.jline.verbose", "false") != "true")
jline.internal.Log.setOutput(new PrintStream(_ => {}, false))
def consoleLog(string: String): Unit = {
Terminal.console.printStream.println(s"[info] $string")
}
private[sbt] def set(terminal: Terminal) = {
activeTerminal.set(terminal)
jline.TerminalFactory.set(terminal.toJLine)
}
implicit class TerminalOps(private val term: Terminal) extends AnyVal {
def ansi(richString: => String, string: => String): String =
if (term.isAnsiSupported) richString else string
/*
* Whenever we are dealing with JLine, which is true in sbt's ConsoleReader
* as well as in the scala `console` task, we need to provide a jline.Terminal2
* instance that can be consumed by the ConsoleReader. The ConsoleTerminal
* already wraps a jline terminal, so we can just return the wrapped jline
* terminal.
*/
private[sbt] def toJLine: jline.Terminal with jline.Terminal2 = term match {
case t: ConsoleTerminal => t.term
case _ =>
new jline.Terminal with jline.Terminal2 {
override def init(): Unit = {}
override def restore(): Unit = {}
override def reset(): Unit = {}
override def isSupported: Boolean = true
override def getWidth: Int = term.getWidth
override def getHeight: Int = term.getHeight
override def isAnsiSupported: Boolean = term.isAnsiSupported
override def wrapOutIfNeeded(out: OutputStream): OutputStream = out
override def wrapInIfNeeded(in: InputStream): InputStream = in
override def hasWeirdWrap: Boolean = false
override def isEchoEnabled: Boolean = term.isEchoEnabled
override def setEchoEnabled(enabled: Boolean): Unit = {}
override def disableInterruptCharacter(): Unit = {}
override def enableInterruptCharacter(): Unit = {}
override def getOutputEncoding: String = null
override def getBooleanCapability(capability: String): Boolean = {
term.getBooleanCapability(capability)
}
override def getNumericCapability(capability: String): Integer = {
term.getNumericCapability(capability)
}
override def getStringCapability(capability: String): String = {
term.getStringCapability(capability)
}
}
}
}
/*
* Closes the standard input and output streams for the process. This allows
* the sbt client to detach from the server it launches.
*/
def isAnsiSupported: Boolean =
try terminal.isAnsiSupported
catch { case NonFatal(_) => !isWindows }
def close(): Unit = {
if (System.console == null) {
originalOut.close()
originalIn.close()
System.err.close()
}
}
/**
* Returns true if System.in is attached. When sbt is run as a subprocess, like in scripted or
@ -90,15 +221,21 @@ object Terminal {
*/
def systemInIsAttached: Boolean = attached.get
def read: Int = inputStream.get match {
case null => -1
case is => is.read
}
/**
* Returns an InputStream that will throw a [[ClosedChannelException]] if read returns -1.
* @return the wrapped InputStream.
*/
private[sbt] def throwOnClosedSystemIn: InputStream = new InputStream {
override def available(): Int = WrappedSystemIn.available()
override def read(): Int = WrappedSystemIn.read() match {
case -1 => throw new ClosedChannelException
case r => r
private[sbt] def throwOnClosedSystemIn(in: InputStream): InputStream = new InputStream {
override def available(): Int = in.available()
override def read(): Int = in.read() match {
case -1 => throw new ClosedChannelException
case r if r >= 0 => r
case _ => -1
}
}
@ -114,27 +251,7 @@ object Terminal {
/**
* Restore the terminal to its initial state.
*/
private[sbt] def restore(): Unit = terminal.restore()
/**
* Runs a thunk ensuring that the terminal has echo enabled. Most of the time sbt should have
* echo mode on except when it is explicitly set to raw mode via [[withRawSystemIn]].
*
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withEcho[T](toggle: Boolean)(f: => T): T = {
val previous = terminal.isEchoEnabled
terminalLock.lockInterruptibly()
try {
terminal.setEchoEnabled(toggle)
f
} finally {
terminal.setEchoEnabled(previous)
terminalLock.unlock()
}
}
private[sbt] def restore(): Unit = console.toJLine.restore()
/**
*
@ -144,170 +261,373 @@ object Terminal {
*/
private[sbt] def withStreams[T](f: => T): T =
if (System.getProperty("sbt.io.virtual", "true") == "true") {
withOut(withIn(f))
try withOut(withIn(f))
finally {
jline.TerminalFactory.reset()
console.close()
}
} else f
/**
* Runs a thunk ensuring that the terminal is in canonical mode:
* [[https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html Canonical or Not]].
* Most of the time sbt should be in canonical mode except when it is explicitly set to raw mode
* via [[withRawSystemIn]].
*
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withCanonicalIn[T](f: => T): T = withTerminal { t =>
t.restore()
f
private[this] object ProxyTerminal extends Terminal {
private def t: Terminal = activeTerminal.get
override def getWidth: Int = t.getWidth
override def getHeight: Int = t.getHeight
override def getLineHeightAndWidth(line: String): (Int, Int) = t.getLineHeightAndWidth(line)
override def lineCount(line: String): Int = t.lineCount(line)
override def inputStream: InputStream = t.inputStream
override def outputStream: OutputStream = t.outputStream
override def isAnsiSupported: Boolean = t.isAnsiSupported
override def isColorEnabled: Boolean = t.isColorEnabled
override def isEchoEnabled: Boolean = t.isEchoEnabled
override def isSuccessEnabled: Boolean = t.isSuccessEnabled
override def isSupershellEnabled: Boolean = t.isSupershellEnabled
override def getBooleanCapability(capability: String): Boolean =
t.getBooleanCapability(capability)
override def getNumericCapability(capability: String): Int = t.getNumericCapability(capability)
override def getStringCapability(capability: String): String = t.getStringCapability(capability)
override def withRawSystemIn[T](f: => T): T = t.withRawSystemIn(f)
override def withCanonicalIn[T](f: => T): T = t.withCanonicalIn(f)
override def printStream: PrintStream = t.printStream
override def withPrintStream[T](f: PrintStream => T): T = t.withPrintStream(f)
override def restore(): Unit = t.restore()
override def close(): Unit = {}
override private[sbt] def write(bytes: Int*): Unit = t.write(bytes: _*)
override def getLastLine: Option[String] = t.getLastLine
override private[sbt] def name: String = t.name
}
private[sbt] def get: Terminal = ProxyTerminal
private[sbt] def withIn[T](in: InputStream)(f: => T): T = {
val original = inputStream.get
try {
inputStream.set(in)
System.setIn(in)
scala.Console.withIn(in)(f)
} finally {
inputStream.set(original)
System.setIn(original)
}
}
/**
* Runs a thunk ensuring that the terminal is in in non-canonical mode:
* [[https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html Canonical or Not]].
* This should be used when sbt is reading user input, e.g. in `shell` or a continuous build.
* @param f the thunk to run
* @tparam T the result type of the thunk
* @return the result of the thunk
*/
private[sbt] def withRawSystemIn[T](f: => T): T = withTerminal { t =>
t.init()
f
}
private[this] def withTerminal[T](f: jline.Terminal => T): T = {
val t = terminal
terminalLock.lockInterruptibly()
try f(t)
finally {
t.restore()
terminalLock.unlock()
private[sbt] def withOut[T](out: PrintStream)(f: => T): T = {
val originalOut = System.out
val originalProxyOut = ConsoleOut.getGlobalProxy
try {
ConsoleOut.setGlobalProxy(ConsoleOut.printStreamOut(out))
System.setOut(out)
scala.Console.withOut(out)(f)
} finally {
ConsoleOut.setGlobalProxy(originalProxyOut)
System.setOut(originalOut)
}
}
private[this] val originalOut = System.out
private[this] val originalIn = System.in
private[this] val currentLine = new AtomicReference(new ArrayBuffer[Byte])
private[this] val lineBuffer = new LinkedBlockingQueue[Byte]
private[this] val flushQueue = new LinkedBlockingQueue[Unit]
private[this] val writeLock = new AnyRef
private[this] final class WriteThread extends Thread("sbt-stdout-write-thread") {
setDaemon(true)
start()
private[this] val isStopped = new AtomicBoolean(false)
def close(): Unit = {
isStopped.set(true)
flushQueue.put(())
private[sbt] class WriteableInputStream(in: InputStream, name: String)
extends InputStream
with AutoCloseable {
final def write(bytes: Int*): Unit = bytes.foreach(i => buffer.put(i))
private[this] val executor =
Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader"))
private[this] val buffer = new LinkedBlockingQueue[Integer]
private[this] val closed = new AtomicBoolean(false)
private[this] val resultQueue = new LinkedBlockingQueue[LinkedBlockingQueue[Int]]
private[this] val waiting = ConcurrentHashMap.newKeySet[LinkedBlockingQueue[Int]]
/*
* Starts a loop that waits for consumers of the InputStream to call read.
* When read is called, we enqueue a `LinkedBlockingQueue[Int]` to which
* the runnable can return a byte from stdin. If the read caller is interrupted,
* they remove the result from the waiting set and any byte read will be
* enqueued in the buffer. It is done this way so that we only read from
* System.in when a caller actually asks for bytes. If we constantly poll
* from System.in, then when the user calls reboot from the console, the
* first character they type after reboot is swallowed by the previous
* sbt main program. If the user calls reboot from a remote client, we
* can't avoid losing the first byte inputted in the console. A more
* robust fix would be to override System.in at the launcher level instead
* of at the sbt level. At the moment, the use case of a user calling
* reboot from a network client and the adding input at the server console
* seems pathological enough that it isn't worth putting more effort into
* fixing.
*
*/
private[this] val runnable: Runnable = () => {
@tailrec def impl(): Unit = {
val result = resultQueue.take
val b = in.read
// The downstream consumer may have been interrupted. Buffer the result
// when that hapens.
if (waiting.contains(result)) result.put(b) else buffer.put(b)
if (b != -1 && !Thread.interrupted()) impl()
else closed.set(true)
}
try impl()
catch { case _: InterruptedException => closed.set(true) }
}
executor.submit(runnable)
override def read(): Int =
if (closed.get) -1
else
synchronized {
buffer.poll match {
case null =>
val result = new LinkedBlockingQueue[Int]
waiting.add(result)
resultQueue.offer(result)
try result.take
catch {
case e: InterruptedException =>
waiting.remove(result)
throw e
}
case b if b == -1 => throw new ClosedChannelException
case b => b
}
}
override def available(): Int = {
buffer.size
}
override def close(): Unit = if (closed.compareAndSet(false, true)) {
executor.shutdownNow()
()
}
@tailrec override def run(): Unit = {
try {
flushQueue.take()
val bytes = new java.util.ArrayList[Byte]
writeLock.synchronized {
lineBuffer.drainTo(bytes)
import scala.collection.JavaConverters._
val remaining = bytes.asScala.foldLeft(new ArrayBuffer[Byte]) { (buf, i) =>
if (i == 10) {
ProgressState.addBytes(buf)
ProgressState.clearBytes()
buf.foreach(b => originalOut.write(b & 0xFF))
ProgressState.reprint(originalOut)
currentLine.set(new ArrayBuffer[Byte])
new ArrayBuffer[Byte]
} else buf += i
}
if (remaining.nonEmpty) {
currentLine.get ++= remaining
originalOut.write(remaining.toArray)
}
originalOut.flush()
}
} catch { case _: InterruptedException => isStopped.set(true) }
if (!isStopped.get) run()
}
}
private[this] val nonBlockingIn: WriteableInputStream =
new WriteableInputStream(jline.TerminalFactory.get.wrapInIfNeeded(originalIn), "console")
private[this] val inputStream = new AtomicReference[InputStream](System.in)
private[this] def withOut[T](f: => T): T = {
val thread = new WriteThread
try {
System.setOut(SystemPrintStream)
scala.Console.withOut(SystemPrintStream)(f)
System.setOut(proxyPrintStream)
scala.Console.withOut(proxyOutputStream)(f)
} finally {
thread.close()
System.setOut(originalOut)
}
}
private[this] def withIn[T](f: => T): T =
try {
System.setIn(Terminal.wrappedSystemIn)
scala.Console.withIn(Terminal.wrappedSystemIn)(f)
inputStream.set(Terminal.wrappedSystemIn)
System.setIn(wrappedSystemIn)
scala.Console.withIn(proxyInputStream)(f)
} finally System.setIn(originalIn)
private[sbt] def withPrintStream[T](f: PrintStream => T): T = writeLock.synchronized {
f(originalOut)
private[sbt] def withPrintStream[T](f: PrintStream => T): T = console.withPrintStream(f)
private[this] val attached = new AtomicBoolean(true)
/**
* A wrapped instance of a jline.Terminal2 instance. It should only ever be changed when the
* backgrounds sbt with ctrl+z and then foregrounds sbt which causes a call to reset. The
* Terminal.console method returns this terminal and the ConsoleChannel delegates its
* terminal method to it.
*/
private[this] val consoleTerminalHolder = new AtomicReference(wrap(jline.TerminalFactory.get))
/**
* The terminal that is currently being used by the proxyInputStream and proxyOutputStream.
* It is set through the Terminal.set method which is called by the SetTerminal command, which
* is used to change the terminal during task evaluation. This allows us to route System.in and
* 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
* it is still loading. It works by updating proxyInputStream to read from the
* value of bootInputStreamHolder if it is non-null as well as from the normal process
* console io (assuming there is console io).
*/
private[this] val bootInputStreamHolder = new AtomicReference[InputStream]
/**
* The boot output stream allows sbt to relay the bytes written to stdout to one or
* more remote clients while the sbt build is loading and hasn't yet loaded a server.
* The output stream of TerminalConsole is updated to write to value of
* bootOutputStreamHolder when it is non-null as well as the normal process console
* output stream.
*/
private[this] val bootOutputStreamHolder = new AtomicReference[OutputStream]
private[sbt] def setBootStreams(
bootInputStream: InputStream,
bootOutputStream: OutputStream
): Unit = {
bootInputStreamHolder.set(bootInputStream)
bootOutputStreamHolder.set(bootOutputStream)
}
private object SystemOutputStream extends OutputStream {
override def write(b: Int): Unit = writeLock.synchronized(lineBuffer.put(b.toByte))
override def write(b: Array[Byte]): Unit = writeLock.synchronized(b.foreach(lineBuffer.put))
override def write(b: Array[Byte], off: Int, len: Int): Unit = writeLock.synchronized {
val lo = math.max(0, off)
val hi = math.min(math.max(off + len, 0), b.length)
(lo until hi).foreach(i => lineBuffer.put(b(i)))
private[this] object proxyInputStream extends InputStream {
private[this] val isScripted = System.getProperty("sbt.scripted", "false") == "true"
/*
* This is to handle the case when a remote client starts sbt and the build fails.
* We need to be able to consume input bytes from the remote client, but they
* haven't yet connected to the main server but may be connected to the
* BootServerSocket. Unfortunately there is no poll method on input stream that
* takes a duration so we have to manually implement that here. All of the input
* streams that we create in sbt are interruptible, so we can just poll each
* of the input streams and periodically interrupt the thread to switch between
* the two input streams.
*/
private class ReadThread extends Thread with AutoCloseable {
val result = new LinkedBlockingQueue[Integer]
setDaemon(true)
start()
val running = new AtomicBoolean(true)
override def run(): Unit = while (running.get) {
bootInputStreamHolder.get match {
case null =>
case is =>
def readFrom(inputStream: InputStream) =
try {
if (running.get) {
inputStream.read match {
case -1 =>
case i =>
result.put(i)
running.set(false)
}
}
} catch { case _: InterruptedException => }
readFrom(is)
readFrom(activeTerminal.get().inputStream)
}
}
override def close(): Unit = if (running.compareAndSet(true, false)) this.interrupt()
}
def read(): Int = {
if (isScripted) -1
else if (bootInputStreamHolder.get == null) activeTerminal.get().inputStream.read()
else {
val thread = new ReadThread
@tailrec def poll(): Int = thread.result.poll(10, TimeUnit.MILLISECONDS) match {
case null =>
thread.interrupt()
poll()
case i => i
}
poll()
}
}
def write(s: String): Unit = s.getBytes.foreach(lineBuffer.put)
override def flush(): Unit = writeLock.synchronized(flushQueue.put(()))
}
private object SystemPrintStream extends PrintStream(SystemOutputStream, true)
private[this] object proxyOutputStream extends OutputStream {
private[this] def os: OutputStream = activeTerminal.get().outputStream
def write(byte: Int): Unit = {
os.write(byte)
os.flush()
if (byte == 10) os.flush()
}
override def write(bytes: Array[Byte]): Unit = write(bytes, 0, bytes.length)
override def write(bytes: Array[Byte], offset: Int, len: Int): Unit = {
os.write(bytes, offset, len)
os.flush()
}
override def flush(): Unit = os.flush()
}
private[this] val proxyPrintStream = new PrintStream(proxyOutputStream, true) {
override def toString: String = s"proxyPrintStream($proxyOutputStream)"
}
private[this] lazy val isWindows =
System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
private[this] object WrappedSystemIn extends InputStream {
private[this] val in = terminal.wrapInIfNeeded(System.in)
override def available(): Int = if (attached.get) in.available else 0
private[this] val in = proxyInputStream
override def available(): Int = if (attached.get) in.available() else 0
override def read(): Int = synchronized {
if (attached.get) {
val res = in.read
val res = in.read()
if (res == -1) attached.set(false)
res
} else -1
}
}
private[this] val terminalLock = new ReentrantLock()
private[this] val attached = new AtomicBoolean(true)
private[this] val terminalHolder = new AtomicReference(wrap(jline.TerminalFactory.get))
private[this] lazy val isWindows =
System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).indexOf("windows") >= 0
/*
* When the server is booted by a remote client, it may not be able to accurately
* calculate the terminal properties. To work around this, we can set the
* properties via an environment property. It was too difficult to get system
* properties working correctly with windows.
*/
private class Props(
val width: Int,
val height: Int,
val ansi: Boolean,
val color: Boolean,
val supershell: Boolean
)
private[sbt] val TERMINAL_PROPS = "SBT_TERMINAL_PROPS"
private val props = System.getenv(TERMINAL_PROPS) match {
case null => None
case p =>
p.split(",") match {
case Array(width, height, ansi, color, supershell) =>
Try(
new Props(
width.toInt,
height.toInt,
ansi.toBoolean,
color.toBoolean,
supershell.toBoolean
)
).toOption
case _ => None
}
}
private[sbt] def startedByRemoteClient = props.isDefined
private[this] def wrap(terminal: jline.Terminal): jline.Terminal = {
val term: jline.Terminal = new jline.Terminal {
/**
* Creates an instance of [[Terminal]] that delegates most of its methods to an underlying
* jline.Terminal2 instance. In the long run, sbt should upgrade to jline3, which has a
* completely different terminal interface so whereever possible, we should avoid
* directly referencing jline.Terminal. Wrapping jline Terminal in sbt terminal helps
* with that goal.
*
* @param terminal the jline terminal to wrap
* @return an sbt Terminal
*/
private[this] def wrap(terminal: jline.Terminal): Terminal = {
val term: jline.Terminal with jline.Terminal2 = new jline.Terminal with jline.Terminal2 {
private[this] val hasConsole = System.console != null
private[this] def alive = hasConsole && attached.get
private[this] val term2: jline.Terminal2 = terminal match {
case t: jline.Terminal2 => t
case _ => new DefaultTerminal2(terminal)
}
override def init(): Unit = if (alive) terminal.init()
override def restore(): Unit = if (alive) terminal.restore()
override def reset(): Unit = if (alive) terminal.reset()
override def isSupported: Boolean = terminal.isSupported
override def getWidth: Int = terminal.getWidth
override def getHeight: Int = terminal.getHeight
override def isAnsiSupported: Boolean = terminal.isAnsiSupported
override def getWidth: Int = props.map(_.width).getOrElse(terminal.getWidth)
override def getHeight: Int = props.map(_.height).getOrElse(terminal.getHeight)
override def isAnsiSupported: Boolean = props.map(_.ansi).getOrElse(terminal.isAnsiSupported)
override def wrapOutIfNeeded(out: OutputStream): OutputStream = terminal.wrapOutIfNeeded(out)
override def wrapInIfNeeded(in: InputStream): InputStream = terminal.wrapInIfNeeded(in)
override def hasWeirdWrap: Boolean = terminal.hasWeirdWrap
override def isEchoEnabled: Boolean = terminal.isEchoEnabled
override def setEchoEnabled(enabled: Boolean): Unit = if (alive) {
terminal.setEchoEnabled(enabled)
}
override def setEchoEnabled(enabled: Boolean): Unit =
if (alive) terminal.setEchoEnabled(enabled)
override def disableInterruptCharacter(): Unit =
if (alive) terminal.disableInterruptCharacter()
override def enableInterruptCharacter(): Unit =
if (alive) terminal.enableInterruptCharacter()
override def getOutputEncoding: String = terminal.getOutputEncoding
override def getBooleanCapability(capability: String): Boolean =
term2.getBooleanCapability(capability)
override def getNumericCapability(capability: String): Integer =
term2.getNumericCapability(capability)
override def getStringCapability(capability: String): String = {
term2.getStringCapability(capability)
}
}
term.restore()
term.setEchoEnabled(true)
term
new ConsoleTerminal(term, nonBlockingIn, originalOut)
}
private[util] def reset(): Unit = {
private[sbt] def reset(): Unit = {
jline.TerminalFactory.reset()
terminalHolder.set(wrap(jline.TerminalFactory.get))
console.close()
consoleTerminalHolder.set(wrap(jline.TerminalFactory.get))
}
// translate explicit class names to type in order to support
@ -329,14 +649,200 @@ object Terminal {
}
fixTerminalProperty()
private[sbt] def createReader(in: InputStream): ConsoleReader =
new ConsoleReader(in, System.out, terminal)
private[sbt] def createReader(term: Terminal, prompt: Prompt): ConsoleReader = {
new ConsoleReader(term.inputStream, prompt.wrappedOutputStream(term), term.toJLine) {
override def readLine(prompt: String, mask: Character): String =
term.withRawSystemIn(super.readLine(prompt, mask))
override def readLine(prompt: String): String = term.withRawSystemIn(super.readLine(prompt))
}
}
private[this] def terminal: jline.Terminal = terminalHolder.get match {
private[sbt] def console: Terminal = consoleTerminalHolder.get match {
case null => throw new IllegalStateException("Uninitialized terminal.")
case term => term
}
@deprecated("For compatibility only", "1.4.0")
private[sbt] def deprecatedTeminal: jline.Terminal = terminal
private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine
private class ConsoleTerminal(
val term: jline.Terminal with jline.Terminal2,
in: InputStream,
out: OutputStream
) extends TerminalImpl(in, out, "console0") {
private[this] def isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")
override def getWidth: Int = term.getWidth
override def getHeight: Int = term.getHeight
override def isAnsiSupported: Boolean = term.isAnsiSupported && !isCI
override def isEchoEnabled: Boolean = term.isEchoEnabled
override def isSuccessEnabled: Boolean = true
override def getBooleanCapability(capability: String): Boolean =
term.getBooleanCapability(capability)
override def getNumericCapability(capability: String): Int =
term.getNumericCapability(capability)
override def getStringCapability(capability: String): String =
term.getStringCapability(capability)
override private[sbt] def restore(): Unit = term.restore()
override def withRawSystemIn[T](f: => T): T = term.synchronized {
try {
term.init()
term.setEchoEnabled(false)
f
} finally {
term.restore()
term.setEchoEnabled(true)
}
}
override def isColorEnabled: Boolean =
props.map(_.color).getOrElse(ConsoleAppender.formatEnabledInEnv)
override def isSupershellEnabled: Boolean =
props
.map(_.supershell)
.getOrElse(System.getProperty("sbt.supershell") match {
case null =>
!(sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI")) && isColorEnabled
case "true" => true
case _ => false
})
}
private[sbt] abstract class TerminalImpl private[sbt] (
val in: InputStream,
val out: OutputStream,
override private[sbt] val name: String
) extends Terminal {
private[this] val directWrite = new AtomicBoolean(false)
private[this] val currentLine = new AtomicReference(new ArrayBuffer[Byte])
private[this] val lineBuffer = new LinkedBlockingQueue[Byte]
private[this] val flushQueue = new LinkedBlockingQueue[Seq[Byte]]
private[this] val writeLock = new AnyRef
private[this] val writeableInputStream = in match {
case w: WriteableInputStream => w
case _ => new WriteableInputStream(in, name)
}
def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f
private val combinedOutputStream = new OutputStream {
override def write(b: Int): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.write(b))
out.write(b)
}
override def write(b: Array[Byte]): Unit = write(b, 0, b.length)
override def write(b: Array[Byte], offset: Int, len: Int): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.write(b, offset, len))
out.write(b, offset, len)
}
override def flush(): Unit = {
Option(bootOutputStreamHolder.get).foreach(_.flush())
out.flush()
}
}
override val outputStream = new OutputStream {
override def write(b: Int): Unit = throwIfClosed {
writeLock.synchronized {
if (b == Int.MinValue) currentLine.set(new ArrayBuffer[Byte])
else doWrite(Vector((b & 0xFF).toByte))
if (b == 10) combinedOutputStream.flush()
}
}
override def write(b: Array[Byte]): Unit = throwIfClosed(write(b, 0, b.length))
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
throwIfClosed {
writeLock.synchronized {
val lo = math.max(0, off)
val hi = math.min(math.max(off + len, 0), b.length)
doWrite(b.slice(off, off + len).toSeq)
}
}
}
override def flush(): Unit = combinedOutputStream.flush()
private[this] val clear = s"$CursorLeft1000$ClearScreenAfterCursor"
private def doWrite(bytes: Seq[Byte]): Unit = {
def doWrite(b: Byte): Unit = out.write(b & 0xFF)
val remaining = bytes.foldLeft(new ArrayBuffer[Byte]) { (buf, i) =>
if (i == 10) {
progressState.addBytes(TerminalImpl.this, buf)
progressState.clearBytes()
val cl = currentLine.get
if (buf.nonEmpty && isAnsiSupported && cl.isEmpty) clear.getBytes.foreach(doWrite)
combinedOutputStream.write(buf.toArray)
combinedOutputStream.write(10)
currentLine.get match {
case s if s.nonEmpty => currentLine.set(new ArrayBuffer[Byte])
case _ =>
}
progressState.reprint(TerminalImpl.this, rawPrintStream)
new ArrayBuffer[Byte]
} else buf += i
}
if (remaining.nonEmpty) {
val cl = currentLine.get
if (isAnsiSupported && cl.isEmpty) {
clear.getBytes.foreach(doWrite)
}
cl ++= remaining
combinedOutputStream.write(remaining.toArray)
}
combinedOutputStream.flush()
}
}
override private[sbt] val printStream: PrintStream = new PrintStream(outputStream, true)
override def inputStream: InputStream = writeableInputStream
private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*)
private[this] val isStopped = new AtomicBoolean(false)
override def getLineHeightAndWidth(line: String): (Int, Int) = getWidth match {
case width if width > 0 =>
val position = EscHelpers.cursorPosition(line)
val count = (position + width - 1) / width
(count, position - (math.max((count - 1), 0) * width))
case _ => (0, 0)
}
override def getLastLine: Option[String] = currentLine.get match {
case bytes if bytes.isEmpty => None
case bytes =>
// TODO there are ghost characters when the user deletes prompt characters
// when they are given the cancellation option
Some(new String(bytes.toArray).replaceAllLiterally(ClearScreenAfterCursor, ""))
}
private[this] val rawPrintStream: PrintStream = new PrintStream(combinedOutputStream, true) {
override def close(): Unit = {}
}
override def withPrintStream[T](f: PrintStream => T): T =
writeLock.synchronized(f(rawPrintStream))
override def close(): Unit = if (isStopped.compareAndSet(false, true)) {
writeableInputStream.close()
}
}
private[sbt] val NullTerminal = new Terminal {
override def close(): Unit = {}
override def getBooleanCapability(capability: String): Boolean = false
override def getHeight: Int = 0
override def getLastLine: Option[String] = None
override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0)
override def getNumericCapability(capability: String): Int = -1
override def getStringCapability(capability: String): String = null
override def getWidth: Int = 0
override def inputStream: java.io.InputStream = () => {
try this.synchronized(this.wait)
catch { case _: InterruptedException => }
-1
}
override def isAnsiSupported: Boolean = false
override def isColorEnabled: Boolean = false
override def isEchoEnabled: Boolean = false
override def isSuccessEnabled: Boolean = false
override def isSupershellEnabled: Boolean = false
override def outputStream: java.io.OutputStream = _ => {}
override private[sbt] def name: String = "NullTerminal"
override private[sbt] val printStream: java.io.PrintStream =
new PrintStream(outputStream, false)
override private[sbt] def withPrintStream[T](f: java.io.PrintStream => T): T = f(printStream)
override private[sbt] def write(bytes: Int*): Unit = {}
}
}

View File

@ -49,7 +49,7 @@ sealed abstract class LogExchange {
config.addLogger(name, loggerConfig)
ctx.updateLoggers
val logger = ctx.getLogger(name)
new ManagedLogger(name, channelName, execId, logger)
new ManagedLogger(name, channelName, execId, logger, Some(Terminal.get))
}
def unbindLoggerAppenders(loggerName: String): Unit = {
val lc = loggerConfig(loggerName)

View File

@ -0,0 +1,66 @@
/*
* 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.scalatest.FlatSpec
class CleanStringSpec extends FlatSpec {
"EscHelpers" should "not modify normal strings" in {
val cleanString = s"1234"
assert(EscHelpers.stripColorsAndMoves(cleanString) == cleanString)
}
it should "remove delete lines" in {
val clean = "1234"
val string = s"${ConsoleAppender.DeleteLine}$clean"
assert(EscHelpers.stripColorsAndMoves(string) == clean)
}
it should "remove cursor left" in {
val clean = "1234"
val backspaced = s"1235${ConsoleAppender.cursorLeft(1)}${ConsoleAppender.clearLine(0)}4"
assert(EscHelpers.stripColorsAndMoves(backspaced) == clean)
}
it should "remove colors" in {
val clean = "1234"
val colored = s"${scala.Console.RED}$clean${scala.Console.RESET}"
assert(EscHelpers.stripColorsAndMoves(colored) == clean)
}
it should "remove backspaces" in {
// Taken from an actual failure case. In the scala client, type 'clean', then type backspace
// five times to clear 'clean' and then retype 'clean'.
val bytes = Array[Byte](27, 91, 50, 75, 27, 91, 48, 74, 27, 91, 50, 75, 27, 91, 49, 48, 48, 48,
68, 115, 98, 116, 58, 115, 99, 97, 108, 97, 45, 99, 111, 109, 112, 105, 108, 101, 27, 91, 51,
54, 109, 62, 32, 27, 91, 48, 109, 99, 108, 101, 97, 110, 8, 27, 91, 75, 110)
val str = new String(bytes)
assert(EscHelpers.stripColorsAndMoves(str) == "sbt:scala-compile> clean")
}
it should "handle cursor left overwrite" in {
val clean = "1234"
val backspaced = s"1235${8.toChar}4${8.toChar}"
assert(EscHelpers.stripColorsAndMoves(backspaced) == clean)
}
it should "remove moves in string with only moves" in {
val original =
new String(Array[Byte](27, 91, 50, 75, 27, 91, 51, 65, 27, 91, 49, 48, 48, 48, 68))
assert(EscHelpers.stripMoves(original) == "")
}
it should "remove moves in string with moves and letters" in {
val original = new String(
Array[Byte](27, 91, 50, 75, 27, 91, 51, 65) ++ "foo".getBytes ++ Array[Byte](27, 91, 49, 48,
48, 48, 68)
)
assert(EscHelpers.stripMoves(original) == "foo")
}
it should "preserve colors" in {
val original = new String(
Array[Byte](27, 91, 49, 48, 48, 48, 68, 27, 91, 48, 74, 102, 111, 111, 27, 91, 51, 54, 109,
62, 32, 27, 91, 48, 109)
) // this is taken from an sbt prompt that looks like "foo> " with the > rendered blue
val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62))
assert(EscHelpers.stripMoves(original) == "foo" + colorArrow + " " + scala.Console.RESET)
}
}

View File

@ -8,6 +8,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.util.Logger
@ -45,14 +46,30 @@ final class Console(compiler: AnalyzingCompiler) {
initialCommands: String,
cleanupCommands: String
)(loader: Option[ClassLoader], bindings: Seq[(String, Any)])(implicit log: Logger): Try[Unit] = {
def console0() =
compiler.console(classpath map { x =>
apply(classpath, options, initialCommands, cleanupCommands, Terminal.get)(loader, bindings)
}
def apply(
classpath: Seq[File],
options: Seq[String],
initialCommands: String,
cleanupCommands: String,
terminal: Terminal
)(loader: Option[ClassLoader], bindings: Seq[(String, Any)])(implicit log: Logger): Try[Unit] = {
def console0(): Unit =
try compiler.console(classpath map { x =>
PlainVirtualFile(x.toPath)
}, options, initialCommands, cleanupCommands, log)(
loader,
bindings
)
Terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
catch { case _: InterruptedException | _: ClosedChannelException => }
val previous = sys.props.get("scala.color").getOrElse("auto")
try {
sys.props("scala.color") = if (terminal.isColorEnabled) "true" else "false"
terminal.withRawSystemIn(Run.executeTrapExit(console0, log))
} finally {
sys.props("scala.color") = previous
}
}
}

View File

@ -0,0 +1,318 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import net.openhft.hashing.LongHashFunction;
import org.scalasbt.ipcsocket.UnixDomainServerSocket;
import org.scalasbt.ipcsocket.UnixDomainSocket;
import org.scalasbt.ipcsocket.Win32NamedPipeServerSocket;
import org.scalasbt.ipcsocket.Win32NamedPipeSocket;
import org.scalasbt.ipcsocket.Win32SecurityLevel;
import xsbti.AppConfiguration;
/**
* A BootServerSocket is used for remote clients to connect to sbt for io while sbt is still loading
* the build. There are two scenarios in which this functionality is needed:
*
* <p>1. client a starts an sbt server and then client b tries to connect to the server before the
* server has loaded. Presently, client b will try to start a new server even though there is one
* booting. This can cause a java process leak because the second server launched by client b is
* unable to create a server because there is an existing portfile by the time it starts up.
*
* <p>2. a remote client initiates a reboot command. Reboot causes sbt to shutdown the server which
* makes the client disconnect. Since sbt does not start the server until the project has
* successfully loaded, there is no way for the client to see the output of the server. This is
* particularly problematic if loading fails because the server will be stuck waiting for input that
* will not be forthcoming.
*
* <p>To address these issues, the BootServerSocket can be used to immediately create a server
* socket before sbt even starts loading the build. It works by creating a local socket either in
* project/target/SOCK_NAME or a windows named pipe with name SOCK_NAME where SOCK_NAME is computed
* as the hash of the project's base directory (for disambiguation in the windows case). If the
* server can't create a server socket because there is already one running, it either prompts the
* user if they want to start a new server even if there is already one running if there is a
* console available or exits with the status code 2 which indicates that there is another sbt
* process starting up.
*
* <p>Once the server socket is created, it listens for new client connections. When a client
* connects, the server will forward its input and output to the client via Terminal.setBootStreams
* which updates the Terminal.proxyOutputStream to forward all bytes written to the
* BootServerSocket's outputStream which in turn writes the output to each of the connected clients.
* Input is handed similarly.
*
* <p>When the server finishes loading, it closes the boot server socket.
*
* <p>BootServerSocket is implemented in java so that it can be classloaded as quickly as possible.
*/
public class BootServerSocket implements AutoCloseable {
private ServerSocket serverSocket = null;
private final AtomicBoolean closed = new AtomicBoolean(false);
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicInteger threadId = new AtomicInteger(1);
private final Future<?> acceptFuture;
private final ExecutorService service =
Executors.newCachedThreadPool(
r -> new Thread(r, "boot-server-socket-thread-" + threadId.getAndIncrement()));
private final Set<ClientSocket> clientSockets = ConcurrentHashMap.newKeySet();
private final Object lock = new Object();
private final LinkedBlockingQueue<ClientSocket> clientSocketReads = new LinkedBlockingQueue<>();
private final Path socketFile;
private class ClientSocket implements AutoCloseable {
final Socket socket;
final AtomicBoolean alive = new AtomicBoolean(true);
final Future<?> future;
private final LinkedBlockingQueue<Integer> bytes = new LinkedBlockingQueue<Integer>();
private final AtomicBoolean closed = new AtomicBoolean(false);
@SuppressWarnings("deprecation")
ClientSocket(final Socket socket) {
this.socket = socket;
clientSockets.add(this);
Future<?> f = null;
try {
f =
service.submit(
() -> {
try {
final InputStream inputStream = socket.getInputStream();
while (alive.get()) {
try {
int b = inputStream.read();
if (b != -1) {
bytes.put(b);
clientSocketReads.put(ClientSocket.this);
} else {
alive.set(false);
}
} catch (IOException e) {
alive.set(false);
}
}
} catch (final Exception ex) {
}
});
} catch (final RejectedExecutionException e) {
alive.set(false);
}
future = f;
}
private void write(final int i) {
try {
if (alive.get()) socket.getOutputStream().write(i);
} catch (final IOException e) {
alive.set(false);
close();
}
}
private void write(final byte[] b, final int offset, final int len) {
try {
if (alive.get()) socket.getOutputStream().write(b, offset, len);
} catch (final IOException e) {
alive.set(false);
close();
}
}
private void flush() {
try {
socket.getOutputStream().flush();
} catch (final IOException e) {
alive.set(false);
close();
}
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
if (alive.get()) {
write(2);
bytes.forEach(this::write);
bytes.clear();
write(3);
flush();
}
alive.set(false);
if (future != null) future.cancel(true);
try {
socket.getOutputStream().close();
socket.getInputStream().close();
// Windows is very slow to close the socket for whatever reason
// We close the server socket anyway, so this should die then.
if (!System.getProperty("os.name", "").toLowerCase().startsWith("win")) socket.close();
} catch (final IOException e) {
}
clientSockets.remove(this);
}
}
}
private final Object writeLock = new Object();
public InputStream inputStream() {
return inputStream;
}
private final InputStream inputStream =
new InputStream() {
@Override
public int read() {
try {
ClientSocket clientSocket = clientSocketReads.take();
return clientSocket.bytes.take();
} catch (final InterruptedException e) {
return -1;
}
}
};
private final OutputStream outputStream =
new OutputStream() {
@Override
public void write(final int b) {
synchronized (lock) {
clientSockets.forEach(cs -> cs.write(b));
}
}
@Override
public void write(final byte[] b) {
write(b, 0, b.length);
}
@Override
public void write(final byte[] b, final int offset, final int len) {
synchronized (lock) {
clientSockets.forEach(cs -> cs.write(b, offset, len));
}
}
@Override
public void flush() {
synchronized (lock) {
clientSockets.forEach(cs -> cs.flush());
}
}
};
public OutputStream outputStream() {
return outputStream;
}
private final Runnable acceptRunnable =
() -> {
try {
serverSocket.setSoTimeout(5000);
while (running.get()) {
try {
ClientSocket clientSocket = new ClientSocket(serverSocket.accept());
} catch (final SocketTimeoutException e) {
} catch (final IOException e) {
running.set(false);
}
}
} catch (final SocketException e) {
}
};
public BootServerSocket(final AppConfiguration configuration)
throws ServerAlreadyBootingException, IOException {
final Path base = configuration.baseDirectory().toPath().toRealPath();
final Path target = base.resolve("project").resolve("target");
if (!isWindows) {
socketFile = Paths.get(socketLocation(base));
Files.createDirectories(target);
} else {
socketFile = null;
}
serverSocket = newSocket(socketLocation(base));
if (serverSocket != null) {
running.set(true);
acceptFuture = service.submit(acceptRunnable);
} else {
closed.set(true);
acceptFuture = null;
}
}
public static String socketLocation(final Path base) throws UnsupportedEncodingException {
final Path target = base.resolve("project").resolve("target");
if (isWindows) {
long hash = LongHashFunction.farmNa().hashBytes(target.toString().getBytes("UTF-8"));
return "sbt-load" + hash;
} else {
return base.relativize(target.resolve("sbt-load.sock")).toString();
}
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
// avoid concurrent modification exception
clientSockets.forEach(ClientSocket::close);
if (acceptFuture != null) acceptFuture.cancel(true);
service.shutdownNow();
try {
if (serverSocket != null) serverSocket.close();
} catch (final IOException e) {
}
try {
if (socketFile != null) Files.deleteIfExists(socketFile);
} catch (final IOException e) {
}
}
}
static final boolean isWindows =
System.getProperty("os.name", "").toLowerCase().startsWith("win");
static ServerSocket newSocket(final String sock) throws ServerAlreadyBootingException {
ServerSocket socket = null;
String name = socketName(sock);
try {
if (!isWindows) Files.deleteIfExists(Paths.get(sock));
socket =
isWindows
? new Win32NamedPipeServerSocket(name, false, Win32SecurityLevel.OWNER_DACL)
: new UnixDomainServerSocket(name);
return socket;
} catch (final IOException e) {
throw new ServerAlreadyBootingException();
}
}
private static String socketName(String sock) {
return isWindows ? "\\\\.\\pipe\\" + sock : sock;
}
}

View File

@ -0,0 +1,10 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal;
public class ServerAlreadyBootingException extends Exception {}

View File

@ -14,8 +14,10 @@ object BasicCommandStrings {
val HelpCommand: String = "help"
val CompletionsCommand: String = "completions"
val Exit: String = "exit"
val Shutdown: String = "shutdown"
val Quit: String = "quit"
val TemplateCommand: String = "new"
val Cancel: String = "cancel"
/** The command name to terminate the program.*/
val TerminateAction: String = Exit
@ -57,7 +59,8 @@ $HelpCommand <regular expression>
def historyHelp =
Help(Nil, (HistoryHelpBrief +: HistoryCommands.descriptions).toMap, Set(HistoryCommands.Start))
def exitBrief: String = "Terminates the build."
def exitBrief: String = "Terminates the remote client or the build when called from the console."
def shutdownBrief: String = "Terminates the build."
def logLevelHelp: Help = {
val levels = Level.values.toSeq
@ -134,6 +137,8 @@ $HelpCommand <regular expression>
If a classpath is provided, modules are loaded from a new class loader for this classpath.
"""
private[sbt] def RebootNetwork: String = "sbtRebootNetwork"
private[sbt] def RebootImpl: String = "sbtRebootImpl"
def RebootCommand: String = "reboot"
def RebootDetailed: String =
RebootCommand + """ [dev | full]
@ -203,12 +208,19 @@ $AliasCommand name=
"Provides an interactive prompt from which commands can be run on a server."
def DashClient: String = "-client"
def DashDashClient: String = "--client"
def CloseIOStreams: String = "--close-io-streams"
def StashOnFailure: String = "sbtStashOnFailure"
def PopOnFailure: String = "sbtPopOnFailure"
def FailureWall: String = "resumeFromFailure"
def SetTerminal = "sbtSetTerminal"
def ReportResult = "sbtReportResult"
def CompleteExec = "sbtCompleteExec"
def MapExec = "sbtMapExec"
def PromptChannel = "sbtPromptChannel"
def ClearOnFailure: String = "sbtClearOnFailure"
def OnFailure: String = "onFailure"
def OnFailureDetailed: String =
@ -235,4 +247,7 @@ $AliasCommand name=
(ContinuousExecutePrefix + " <command>", continuousDetail)
def ClearCaches: String = "clearCaches"
def ClearCachesDetailed: String = "Clears all of sbt's internal caches."
private[sbt] val networkExecPrefix = "__"
private[sbt] val DisconnectNetworkChannel = s"${networkExecPrefix}disconnectNetworkChannel"
}

View File

@ -53,14 +53,19 @@ object BasicCommands {
stashOnFailure,
popOnFailure,
reboot,
rebootImpl,
call,
early,
exit,
shutdown,
history,
oldshell,
client,
read,
alias
alias,
reportResultsCommand,
mapExecCommand,
completeExecCommand,
)
def nop: Command = Command.custom(s => success(() => s))
@ -300,6 +305,12 @@ object BasicCommands {
def reboot: Command =
Command(RebootCommand, Help.more(RebootCommand, RebootDetailed))(_ => rebootOptionParser) {
case (s, (full, currentOnly)) =>
val option = if (full) " full" else if (currentOnly) " dev" else ""
RebootNetwork :: s"$RebootImpl$option" :: s
}
def rebootImpl: Command =
Command.arb(_ => (RebootImpl ~> rebootOptionParser).examples()) {
case (s, (full, currentOnly)) =>
s.reboot(full, currentOnly)
}
@ -346,7 +357,20 @@ object BasicCommands {
private[this] def classpathStrings: Parser[Seq[String]] =
token(StringBasic.map(s => IO.pathSplit(s).toSeq), "<classpath>")
def exit: Command = Command.command(TerminateAction, exitBrief, exitBrief)(_ exit true)
def exit: Command = Command.command(TerminateAction, exitBrief, exitBrief) { s =>
s.source match {
case Some(c) if c.channelName.startsWith("network") =>
s"${DisconnectNetworkChannel} ${c.channelName}" :: s
case _ => s exit true
}
}
def shutdown: Command = Command.command(Shutdown, shutdownBrief, shutdownBrief) { s =>
s.source match {
case Some(c) if c.channelName.startsWith("network") =>
s"${DisconnectNetworkChannel} ${c.channelName}" :: (Exec(Shutdown, None) +: s)
case _ => s exit true
}
}
@deprecated("Replaced by BuiltInCommands.continuous", "1.3.0")
def continuous: Command =
@ -375,8 +399,7 @@ object BasicCommands {
def oldshell: Command = Command.command(OldShell, Help.more(Shell, OldShellDetailed)) { s =>
val history = (s get historyPath) getOrElse (new File(s.baseDir, ".history")).some
val prompt = (s get shellPrompt) match { case Some(pf) => pf(s); case None => "> " }
val reader =
new FullReader(history, s.combinedParser, LineReader.HandleCONT, Terminal.wrappedSystemIn)
val reader = new FullReader(history, s.combinedParser, LineReader.HandleCONT, Terminal.console)
val line = reader.readLine(prompt)
line match {
case Some(line) =>
@ -404,7 +427,7 @@ object BasicCommands {
case xs => xs map (_.commandLine)
})
NetworkClient.run(s0.configuration, arguments)
"exit" :: s0.copy(remainingCommands = Nil)
TerminateAction :: s0.copy(remainingCommands = Nil)
}
def read: Command =
@ -539,4 +562,42 @@ object BasicCommands {
"is-command-alias",
"Internal: marker for Commands created as aliases for another command."
)
private[sbt] def reportParser(key: String) =
(key: Parser[String]).examples() ~> " ".examples() ~> matched(any.*).examples()
def reportResultsCommand =
Command.arb(_ => reportParser(ReportResult)) { (state, id) =>
val newState = state.get(execMap) match {
case Some(m) => state.put(execMap, m - id)
case _ => state
}
newState.get(execResults) match {
case Some(m) if m.contains(id) => state.put(execResults, m - id)
case _ => state.fail
}
}
def mapExecCommand =
Command.arb(_ => reportParser(MapExec)) { (state, mapping) =>
mapping.split(" ") match {
case Array(key, value) =>
state.get(execMap) match {
case Some(m) => state.put(execMap, m + (key -> value))
case None => state.put(execMap, Map(key -> value))
}
case _ => state
}
}
def completeExecCommand =
Command.arb(_ => reportParser(CompleteExec)) { (state, id) =>
val newState = state.get(execResults) match {
case Some(m) => state.put(execResults, m + (id -> true))
case _ => state.put(execResults, Map(id -> true))
}
newState.get(execMap) match {
case Some(m) => newState.put(execMap, m - id)
case _ => newState
}
}
private[sbt] val execResults = AttributeKey[Map[String, Boolean]]("execResults", Int.MaxValue)
private[sbt] val execMap = AttributeKey[Map[String, String]]("execMap", Int.MaxValue)
}

View File

@ -13,9 +13,10 @@ import com.github.ghik.silencer.silent
import sbt.internal.inc.classpath.{ ClassLoaderCache => IncClassLoaderCache }
import sbt.internal.classpath.ClassLoaderCache
import sbt.internal.server.ServerHandler
import sbt.internal.util.AttributeKey
import sbt.internal.util.{ AttributeKey, Terminal }
import sbt.librarymanagement.ModuleID
import sbt.util.Level
import scala.concurrent.duration.FiniteDuration
object BasicKeys {
val historyPath = AttributeKey[Option[File]](
@ -35,6 +36,11 @@ object BasicKeys {
"The function that constructs the command prompt from the current build state.",
10000
)
val terminalShellPrompt = AttributeKey[(Terminal, State) => String](
"new-shell-prompt",
"The function that constructs the command prompt from the current build state for a given terminal.",
10000
)
@silent val watch =
AttributeKey[Watched]("watched", "Continuous execution configuration.", 1000)
val serverPort =
@ -71,6 +77,20 @@ object BasicKeys {
10000
)
val windowsServerSecurityLevel =
AttributeKey[Int](
"windowsServerSecurityLevel",
"Configures the security level of the named pipe. Values: 0 - No security; 1 - Logon user only; 2 - Process owner only",
10000
)
val serverIdleTimeout =
AttributeKey[Option[FiniteDuration]](
"serverIdleTimeOut",
"If set to a defined value, sbt server will exit if it goes at least the specified duration without receiving any commands.",
10000
)
// Unlike other BasicKeys, this is not used directly as a setting key,
// and severLog / logLevel is used instead.
private[sbt] val serverLogLevel =
@ -109,6 +129,11 @@ object BasicKeys {
"List of template resolver infos.",
1000
)
private[sbt] val closeIOStreams = AttributeKey[Boolean](
"close-io-streams",
"Toggles wheter or not to close system in, out and error when the server starts.",
1000
)
}
case class TemplateResolverInfo(module: ModuleID, implementationClass: String)

View File

@ -16,6 +16,16 @@ import sbt.internal.inc.classpath.{ ClassLoaderCache => IncClassLoaderCache }
import sbt.internal.util.complete.{ HistoryCommands, Parser }
import sbt.internal.util._
import sbt.util.Logger
import BasicCommandStrings.{
CompleteExec,
MapExec,
PopOnFailure,
ReportResult,
SetTerminal,
StartServer,
StashOnFailure,
networkExecPrefix,
}
/**
* Data structure representing all command execution information.
@ -273,8 +283,43 @@ object State {
f(cmd, s1)
}
s.remainingCommands match {
case Nil => exit(true)
case x :: xs => runCmd(x, xs)
case Nil => exit(true)
case x :: xs =>
(x.execId, x.source) match {
/*
* If the command is coming from a network source, it might be a multi-command. To handle
* that, we need to give the command a new exec id and wrap some commands around the
* actual command that are used to report it. To make this work, we add a map of exec
* results as well as a mapping of exec ids to the exec id that spawned the exec.
* We add a command that fills the result map for the original exec. If the command fails,
* that map filling command (called sbtCompleteExec) is skipped so the map is never filled
* for the original event. The report command (called sbtReportResult) checks the result
* map and, if it finds an entry, it succeeds and removes the entry. Otherwise it fails.
* The exec for the report command is given the original exec id so the result reported
* to the client will be the result of the report command (which should correspond to
* the result of the underlying multi-command, which succeeds only if all of the commands
* succeed)
*
*/
case (Some(id), Some(s))
if s.channelName.startsWith("network") &&
!x.commandLine.startsWith(ReportResult) &&
!x.commandLine.startsWith(networkExecPrefix) &&
!id.startsWith(networkExecPrefix) =>
val newID = networkExecPrefix + Exec.newExecId
val cmd = x.withExecId(newID)
val map = Exec(s"$MapExec $id $newID", None)
val complete = Exec(s"$CompleteExec $id", None)
val report = Exec(s"$ReportResult $id", Some(id), x.source)
val stash = Exec(StashOnFailure, None)
val failureWall = Exec(FailureWall, None)
val pop = Exec(PopOnFailure, None)
val setTerm = Exec(s"$SetTerminal ${s.channelName}", None)
val setConsole = Exec(s"$SetTerminal console0", None)
val remaining = stash :: map :: cmd :: complete :: failureWall :: pop :: setConsole :: report :: xs
runCmd(setTerm, remaining)
case _ => runCmd(x, xs)
}
}
}
def :::(newCommands: List[String]): State = ++:(newCommands map { Exec(_, s.source) })
@ -295,9 +340,14 @@ object State {
/** Implementation of reboot. */
private[sbt] def reboot(full: Boolean, currentOnly: Boolean): State = {
runExitHooks()
val rs = s.remainingCommands map { case e: Exec => e.commandLine }
if (currentOnly) throw new RebootCurrent(rs)
else throw new xsbti.FullReload(rs.toArray, full)
val remaining: List[String] = s.remainingCommands.map(_.commandLine)
val fullRemaining = s.source match {
case Some(s) if s.channelName.startsWith("network") =>
StartServer :: remaining.dropWhile(!_.startsWith(ReportResult)).tail ::: "shell" :: Nil
case _ => remaining
}
if (currentOnly) throw new RebootCurrent(fullRemaining)
else throw new xsbti.FullReload(fullRemaining.toArray, full)
}
def reload = runExitHooks().setNext(new Return(defaultReload(s)))

View File

@ -9,8 +9,14 @@ package sbt
package internal
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicReference
import sbt.internal.ui.{ UITask, UserThread }
import sbt.internal.util.Terminal
import sbt.protocol.EventMessage
import sbt.util.Level
import scala.collection.JavaConverters._
/**
* A command channel represents an IO device such as network socket or human
@ -19,31 +25,81 @@ import sbt.protocol.EventMessage
*/
abstract class CommandChannel {
private val commandQueue: ConcurrentLinkedQueue[Exec] = new ConcurrentLinkedQueue()
private val registered: java.util.Set[java.util.Queue[CommandChannel]] = new java.util.HashSet
private[sbt] final def register(queue: java.util.Queue[CommandChannel]): Unit = {
registered.add(queue)
()
private val registered: java.util.Set[java.util.Queue[Exec]] = new java.util.HashSet
private val fastTrack: java.util.Set[java.util.Queue[FastTrackTask]] = new java.util.HashSet
private[sbt] final def register(
queue: java.util.Queue[Exec],
fastTrackQueue: java.util.Queue[FastTrackTask]
): Unit =
registered.synchronized {
registered.add(queue)
if (!commandQueue.isEmpty) {
queue.addAll(commandQueue)
commandQueue.clear()
}
fastTrack.add(fastTrackQueue)
()
}
private[sbt] final def unregister(
queue: java.util.Queue[CommandChannel],
fastTrackQueue: java.util.Queue[FastTrackTask]
): Unit =
registered.synchronized {
registered.remove(queue)
fastTrack.remove(fastTrackQueue)
()
}
private[sbt] final def addFastTrackTask(task: String): Unit = {
fastTrack.forEach(q => q.synchronized { q.add(new FastTrackTask(this, task)); () })
}
private[sbt] final def unregister(queue: java.util.Queue[CommandChannel]): Unit = {
registered.remove(queue)
()
}
def append(exec: Exec): Boolean = {
registered.forEach(
q =>
q.synchronized {
if (!q.contains(this)) {
q.add(this); ()
}
}
)
commandQueue.add(exec)
private[sbt] def mkUIThread: (State, CommandChannel) => UITask
private[sbt] def makeUIThread(state: State): UITask = mkUIThread(state, this)
final def append(exec: Exec): Boolean = {
registered.synchronized {
exec.commandLine.nonEmpty && {
if (registered.isEmpty) commandQueue.add(exec)
else registered.asScala.forall(_.add(exec))
}
}
}
def poll: Option[Exec] = Option(commandQueue.poll)
def prompt(e: ConsolePromptEvent): Unit = userThread.onConsolePromptEvent(e)
def unprompt(e: ConsoleUnpromptEvent): Unit = userThread.onConsoleUnpromptEvent(e)
def publishBytes(bytes: Array[Byte]): Unit
def shutdown(): Unit
private[sbt] def userThread: UserThread
def shutdown(logShutdown: Boolean): Unit = {
userThread.stopThread()
userThread.close()
}
@deprecated("Use the variant that takes the logShutdown parameter", "1.4.0")
def shutdown(): Unit = shutdown(true)
def name: String
private[this] val level = new AtomicReference[Level.Value](Level.Info)
private[sbt] final def setLevel(l: Level.Value): Unit = level.set(l)
private[sbt] final def logLevel: Level.Value = level.get
private[this] def setLevel(value: Level.Value, cmd: String): Boolean = {
level.set(value)
append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name))))
}
private[sbt] def onCommand: String => Boolean = {
case "error" => setLevel(Level.Error, "error")
case "debug" => setLevel(Level.Debug, "debug")
case "info" => setLevel(Level.Info, "info")
case "warn" => setLevel(Level.Warn, "warn")
case cmd =>
if (cmd.nonEmpty) append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name))))
else false
}
private[sbt] def onFastTrackTask: String => Boolean = { s: String =>
fastTrack.synchronized(fastTrack.forEach { q =>
q.add(new FastTrackTask(this, s))
()
})
true
}
private[sbt] def terminal: Terminal
}
// case class Exec(commandLine: String, source: Option[CommandSource])
@ -58,5 +114,6 @@ case class ConsolePromptEvent(state: State) extends EventMessage
/*
* This is a data passed specifically for unprompting local console.
*/
@deprecated("No longer used", "1.4.0")
case class ConsoleUnpromptEvent(lastSource: Option[CommandSource]) extends EventMessage
private[internal] class FastTrackTask(val channel: CommandChannel, val task: String)

View File

@ -8,76 +8,24 @@
package sbt
package internal
import java.io.File
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicReference
import sbt.BasicKeys._
import sbt.internal.ui.{ UITask, UserThread }
import sbt.internal.util._
import sjsonnew.JsonFormat
private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel {
private[this] val askUserThread = new AtomicReference[AskUserThread]
private[this] def getPrompt(s: State): String = s.get(shellPrompt) match {
case Some(pf) => pf(s)
case None =>
def ansi(s: String): String = if (ConsoleAppender.formatEnabledInEnv) s"$s" else ""
s"${ansi(ConsoleAppender.DeleteLine)}> ${ansi(ConsoleAppender.ClearScreenAfterCursor)}"
}
private[this] class AskUserThread(s: State) extends Thread("ask-user-thread") {
private val history = s.get(historyPath).getOrElse(Some(new File(s.baseDir, ".history")))
private val prompt = getPrompt(s)
private val reader =
new FullReader(
history,
s.combinedParser,
LineReader.HandleCONT,
Terminal.throwOnClosedSystemIn
)
setDaemon(true)
start()
override def run(): Unit =
try {
reader.readLine(prompt) match {
case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name))))
case None =>
println("") // Prevents server shutdown log lines from appearing on the prompt line
append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name))))
}
()
} catch {
case _: ClosedChannelException =>
} finally askUserThread.synchronized(askUserThread.set(null))
def redraw(): Unit = {
System.out.print(ConsoleAppender.clearLine(0))
reader.redraw()
System.out.print(ConsoleAppender.ClearScreenAfterCursor)
System.out.flush()
}
}
private[this] def makeAskUserThread(s: State): AskUserThread = new AskUserThread(s)
private[sbt] final class ConsoleChannel(
val name: String,
override private[sbt] val mkUIThread: (State, CommandChannel) => UITask
) extends CommandChannel {
def run(s: State): State = s
def publishBytes(bytes: Array[Byte]): Unit = ()
def prompt(event: ConsolePromptEvent): Unit = {
if (Terminal.systemInIsAttached) {
askUserThread.synchronized {
askUserThread.get match {
case null => askUserThread.set(makeAskUserThread(event.state))
case t => t.redraw()
}
}
}
}
def publishEvent[A: JsonFormat](event: A, execId: Option[String]): Unit = ()
def shutdown(): Unit = askUserThread.synchronized {
askUserThread.get match {
case null =>
case t if t.isAlive =>
t.interrupt()
askUserThread.set(null)
case _ => ()
}
}
override val userThread: UserThread = new UserThread(this)
private[sbt] def terminal = Terminal.console
}
private[sbt] object ConsoleChannel {
private[sbt] def defaultName = "console0"
}

View File

@ -9,85 +9,50 @@ package sbt
package internal
package client
import java.net.{ SocketTimeoutException, Socket }
import java.io.IOException
import java.net.{ Socket, SocketTimeoutException }
import java.util.concurrent.atomic.AtomicBoolean
import sbt.protocol._
import sbt.internal.protocol._
import sbt.internal.util.ReadJsonFromInputStream
abstract class ServerConnection(connection: Socket) {
private val running = new AtomicBoolean(true)
private val closed = new AtomicBoolean(false)
private val retByte: Byte = '\r'.toByte
private val delimiter: Byte = '\n'.toByte
private val out = connection.getOutputStream
val thread = new Thread(s"sbt-serverconnection-${connection.getPort}") {
setDaemon(true)
override def run(): Unit = {
try {
val readBuffer = new Array[Byte](4096)
val in = connection.getInputStream
connection.setSoTimeout(5000)
var buffer: Vector[Byte] = Vector.empty
def readFrame: Vector[Byte] = {
def getContentLength: Int = {
readLine.drop(16).toInt
}
val l = getContentLength
readLine
readLine
readContentLength(l)
}
def readLine: String = {
if (buffer.isEmpty) {
val bytesRead = in.read(readBuffer)
if (bytesRead > 0) {
buffer = buffer ++ readBuffer.toVector.take(bytesRead)
}
}
val delimPos = buffer.indexOf(delimiter)
if (delimPos > 0) {
val chunk0 = buffer.take(delimPos)
buffer = buffer.drop(delimPos + 1)
// remove \r at the end of line.
val chunk1 = if (chunk0.lastOption contains retByte) chunk0.dropRight(1) else chunk0
new String(chunk1.toArray, "utf-8")
} else readLine
}
def readContentLength(length: Int): Vector[Byte] = {
if (buffer.size < length) {
val bytesRead = in.read(readBuffer)
if (bytesRead > 0) {
buffer = buffer ++ readBuffer.toVector.take(bytesRead)
} else ()
} else ()
if (length <= buffer.size) {
val chunk = buffer.take(length)
buffer = buffer.drop(length)
chunk
} else readContentLength(length)
}
while (running.get) {
try {
val frame = readFrame
Serialization
.deserializeJsonMessage(frame)
.fold(
{ errorDesc =>
val s = frame.mkString("") // new String(: Array[Byte], "UTF-8")
println(s"Got invalid chunk from server: $s \n" + errorDesc)
},
_ match {
case msg: JsonRpcRequestMessage => onRequest(msg)
case msg: JsonRpcResponseMessage => onResponse(msg)
case msg: JsonRpcNotificationMessage => onNotification(msg)
}
)
val frame = ReadJsonFromInputStream(in, running, None)
if (running.get) {
Serialization
.deserializeJsonMessage(frame)
.fold(
{ errorDesc =>
val s = frame.mkString("") // new String(: Array[Byte], "UTF-8")
println(s"Got invalid chunk from server: $s \n" + errorDesc)
},
_ match {
case msg: JsonRpcRequestMessage => onRequest(msg)
case msg: JsonRpcResponseMessage => onResponse(msg)
case msg: JsonRpcNotificationMessage => onNotification(msg)
}
)
}
} catch {
case _: SocketTimeoutException => // its ok
case e: IOException => running.set(false)
}
}
} finally {
@ -97,24 +62,29 @@ abstract class ServerConnection(connection: Socket) {
}
thread.start()
def sendString(message: String): Unit = {
def sendString(message: String): Unit = this.synchronized {
val a = message.getBytes("UTF-8")
writeLine(s"""Content-Length: ${a.length + 2}""".getBytes("UTF-8"))
writeLine(Array())
writeLine(a)
}
def writeLine(a: Array[Byte]): Unit = {
def writeEndLine(): Unit = {
out.write(retByte.toInt)
out.write(delimiter.toInt)
out.flush
def writeLine(a: Array[Byte]): Unit =
try {
def writeEndLine(): Unit = {
out.write(retByte.toInt)
out.write(delimiter.toInt)
out.flush
}
if (a.nonEmpty) {
out.write(a)
}
writeEndLine
} catch {
case e: IOException =>
shutdown()
throw e
}
if (a.nonEmpty) {
out.write(a)
}
writeEndLine
}
def onRequest(msg: JsonRpcRequestMessage): Unit
def onResponse(msg: JsonRpcResponseMessage): Unit
@ -122,10 +92,14 @@ abstract class ServerConnection(connection: Socket) {
def onShutdown(): Unit
def shutdown(): Unit = {
println("Shutting down client connection")
running.set(false)
out.close()
def shutdown(): Unit = if (closed.compareAndSet(false, true)) {
if (!running.compareAndSet(true, false)) {
System.err.println("\nsbt server connection closed.")
}
try {
out.close()
connection.close()
} catch { case e: IOException => e.printStackTrace() }
onShutdown
}

View File

@ -12,7 +12,7 @@ package server
import java.io.{ File, IOException }
import java.lang.management.ManagementFactory
import java.net.{ InetAddress, ServerSocket, Socket, SocketTimeoutException }
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.nio.file.attribute.{ AclEntry, AclEntryPermission, AclEntryType, UserPrincipal }
import java.security.SecureRandom
import java.math.BigInteger
@ -56,7 +56,7 @@ private[sbt] object Server {
val ready: Future[Unit] = p.future
private[this] val rand = new SecureRandom
private[this] var token: String = nextToken
private[this] var serverSocketOpt: Option[ServerSocket] = None
private[this] val serverSocketHolder = new AtomicReference[ServerSocket]
val serverThread = new Thread("sbt-socket-server") {
override def run(): Unit = {
@ -64,7 +64,13 @@ private[sbt] object Server {
connection.connectionType match {
case ConnectionType.Local if isWindows =>
// Named pipe already has an exclusive lock.
addServerError(new Win32NamedPipeServerSocket(pipeName))
addServerError(
new Win32NamedPipeServerSocket(
pipeName,
false,
connection.windowsServerSecurityLevel
)
)
case ConnectionType.Local =>
val maxSocketLength = new UnixDomainSocketLibrary.SockaddrUn().sunPath.length - 1
val path = socketfile.getAbsolutePath
@ -86,7 +92,10 @@ private[sbt] object Server {
case Failure(e) => p.failure(e)
case Success(serverSocket) =>
serverSocket.setSoTimeout(5000)
serverSocketOpt = Option(serverSocket)
serverSocketHolder.getAndSet(serverSocket) match {
case null =>
case s => s.close()
}
log.info(s"sbt server started at ${connection.shortName}")
writePortfile()
writeBspConnectionDetails()
@ -97,10 +106,14 @@ private[sbt] object Server {
val socket = serverSocket.accept()
onIncomingSocket(socket, self)
} catch {
case _: SocketTimeoutException => // its ok
case e: IOException if e.getMessage.contains("connect") =>
case _: SocketTimeoutException => // its ok
}
}
serverSocket.close()
serverSocketHolder.get match {
case null =>
case s => s.close()
}
}
}
}
@ -146,6 +159,10 @@ private[sbt] object Server {
IO.delete(tokenfile)
}
running.set(false)
serverSocketHolder.getAndSet(null) match {
case null =>
case s => s.close()
}
}
private[this] def writeTokenfile(): Unit = {
@ -227,7 +244,8 @@ private[sbt] case class ServerConnection(
socketfile: File,
pipeName: String,
bspConnectionFile: File,
appConfiguration: AppConfiguration
appConfiguration: AppConfiguration,
windowsServerSecurityLevel: Int
) {
def shortName: String = {
connectionType match {

View File

@ -29,14 +29,18 @@ object ServerHandler {
lazy val fallback: ServerHandler = ServerHandler({ handler =>
ServerIntent(
{ case x => handler.log.debug(s"Unhandled notification received: ${x.method}: $x") },
{ case x => handler.log.debug(s"Unhandled request received: ${x.method}: $x") }
onRequest = { case x => handler.log.debug(s"Unhandled request received: ${x.method}: $x") },
onResponse = { case x => handler.log.debug(s"Unhandled responce received") },
onNotification = {
case x => handler.log.debug(s"Unhandled notification received: ${x.method}: $x")
},
)
})
}
final class ServerIntent(
val onRequest: PartialFunction[JsonRpcRequestMessage, Unit],
val onResponse: PartialFunction[JsonRpcResponseMessage, Unit],
val onNotification: PartialFunction[JsonRpcNotificationMessage, Unit]
) {
override def toString: String = s"ServerIntent(...)"
@ -45,15 +49,18 @@ final class ServerIntent(
object ServerIntent {
def apply(
onRequest: PartialFunction[JsonRpcRequestMessage, Unit],
onResponse: PartialFunction[JsonRpcResponseMessage, Unit],
onNotification: PartialFunction[JsonRpcNotificationMessage, Unit]
): ServerIntent =
new ServerIntent(onRequest, onNotification)
new ServerIntent(onRequest, onResponse, onNotification)
def request(onRequest: PartialFunction[JsonRpcRequestMessage, Unit]): ServerIntent =
new ServerIntent(onRequest, PartialFunction.empty)
new ServerIntent(onRequest, PartialFunction.empty, PartialFunction.empty)
def response(onResponse: PartialFunction[JsonRpcResponseMessage, Unit]): ServerIntent =
new ServerIntent(PartialFunction.empty, onResponse, PartialFunction.empty)
def notify(onNotification: PartialFunction[JsonRpcNotificationMessage, Unit]): ServerIntent =
new ServerIntent(PartialFunction.empty, onNotification)
new ServerIntent(PartialFunction.empty, PartialFunction.empty, onNotification)
}
/**

View File

@ -0,0 +1,107 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.ui
import java.io.File
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicBoolean
import jline.console.history.PersistentHistory
import sbt.BasicCommandStrings.{ Cancel, TerminateAction, Shutdown }
import sbt.BasicKeys.{ historyPath, terminalShellPrompt }
import sbt.State
import sbt.internal.CommandChannel
import sbt.internal.util.ConsoleAppender.{ ClearPromptLine, ClearScreenAfterCursor, DeleteLine }
import sbt.internal.util._
import sbt.internal.util.complete.{ JLineCompletion, Parser }
import scala.annotation.tailrec
private[sbt] trait UITask extends Runnable with AutoCloseable {
private[sbt] def channel: CommandChannel
private[sbt] def reader: UITask.Reader
private[this] final def handleInput(s: Either[String, String]): Boolean = s match {
case Left(m) => channel.onFastTrackTask(m)
case Right(cmd) => channel.onCommand(cmd)
}
private[this] val isStopped = new AtomicBoolean(false)
override def run(): Unit = {
@tailrec def impl(): Unit = {
val res = reader.readLine()
if (!handleInput(res) && !isStopped.get) impl()
}
try impl()
catch { case _: InterruptedException | _: ClosedChannelException => isStopped.set(true) }
}
override def close(): Unit = isStopped.set(true)
}
private[sbt] object UITask {
trait Reader { def readLine(): Either[String, String] }
object Reader {
def terminalReader(parser: Parser[_])(
terminal: Terminal,
state: State
): Reader = {
val lineReader = LineReader.createReader(history(state), terminal, terminal.prompt)
JLineCompletion.installCustomCompletor(lineReader, parser)
() => {
val clear = terminal.ansi(ClearPromptLine, "")
try {
@tailrec def impl(): Either[String, String] = {
lineReader.readLine(clear + terminal.prompt.mkPrompt()) match {
case null if terminal == Terminal.console && System.console == null =>
// No stdin is attached to the process so just ignore the result and
// block until the thread is interrupted.
this.synchronized(this.wait())
Right("") // should be unreachable
// JLine returns null on ctrl+d when there is no other input. This interprets
// ctrl+d with no imput as an exit
case null => Left(TerminateAction)
case s: String =>
lineReader.getHistory match {
case p: PersistentHistory =>
p.add(s)
p.flush()
case _ =>
}
s match {
case "" => impl()
case cmd @ (`Shutdown` | `TerminateAction` | `Cancel`) => Left(cmd)
case cmd =>
if (terminal.prompt != Prompt.Batch) terminal.setPrompt(Prompt.Running)
terminal.printStream.write(Int.MinValue)
Right(cmd)
}
}
}
impl()
} catch {
case _: InterruptedException => Right("")
} finally lineReader.close()
}
}
}
private[this] def history(s: State): Option[File] =
s.get(historyPath).getOrElse(Some(new File(s.baseDir, ".history")))
private[sbt] def shellPrompt(terminal: Terminal, s: State): String =
s.get(terminalShellPrompt) match {
case Some(pf) => pf(terminal, s)
case None =>
def ansi(s: String): String = if (terminal.isAnsiSupported) s"$s" else ""
s"${ansi(DeleteLine)}> ${ansi(ClearScreenAfterCursor)}"
}
private[sbt] class AskUserTask(
state: State,
override val channel: CommandChannel,
) extends UITask {
override private[sbt] def reader: UITask.Reader = {
UITask.Reader.terminalReader(state.combinedParser)(channel.terminal, state)
}
}
}

View File

@ -0,0 +1,91 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
package ui
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.Executors
import sbt.State
import sbt.internal.util.{ ConsoleAppender, ProgressEvent, ProgressState, Util }
import sbt.internal.util.Prompt.{ AskUser, Running }
private[sbt] class UserThread(val channel: CommandChannel) extends AutoCloseable {
private[this] val uiThread = new AtomicReference[(UITask, Thread)]
private[sbt] final def onProgressEvent(pe: ProgressEvent): Unit = {
lastProgressEvent.set(pe)
ProgressState.updateProgressState(pe, channel.terminal)
}
private[this] val executor =
Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-ui-thread"))
private[this] val lastProgressEvent = new AtomicReference[ProgressEvent]
private[this] val isClosed = new AtomicBoolean(false)
private[sbt] def reset(state: State): Unit = if (!isClosed.get) {
uiThread.synchronized {
val task = channel.makeUIThread(state)
def submit(): Thread = {
val thread = new Thread(() => {
task.run()
uiThread.set(null)
}, s"sbt-$name-ui-thread")
thread.setDaemon(true)
thread.start()
uiThread.getAndSet((task, thread)) match {
case null =>
case (_, t) => t.interrupt()
}
thread
}
uiThread.get match {
case null => uiThread.set((task, submit()))
case (t, _) if t.getClass == task.getClass =>
case (t, thread) =>
thread.interrupt()
uiThread.set((task, submit()))
}
}
Option(lastProgressEvent.get).foreach(onProgressEvent)
}
private[sbt] def stopThread(): Unit = uiThread.synchronized {
uiThread.getAndSet(null) match {
case null =>
case (t, thread) =>
t.close()
Util.ignoreResult(thread.interrupt())
}
}
private[sbt] def onConsolePromptEvent(consolePromptEvent: ConsolePromptEvent): Unit = {
channel.terminal.withPrintStream { ps =>
ps.print(ConsoleAppender.ClearScreenAfterCursor)
ps.flush()
}
val state = consolePromptEvent.state
terminal.prompt match {
case Running => terminal.setPrompt(AskUser(() => UITask.shellPrompt(terminal, state)))
case _ =>
}
onProgressEvent(ProgressEvent("Info", Vector(), None, None, None))
reset(state)
}
private[sbt] def onConsoleUnpromptEvent(
consoleUnpromptEvent: ConsoleUnpromptEvent
): Unit = {
if (consoleUnpromptEvent.lastSource.fold(true)(_.channelName != name)) {
terminal.progressState.reset()
} else stopThread()
}
override def close(): Unit = if (isClosed.compareAndSet(false, true)) executor.shutdown()
private def terminal = channel.terminal
private def name: String = channel.name
}

View File

@ -0,0 +1,99 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import java.io.InputStream
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicBoolean
import scala.util.Try
private[sbt] object ReadJsonFromInputStream {
def apply(
inputStream: InputStream,
running: AtomicBoolean,
onHeader: Option[String => Unit]
): Seq[Byte] = {
val newline = '\n'.toInt
val carriageReturn = '\r'.toInt
val contentLength = "Content-Length: "
var index = 0
/*
* This is the buffer into which we copy headers. The value of 128 bytes is
* somewhat arbitrary but chosen to ensure that there is enough space
* for any reasonable header. Any header exceeding 128 bytes will
* be truncated. The only header we care about at the moment is
* content-length, so this should be fine. If we ever start doing anything
* with headers, we may need to adjust this buffer size.
*/
val headerBuffer = new Array[Byte](128)
def getLine(): String = {
val line = new String(headerBuffer, 0, index, "UTF-8")
index = 0
onHeader.foreach(oh => oh(line))
line
}
var content: Seq[Byte] = Seq.empty[Byte]
var consecutiveLineEndings = 0
var onCarriageReturn = false
do {
val byte = inputStream.read
byte match {
case `newline` =>
val line = getLine()
if (onCarriageReturn) consecutiveLineEndings += 1
onCarriageReturn = false
if (line.startsWith(contentLength)) {
Try(line.drop(contentLength.length).toInt) foreach { len =>
def drainHeaders(): Unit =
do {
inputStream.read match {
case `newline` if onCarriageReturn =>
getLine()
onCarriageReturn = false
consecutiveLineEndings += 1
case `carriageReturn` => onCarriageReturn = true
case -1 => running.set(false)
case c =>
if (c == newline) getLine()
else {
if (index < headerBuffer.length) headerBuffer(index) = c.toByte
index += 1
}
onCarriageReturn = false
consecutiveLineEndings = 0
}
} while (consecutiveLineEndings < 2 && running.get)
drainHeaders()
if (running.get) {
val buf = new Array[Byte](len)
var offset = 0
do {
offset += inputStream.read(buf, offset, len - offset)
} while (offset < len && running.get)
if (running.get) content = buf.toSeq
}
}
} else if (line.startsWith("{")) {
// Assume this is a json object with no headers
content = line.getBytes.toSeq
}
case i if i < 0 =>
running.set(false)
throw new ClosedChannelException
case `carriageReturn` => onCarriageReturn = true
case c =>
onCarriageReturn = false
if (index < headerBuffer.length) headerBuffer(index) = c.toByte
index += 1
}
} while (content.isEmpty && running.get)
content
}
}

View File

@ -170,12 +170,11 @@ object Def extends Init[Scope] with TaskMacroExtra with InitializeImplicits {
def displayMasked(scoped: ScopedKey[_], mask: ScopeMask, showZeroConfig: Boolean): String =
Scope.displayMasked(scoped.scope, scoped.key.label, mask, showZeroConfig)
def withColor(s: String, color: Option[String]): String = {
val useColor = ConsoleAppender.formatEnabledInEnv
color match {
case Some(c) if useColor => c + s + scala.Console.RESET
case _ => s
}
def withColor(s: String, color: Option[String]): String =
withColor(s, color, useColor = ConsoleAppender.formatEnabledInEnv)
def withColor(s: String, color: Option[String], useColor: Boolean): String = color match {
case Some(c) if useColor => c + s + scala.Console.RESET
case _ => s
}
override def deriveAllowed[T](s: Setting[T], allowDynamic: Boolean): Option[String] =

View File

@ -60,17 +60,21 @@ public final class MetaBuildLoader extends URLClassLoader {
* library.
*/
public static MetaBuildLoader makeLoader(final AppProvider appProvider) throws IOException {
final Pattern pattern = Pattern.compile("test-interface-[0-9.]+\\.jar");
final Pattern pattern =
Pattern.compile("(test-interface-[0-9.]+|jline-[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar");
final File[] cp = appProvider.mainClasspath();
final URL[] interfaceURL = new URL[1];
final URL[] interfaceURLs = 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;
for (final File file : cp) {
if (pattern.matcher(file.getName()).find()) {
interfaceURL[0] = file.toURI().toURL();
final String name = file.getName();
if (pattern.matcher(name).find()) {
interfaceURLs[interfaceIndex] = file.toURI().toURL();
interfaceIndex += 1;
} else {
bottomClasspath.add(file);
}
@ -88,11 +92,29 @@ public final class MetaBuildLoader extends URLClassLoader {
}
}
final ScalaProvider scalaProvider = appProvider.scalaProvider();
final ClassLoader topLoader = scalaProvider.launcher().topLoader();
final TestInterfaceLoader interfaceLoader = new TestInterfaceLoader(interfaceURL, topLoader);
ClassLoader topLoader = scalaProvider.launcher().topLoader();
boolean foundSBTLoader = false;
while (!foundSBTLoader && topLoader != null) {
if (topLoader instanceof URLClassLoader) {
for (final URL u : ((URLClassLoader) topLoader).getURLs()) {
if (u.toString().contains("test-interface")) {
topLoader = topLoader.getParent();
foundSBTLoader = true;
}
}
}
if (!foundSBTLoader) topLoader = topLoader.getParent();
}
if (topLoader == null) topLoader = scalaProvider.launcher().topLoader();
final TestInterfaceLoader interfaceLoader = new TestInterfaceLoader(interfaceURLs, topLoader);
final File[] siJars = scalaProvider.jars();
final URL[] lib = new URL[1];
final URL[] scalaRest = new URL[Math.max(0, siJars.length - 1)];
int scalaRestCount = siJars.length - 1;
for (final File file : siJars) {
if (pattern.matcher(file.getName()).find()) scalaRestCount -= 1;
}
final URL[] scalaRest = new URL[Math.max(0, scalaRestCount)];
{
int i = 0;
@ -101,7 +123,7 @@ public final class MetaBuildLoader extends URLClassLoader {
final File file = siJars[i];
if (file.getName().equals("scala-library.jar")) {
lib[0] = file.toURI().toURL();
} else {
} else if (!pattern.matcher(file.getName()).find()) {
scalaRest[j] = file.toURI().toURL();
j += 1;
}

View File

@ -27,9 +27,9 @@ trait CommandLineUIService extends InteractionService {
}
}
override def terminalWidth: Int = Terminal.getWidth
override def terminalWidth: Int = Terminal.get.getWidth
override def terminalHeight: Int = Terminal.getHeight
override def terminalHeight: Int = Terminal.get.getHeight
}
object CommandLineUIService extends CommandLineUIService

View File

@ -17,6 +17,7 @@ import lmcoursier.CoursierDependencyResolution
import lmcoursier.definitions.{ Configuration => CConfiguration }
import org.apache.ivy.core.module.descriptor.ModuleDescriptor
import org.apache.ivy.core.module.id.ModuleRevisionId
import org.scalasbt.ipcsocket.Win32SecurityLevel
import sbt.Def.{ Initialize, ScopedKey, Setting, SettingsDefinition }
import sbt.Keys._
import sbt.Project.{
@ -48,7 +49,8 @@ import sbt.internal.server.{
BuildServerReporter,
Definition,
LanguageServerProtocol,
ServerHandler
ServerHandler,
VirtualTerminal,
}
import sbt.internal.testing.TestLogger
import sbt.internal.util.Attributed.data
@ -208,7 +210,8 @@ object Defaults extends BuildCommon {
Seq(
LanguageServerProtocol.handler(fileConverter.value),
BuildServerProtocol
.handler(sbtVersion.value, semanticdbEnabled.value, semanticdbVersion.value)
.handler(sbtVersion.value, semanticdbEnabled.value, semanticdbVersion.value),
VirtualTerminal.handler,
) ++ serverHandlers.value :+ ServerHandler.fallback
},
uncachedStamper := Stamps.uncachedStamps(fileConverter.value),
@ -342,15 +345,12 @@ object Defaults extends BuildCommon {
() => Clean.deleteContents(tempDirectory, _ => false)
},
turbo :== SysProp.turbo,
useSuperShell := { if (insideCI.value) false else SysProp.supershell },
useSuperShell := { if (insideCI.value) false else Terminal.console.isSupershellEnabled },
progressReports := {
val rs = EvaluateTask.taskTimingProgress.toVector ++ EvaluateTask.taskTraceEvent.toVector
rs map { Keys.TaskProgress(_) }
},
progressState := {
if ((ThisBuild / useSuperShell).value) Some(new ProgressState(SysProp.supershellBlankZone))
else None
},
progressState := Some(new ProgressState(SysProp.supershellBlankZone)),
Previous.cache := new Previous(
Def.streamsManagerKey.value,
Previous.references.value.getReferences
@ -378,6 +378,7 @@ object Defaults extends BuildCommon {
interactionService :== CommandLineUIService,
autoStartServer := true,
serverHost := "127.0.0.1",
serverIdleTimeout := Some(new FiniteDuration(7, TimeUnit.DAYS)),
serverPort := 5000 + (Hash
.toHex(Hash(appConfiguration.value.baseDirectory.toString))
.## % 1000),
@ -387,6 +388,7 @@ object Defaults extends BuildCommon {
else Set()
},
serverHandlers :== Nil,
windowsServerSecurityLevel := Win32SecurityLevel.OWNER_DACL, // allows any owner logon session to access the server
fullServerHandlers := Nil,
insideCI :== sys.env.contains("BUILD_NUMBER") ||
sys.env.contains("CI") || SysProp.ci,
@ -408,7 +410,7 @@ object Defaults extends BuildCommon {
// TODO: This should be on the new default settings for a project.
def projectCore: Seq[Setting[_]] = Seq(
name := thisProject.value.id,
logManager := LogManager.defaults(extraLoggers.value, StandardMain.console),
logManager := LogManager.defaults(extraLoggers.value, ConsoleOut.terminalOut),
onLoadMessage := (onLoadMessage or
Def.setting {
s"set current project to ${name.value} (in build ${thisProjectRef.value.build})"
@ -1496,13 +1498,13 @@ object Defaults extends BuildCommon {
def askForMainClass(classes: Seq[String]): Option[String] =
sbt.SelectMainClass(
if (classes.length >= 10) Some(SimpleReader.readLine(_))
if (classes.length >= 10) Some(SimpleReader(Terminal.get).readLine(_))
else
Some(s => {
def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() }
print(s)
Terminal.withRawSystemIn {
Terminal.wrappedSystemIn.read match {
Terminal.get.withRawSystemIn {
Terminal.get.inputStream.read match {
case -1 => None
case b =>
val res = b.toChar.toString
@ -2344,6 +2346,9 @@ object Classpaths {
CrossVersion(scalaVersion, binVersion)(base).withCrossVersion(Disabled())
},
shellPrompt := shellPromptFromState,
terminalShellPrompt := { (t, s) =>
shellPromptFromState(t)(s)
},
dynamicDependency := { (): Unit },
transitiveClasspathDependency := { (): Unit },
transitiveDynamicInputs :== Nil,
@ -3827,11 +3832,13 @@ object Classpaths {
}
}
def shellPromptFromState: State => String = { s: State =>
def shellPromptFromState: State => String = shellPromptFromState(Terminal.console)
def shellPromptFromState(terminal: Terminal): State => String = { s: State =>
val extracted = Project.extract(s)
(name in extracted.currentRef).get(extracted.structure.data) match {
case Some(name) => s"sbt:$name" + Def.withColor("> ", Option(scala.Console.CYAN))
case _ => "> "
case Some(name) =>
s"sbt:$name" + Def.withColor(s"> ", Option(scala.Console.CYAN), terminal.isColorEnabled)
case _ => "> "
}
}
}

View File

@ -255,17 +255,7 @@ object EvaluateTask {
extracted,
structure
)
val progressReporter = extracted.getOpt(progressState in ThisBuild).flatMap {
case Some(ps) =>
ps.reset()
ConsoleAppender.setShowProgress(true)
val appender = MainAppender.defaultScreen(StandardMain.console)
ProgressState.set(ps)
val log = LogManager.progressLogger(appender)
Some(new TaskProgress(log))
case _ => None
}
val reporters = maker.map(_.progress) ++ progressReporter ++
val reporters = maker.map(_.progress) ++ Some(TaskProgress) ++
(if (SysProp.taskTimings)
new TaskTimings(reportOnShutdown = false, state.globalLogging.full) :: Nil
else Nil)

View File

@ -89,11 +89,14 @@ object Keys {
// Command keys
val historyPath = SettingKey(BasicKeys.historyPath)
val shellPrompt = SettingKey(BasicKeys.shellPrompt)
val terminalShellPrompt = SettingKey(BasicKeys.terminalShellPrompt)
val autoStartServer = SettingKey(BasicKeys.autoStartServer)
val serverPort = SettingKey(BasicKeys.serverPort)
val serverHost = SettingKey(BasicKeys.serverHost)
val serverAuthentication = SettingKey(BasicKeys.serverAuthentication)
val serverConnectionType = SettingKey(BasicKeys.serverConnectionType)
val serverIdleTimeout = SettingKey(BasicKeys.serverIdleTimeout)
val windowsServerSecurityLevel = SettingKey(BasicKeys.windowsServerSecurityLevel)
val fullServerHandlers = SettingKey(BasicKeys.fullServerHandlers)
val serverHandlers = settingKey[Seq[ServerHandler]]("User-defined server handlers.")
@ -546,6 +549,7 @@ object Keys {
val globalPluginUpdate = taskKey[UpdateReport]("A hook to get the UpdateReport of the global plugin.").withRank(DTask)
private[sbt] val taskCancelStrategy = settingKey[State => TaskCancellationStrategy]("Experimental task cancellation handler.").withRank(DTask)
private[sbt] val cacheStoreFactoryFactory = AttributeKey[CacheStoreFactoryFactory]("cache-store-factory-factory")
private[sbt] val bootServerSocket = AttributeKey[BootServerSocket]("boot-server-socket")
val fileCacheSize = settingKey[String]("The approximate maximum size in bytes of the cache used to store previous task results. For example, it could be set to \"256M\" to make the maximum size 256 megabytes.")
// Experimental in sbt 0.13.2 to enable grabbing semantic compile failures.

View File

@ -9,11 +9,13 @@ package sbt
import java.io.{ File, IOException }
import java.net.URI
import java.nio.channels.ClosedChannelException
import java.nio.file.{ FileAlreadyExistsException, FileSystems, Files }
import java.util.Properties
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.atomic.AtomicBoolean
import sbt.BasicCommandStrings.{ Shell, TemplateCommand }
import sbt.BasicCommandStrings.{ SetTerminal, Shell, Shutdown, TemplateCommand, networkExecPrefix }
import sbt.Project.LoadAction
import sbt.compiler.EvalImports
import sbt.internal.Aggregation.AnyKeys
@ -21,7 +23,9 @@ import sbt.internal.CommandStrings.BootCommand
import sbt.internal._
import sbt.internal.client.BspClient
import sbt.internal.inc.ScalaInstance
import sbt.internal.nio.CheckBuildSources
import sbt.internal.io.Retry
import sbt.internal.nio.{ CheckBuildSources, FileTreeRepository }
import sbt.internal.server.NetworkChannel
import sbt.internal.util.Types.{ const, idFun }
import sbt.internal.util._
import sbt.internal.util.complete.{ Parser, SizeParser }
@ -32,7 +36,9 @@ import xsbti.compile.CompilerCache
import scala.annotation.tailrec
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.util.control.NonFatal
import sbt.internal.io.Retry
import xsbti.AppProvider
/** This class is the entry point for sbt. */
@ -51,7 +57,7 @@ private[sbt] object xMain {
override def provider: AppProvider = config.provider()
}
}
private[sbt] def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
private[sbt] def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
try {
import BasicCommandStrings.{ DashClient, DashDashClient, runEarly }
import BasicCommands.early
@ -59,6 +65,10 @@ private[sbt] object xMain {
import sbt.internal.CommandStrings.{ BootCommand, DefaultsCommand, InitCommand }
import sbt.internal.client.NetworkClient
val bootServerSocket = getSocketOrExit(configuration) match {
case (_, Some(e)) => return e
case (s, _) => s
}
// if we detect -Dsbt.client=true or -client, run thin client.
val clientModByEnv = SysProp.client
val userCommands = configuration.arguments.map(_.trim)
@ -67,24 +77,52 @@ private[sbt] object xMain {
if (userCommands.exists(isBsp)) {
BspClient.run(dealiasBaseDirectory(configuration))
} else {
bootServerSocket.foreach(l => Terminal.setBootStreams(l.inputStream, l.outputStream))
Terminal.withStreams {
if (clientModByEnv || userCommands.exists(isClient)) {
val args = userCommands.toList.filterNot(isClient)
NetworkClient.run(dealiasBaseDirectory(configuration), args)
Exit(0)
} else {
val state = StandardMain.initialState(
dealiasBaseDirectory(configuration),
Seq(defaults, early),
runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil
)
StandardMain.runManaged(state)
val closeStreams = userCommands.exists(_ == BasicCommandStrings.CloseIOStreams)
val state0 = StandardMain
.initialState(
dealiasBaseDirectory(configuration),
Seq(defaults, early),
runEarly(DefaultsCommand) :: runEarly(InitCommand) :: BootCommand :: Nil
)
.put(BasicKeys.closeIOStreams, closeStreams)
val state = bootServerSocket match {
case Some(l) => state0.put(Keys.bootServerSocket, l)
case _ => state0
}
try StandardMain.runManaged(state)
finally bootServerSocket.foreach(_.close())
}
}
}
} finally {
ShutdownHooks.close()
}
}
private def getSocketOrExit(
configuration: xsbti.AppConfiguration
): (Option[BootServerSocket], Option[Exit]) =
try (Some(new BootServerSocket(configuration)) -> None)
catch {
case _: ServerAlreadyBootingException
if System.console != null && !Terminal.startedByRemoteClient =>
println("sbt server is already booting. Create a new server? y/n (default y)")
val exit = Terminal.get.withRawSystemIn(System.in.read) match {
case 110 => Some(Exit(1))
case _ => None
}
(None, exit)
case _: ServerAlreadyBootingException =>
if (SysProp.forceServerStart) (None, None)
else (None, Some(Exit(2)))
}
}
final class ScriptMain extends xsbti.AppMain {
@ -139,30 +177,40 @@ object StandardMain {
pool.foreach(_.shutdownNow())
}
private[this] val isShutdown = new AtomicBoolean(false)
def runManaged(s: State): xsbti.MainResult = {
val previous = TrapExit.installManager()
try {
val hook = ShutdownHooks.add(closeRunnable)
try {
MainLoop.runLogged(s)
} catch {
case _: InterruptedException if isShutdown.get =>
new xsbti.Exit { override def code(): Int = 0 }
} finally {
try DefaultBackgroundJobService.shutdown()
finally hook.close()
()
}
} finally TrapExit.uninstallManager(previous)
} finally {
TrapExit.uninstallManager(previous)
}
}
/** The common interface to standard output, used for all built-in ConsoleLoggers. */
val console: ConsoleOut =
ConsoleOut.systemOutOverwrite(ConsoleOut.overwriteContaining("Resolving "))
ConsoleOut.setGlobalProxy(console)
private[this] def initialGlobalLogging(file: Option[File]): GlobalLogging = {
file.foreach(f => if (!f.exists()) IO.createDirectory(f))
def createTemp(attempt: Int = 0): File = Retry {
file.foreach(f => if (!f.exists()) IO.createDirectory(f))
File.createTempFile("sbt-global-log", ".log", file.orNull)
}
GlobalLogging.initial(
MainAppender.globalDefault(console),
File.createTempFile("sbt-global-log", ".log", file.orNull),
console
MainAppender.globalDefault(ConsoleOut.globalProxy),
createTemp(),
ConsoleOut.globalProxy
)
}
def initialGlobalLogging(file: File): GlobalLogging = initialGlobalLogging(Option(file))
@ -178,7 +226,8 @@ object StandardMain {
sys.props.put("jna.nosys", "true")
import BasicCommandStrings.isEarlyCommand
val userCommands = configuration.arguments.map(_.trim)
val userCommands =
configuration.arguments.map(_.trim).filterNot(_ == BasicCommandStrings.CloseIOStreams)
val (earlyCommands, normalCommands) = (preCommands ++ userCommands).partition(isEarlyCommand)
val commands = (earlyCommands ++ normalCommands).toList map { x =>
Exec(x, None)
@ -248,6 +297,7 @@ object BuiltinCommands {
skipBanner,
notifyUsersAboutShell,
shell,
rebootNetwork,
startServer,
eval,
last,
@ -259,7 +309,11 @@ object BuiltinCommands {
act,
continuous,
clearCaches,
) ++ allBasicCommands
NetworkChannel.disconnect,
waitCmd,
promptChannel,
setTerminalCommand,
) ++ allBasicCommands ++ ContinuousCommands.value
def DefaultBootCommands: Seq[String] =
WriteSbtVersion :: LoadProject :: NotifyUsersAboutShell :: s"$IfLast $Shell" :: Nil
@ -781,12 +835,10 @@ object BuiltinCommands {
@tailrec
private[this] def doLoadFailed(s: State, loadArg: String): State = {
s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)")
val result = Terminal.withRawSystemIn {
Terminal.withEcho(toggle = true)(Terminal.wrappedSystemIn.read() match {
case -1 => 'q'.toInt
case b => b
})
}
val result = try Terminal.get.withRawSystemIn(System.in.read) match {
case -1 => 'q'.toInt
case b => b
} catch { case _: ClosedChannelException => 'q' }
def retry: State = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog
def ignoreMsg: String =
if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded"
@ -880,10 +932,14 @@ object BuiltinCommands {
val session = Load.initialSession(structure, eval, s0)
SessionSettings.checkSession(session, s2)
val s3 = addCacheStoreFactoryFactory(Project.setProject(session, structure, s2))
val s4 = LintUnused.lintUnusedFunc(s3)
CheckBuildSources.init(s4)
val s4 = setupGlobalFileTreeRepository(s3)
CheckBuildSources.init(LintUnused.lintUnusedFunc(s4))
}
private val setupGlobalFileTreeRepository: State => State = { state =>
state.get(sbt.nio.Keys.globalFileTreeRepository).foreach(_.close())
state.put(sbt.nio.Keys.globalFileTreeRepository, FileTreeRepository.default)
}
private val addCacheStoreFactoryFactory: State => State = (s: State) => {
val size = Project
.extract(s)
@ -910,30 +966,93 @@ object BuiltinCommands {
Command.command(ClearCaches, help)(f)
}
def setTerminalCommand = Command.arb(_ => BasicCommands.reportParser(SetTerminal)) {
(s, channel) =>
StandardMain.exchange.channelForName(channel).foreach(c => Terminal.set(c.terminal))
s
}
private[sbt] def waitCmd: Command =
Command.arb(_ => (ContinuousCommands.waitWatch: Parser[String]).examples()) { (s0, _) =>
val exchange = StandardMain.exchange
if (exchange.channels.exists(ContinuousCommands.isInWatch)) {
val s1 = exchange.run(s0)
exchange.channels.foreach {
case c if ContinuousCommands.isPending(c) =>
case c => c.prompt(ConsolePromptEvent(s1))
}
val exec: Exec = getExec(s1, Duration.Inf)
val remaining: List[Exec] =
Exec(ContinuousCommands.waitWatch, None) ::
Exec(FailureWall, None) :: s1.remainingCommands
val newState = s1.copy(remainingCommands = exec +: remaining)
if (exec.commandLine.trim.isEmpty) newState
else newState.clearGlobalLog
} else s0
}
private[sbt] def promptChannel = Command.arb(_ => reportParser(PromptChannel)) {
(state, channel) =>
if (channel == ConsoleChannel.defaultName) {
if (!state.remainingCommands.exists(_.commandLine == Shell))
state.copy(remainingCommands = state.remainingCommands ::: (Exec(Shell, None) :: Nil))
else state
} else {
StandardMain.exchange.channelForName(channel) match {
case Some(nc: NetworkChannel) => nc.prompt()
case _ =>
}
state
}
}
private def getExec(state: State, interval: Duration): Exec = {
val exec: Exec =
StandardMain.exchange.blockUntilNextExec(interval, Some(state), state.globalLogging.full)
if (exec.source.fold(true)(_.channelName != ConsoleChannel.defaultName) &&
!exec.commandLine.startsWith(networkExecPrefix)) {
Terminal.consoleLog(s"received remote command: ${exec.commandLine}")
}
exec
}
def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 =>
import sbt.internal.ConsolePromptEvent
val exchange = StandardMain.exchange
val welcomeState = displayWelcomeBanner(s0)
val s1 = exchange run welcomeState
exchange prompt ConsolePromptEvent(s0)
val minGCInterval = Project
.extract(s1)
.getOpt(Keys.minForcegcInterval)
.getOrElse(GCUtil.defaultMinForcegcInterval)
val exec: Exec = exchange.blockUntilNextExec(minGCInterval, s1.globalLogging.full)
if (exec.source.fold(true)(_.channelName != "console0")) {
s1.log.info(s"received remote command: ${exec.commandLine}")
/*
* It is possible for sbt processes to leak if two are started simultaneously
* by a remote client and only one is able to start a server. This seems to
* happen primarily on windows.
*/
if (Terminal.startedByRemoteClient && !exchange.hasServer) {
Exec(Shutdown, None) +: s1
} else {
exchange prompt ConsolePromptEvent(s0)
val minGCInterval = Project
.extract(s1)
.getOpt(Keys.minForcegcInterval)
.getOrElse(GCUtil.defaultMinForcegcInterval)
val exec: Exec = getExec(s1, minGCInterval)
val newState = s1
.copy(
onFailure = Some(Exec(Shell, None)),
remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands
)
.setInteractive(true)
val res =
if (exec.commandLine.trim.isEmpty) newState
else newState.clearGlobalLog
res
}
val newState = s1
.copy(
onFailure = Some(Exec(Shell, None)),
remainingCommands = exec +: Exec(Shell, None) +: s1.remainingCommands
)
.setInteractive(true)
if (exec.commandLine.trim.isEmpty) newState
else newState.clearGlobalLog
}
def rebootNetwork: Command = Command.arb(_ => (RebootNetwork: Parser[String]).examples()) {
(s, _) =>
StandardMain.exchange.reboot(s)
s
}
def startServer: Command =
Command.command(StartServer, Help.more(StartServer, StartServerDetailed)) { s0 =>
val exchange = StandardMain.exchange
@ -1003,10 +1122,12 @@ object BuiltinCommands {
private def intendsToInvokeCompile(state: State) =
state.remainingCommands exists (_.commandLine == Keys.compile.key.label)
private def hasRebooted(state: State) =
state.remainingCommands exists (_.commandLine == StartServer)
private def notifyUsersAboutShell(state: State): Unit = {
val suppress = Project extract state getOpt Keys.suppressSbtShellNotification getOrElse false
if (!suppress && intendsToInvokeCompile(state))
if (!suppress && intendsToInvokeCompile(state) && !hasRebooted(state))
state.log info "Executing in batch mode. For better performance use sbt's shell"
}

View File

@ -10,11 +10,13 @@ package sbt
import java.io.PrintWriter
import java.util.Properties
import sbt.BasicCommandStrings.{ SetTerminal, StashOnFailure, networkExecPrefix }
import sbt.internal.ShutdownHooks
import sbt.internal.langserver.ErrorCodes
import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.nio.CheckBuildSources.CheckBuildSourcesKey
import sbt.internal.util.{ ErrorHandling, GlobalLogBacking, Terminal }
import sbt.internal.{ ConsoleUnpromptEvent, ShutdownHooks }
import sbt.io.{ IO, Using }
import sbt.protocol._
import sbt.util.Logger
@ -195,22 +197,34 @@ object MainLoop {
state.put(sbt.Keys.currentTaskProgress, new Keys.TaskProgress(progress))
} else state
}
StandardMain.exchange.setState(progressState)
StandardMain.exchange.setExec(Some(exec))
StandardMain.exchange.unprompt(ConsoleUnpromptEvent(exec.source))
val newState = Command.process(exec.commandLine, progressState)
val doneEvent = ExecStatusEvent(
"Done",
channelName,
exec.execId,
newState.remainingCommands.toVector map (_.commandLine),
exitCode(newState, state),
)
StandardMain.exchange.respondStatus(doneEvent)
if (exec.execId.fold(true)(!_.startsWith(networkExecPrefix)) &&
!exec.commandLine.startsWith(networkExecPrefix)) {
val doneEvent = ExecStatusEvent(
"Done",
channelName,
exec.execId,
newState.remainingCommands.toVector map (_.commandLine),
exitCode(newState, state),
)
StandardMain.exchange.respondStatus(doneEvent)
}
StandardMain.exchange.setExec(None)
newState.get(sbt.Keys.currentTaskProgress).foreach(_.progress.stop())
newState.remove(sbt.Keys.currentTaskProgress)
}
state.get(CheckBuildSourcesKey) match {
case Some(cbs) =>
if (!cbs.needsReload(state, exec.commandLine)) process()
else Exec("reload", None, None) +: exec +: state.remove(CheckBuildSourcesKey)
else {
if (exec.commandLine.startsWith(SetTerminal))
exec +: Exec("reload", None, None) +: state.remove(CheckBuildSourcesKey)
else
Exec("reload", None, None) +: exec +: state.remove(CheckBuildSourcesKey)
}
case _ => process()
}
} catch {
@ -271,7 +285,8 @@ object MainLoop {
// it's handled by executing the shell again, instead of the state failing
// so we also use that to indicate that the execution failed
private[this] def exitCodeFromStateOnFailure(state: State, prevState: State): ExitCode =
if (prevState.onFailure.isDefined && state.onFailure.isEmpty) ExitCode(ErrorCodes.UnknownError)
if (prevState.onFailure.isDefined && state.onFailure.isEmpty &&
state.currentCommand.fold(true)(_ != StashOnFailure)) ExitCode(ErrorCodes.UnknownError)
else ExitCode.Success
}

View File

@ -19,16 +19,19 @@ import Keys.{
historyPath,
projectCommand,
sessionSettings,
terminalShellPrompt,
shellPrompt,
templateResolverInfos,
autoStartServer,
serverHost,
serverIdleTimeout,
serverLog,
serverPort,
serverAuthentication,
serverConnectionType,
fullServerHandlers,
logLevel,
windowsServerSecurityLevel,
}
import Scope.{ Global, ThisScope }
import Def.{ Flattened, Initialize, ScopedKey, Setting }
@ -50,6 +53,7 @@ import sbt.util.{ Show, Level }
import sjsonnew.JsonFormat
import language.experimental.macros
import scala.concurrent.duration.FiniteDuration
sealed trait ProjectDefinition[PR <: ProjectReference] {
@ -508,10 +512,12 @@ object Project extends ProjectExtra {
val allCommands = commandsIn(ref) ++ commandsIn(BuildRef(ref.build)) ++ (commands in Global get structure.data toList)
val history = get(historyPath) flatMap idFun
val prompt = get(shellPrompt)
val newPrompt = get(terminalShellPrompt)
val trs = (templateResolverInfos in Global get structure.data).toList.flatten
val startSvr: Option[Boolean] = get(autoStartServer)
val host: Option[String] = get(serverHost)
val port: Option[Int] = get(serverPort)
val timeout: Option[Option[FiniteDuration]] = get(serverIdleTimeout)
val authentication: Option[Set[ServerAuthentication]] = get(serverAuthentication)
val connectionType: Option[ConnectionType] = get(serverConnectionType)
val srvLogLevel: Option[Level.Value] = (logLevel in (ref, serverLog)).get(structure.data)
@ -521,17 +527,21 @@ object Project extends ProjectExtra {
s.definedCommands,
projectCommand
)
val winSecurityLevel = get(windowsServerSecurityLevel).getOrElse(2)
val newAttrs =
s.attributes
.put(historyPath.key, history)
.put(windowsServerSecurityLevel.key, winSecurityLevel)
.setCond(autoStartServer.key, startSvr)
.setCond(serverPort.key, port)
.setCond(serverHost.key, host)
.setCond(serverAuthentication.key, authentication)
.setCond(serverConnectionType.key, connectionType)
.setCond(serverIdleTimeout.key, timeout)
.put(historyPath.key, history)
.put(templateResolverInfos.key, trs)
.setCond(shellPrompt.key, prompt)
.setCond(terminalShellPrompt.key, newPrompt)
.setCond(serverLogLevel, srvLogLevel)
.setCond(fullServerHandlers.key, hs)
s.copy(

View File

@ -11,6 +11,7 @@ import java.lang.reflect.InvocationTargetException
import java.nio.file.Path
import java.io.File
import sbt.BasicCommandStrings.TerminateAction
import sbt.io._, syntax._
import sbt.util._
import sbt.internal.util.complete.{ DefaultParsers, Parser }, DefaultParsers._
@ -54,7 +55,7 @@ private[sbt] object TemplateCommandUtil {
case xs => xs map (_.commandLine)
})
run(infos, arguments, state.configuration, ivyConf, globalBase, scalaModuleInfo, log)
"exit" :: s2.copy(remainingCommands = Nil)
TerminateAction :: s2.copy(remainingCommands = Nil)
}
private def run(

View File

@ -6,23 +6,31 @@
*/
package sbt
package internal
package internal
import java.io.IOException
import java.net.Socket
import java.util.concurrent.{ ConcurrentLinkedQueue, LinkedBlockingQueue, TimeUnit }
import java.util.concurrent.atomic._
import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit }
import sbt.BasicCommandStrings.{
Cancel,
CompleteExec,
Shutdown,
TerminateAction,
networkExecPrefix
}
import sbt.BasicKeys._
import sbt.nio.Watch.NullLogger
import sbt.internal.protocol.JsonRpcResponseError
import sbt.internal.server._
import sbt.internal.util.{ ConsoleOut, MainAppender, ObjectEvent, Terminal }
import sbt.internal.ui.UITask
import sbt.internal.util._
import sbt.io.syntax._
import sbt.io.{ Hash, IO }
import sbt.nio.Watch.NullLogger
import sbt.protocol.{ ExecStatusEvent, LogEvent }
import sbt.util.{ Level, LogExchange, Logger }
import sjsonnew.JsonFormat
import sbt.util.Logger
import sbt.protocol.Serialization.attach
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
@ -30,6 +38,8 @@ import scala.concurrent.Await
import scala.concurrent.duration._
import scala.util.{ Failure, Success, Try }
import sjsonnew.JsonFormat
/**
* The command exchange merges multiple command channels (e.g. network and console),
* and acts as the central multiplexing point.
@ -42,76 +52,133 @@ private[sbt] final class CommandExchange {
private var server: Option[ServerInstance] = None
private val firstInstance: AtomicBoolean = new AtomicBoolean(true)
private var consoleChannel: Option[ConsoleChannel] = None
private val commandQueue: ConcurrentLinkedQueue[Exec] = new ConcurrentLinkedQueue()
private val commandQueue: LinkedBlockingQueue[Exec] = new LinkedBlockingQueue[Exec]
private val channelBuffer: ListBuffer[CommandChannel] = new ListBuffer()
private val channelBufferLock = new AnyRef {}
private val commandChannelQueue = new LinkedBlockingQueue[CommandChannel]
private val fastTrackChannelQueue = new LinkedBlockingQueue[FastTrackTask]
private val nextChannelId: AtomicInteger = new AtomicInteger(0)
private[this] val activePrompt = new AtomicBoolean(false)
private[this] val lastState = new AtomicReference[State]
private[this] val currentExecRef = new AtomicReference[Exec]
private[sbt] def hasServer = server.isDefined
def channels: List[CommandChannel] = channelBuffer.toList
private[this] def removeChannel(channel: CommandChannel): Unit = {
channelBufferLock.synchronized {
channelBuffer -= channel
()
}
}
def subscribe(c: CommandChannel): Unit = channelBufferLock.synchronized {
channelBuffer.append(c)
c.register(commandChannelQueue)
c.register(commandQueue, fastTrackChannelQueue)
}
private[sbt] def withState[T](f: State => T): T = f(lastState.get)
def blockUntilNextExec: Exec = blockUntilNextExec(Duration.Inf, NullLogger)
// periodically move all messages from all the channels
private[sbt] def blockUntilNextExec(interval: Duration, logger: Logger): Exec = {
@tailrec def impl(deadline: Option[Deadline]): Exec = {
@tailrec def slurpMessages(): Unit =
channels.foldLeft(Option.empty[Exec]) { _ orElse _.poll } match {
case None => ()
case Some(x) =>
commandQueue.add(x)
slurpMessages()
private[sbt] def blockUntilNextExec(interval: Duration, logger: Logger): Exec =
blockUntilNextExec(interval, None, logger)
private[sbt] def blockUntilNextExec(
interval: Duration,
state: Option[State],
logger: Logger
): Exec = {
val idleDeadline = state.flatMap { s =>
lastState.set(s)
s.get(BasicKeys.serverIdleTimeout) match {
case Some(Some(d)) => Some(d.fromNow)
case _ => None
}
}
@tailrec def impl(gcDeadline: Option[Deadline], idleDeadline: Option[Deadline]): Exec = {
state.foreach(s => prompt(ConsolePromptEvent(s)))
def poll: Option[Exec] = {
val deadline = gcDeadline.toSeq ++ idleDeadline match {
case s @ Seq(_, _) => Some(s.min)
case s => s.headOption
}
commandChannelQueue.poll(1, TimeUnit.SECONDS)
slurpMessages()
Option(commandQueue.poll) match {
case Some(exec) =>
val needFinish = needToFinishPromptLine()
if (exec.source.fold(needFinish)(s => needFinish && s.channelName != "console0"))
ConsoleOut.systemOut.println("")
exec
Option(deadline match {
case Some(d: Deadline) =>
commandQueue.poll(d.timeLeft.toMillis + 1, TimeUnit.MILLISECONDS) match {
case null if idleDeadline.fold(false)(_.isOverdue) =>
state.foreach { s =>
s.get(BasicKeys.serverIdleTimeout) match {
case Some(Some(d)) => s.log.info(s"sbt idle timeout of $d expired")
case _ =>
}
}
Exec(TerminateAction, Some(CommandSource(ConsoleChannel.defaultName)))
case x => x
}
case _ => commandQueue.take
})
}
poll match {
case Some(exec) if exec.source.fold(true)(s => channels.exists(_.name == s.channelName)) =>
exec.commandLine match {
case `TerminateAction`
if exec.source.fold(false)(_.channelName.startsWith("network")) =>
channels.collectFirst {
case c: NetworkChannel if exec.source.fold(false)(_.channelName == c.name) => c
} match {
case Some(c) if c.isAttached =>
c.shutdown(false)
impl(gcDeadline, idleDeadline)
case _ => exec
}
case _ => exec
}
case Some(e) => e
case None =>
val newDeadline = if (deadline.fold(false)(_.isOverdue())) {
val newDeadline = if (gcDeadline.fold(false)(_.isOverdue())) {
GCUtil.forceGcWithInterval(interval, logger)
None
} else deadline
impl(newDeadline)
} else gcDeadline
impl(newDeadline, idleDeadline)
}
}
// Do not manually run GC until the user has been idling for at least the min gc interval.
impl(interval match {
case d: FiniteDuration => Some(d.fromNow)
case _ => None
})
}, idleDeadline)
}
def run(s: State): State = {
private def addConsoleChannel(): Unit =
if (consoleChannel.isEmpty) {
val console0 = new ConsoleChannel("console0")
val name = ConsoleChannel.defaultName
val console0 = new ConsoleChannel(name, mkAskUser(name))
consoleChannel = Some(console0)
subscribe(console0)
}
val autoStartServerAttr = s get autoStartServer match {
case Some(bool) => bool
case None => true
}
if (autoStartServerSysProp && autoStartServerAttr) runServer(s)
def run(s: State): State = run(s, s.get(autoStartServer).getOrElse(true))
def run(s: State, autoStart: Boolean): State = {
if (autoStartServerSysProp && autoStart) runServer(s)
else s
}
private[sbt] def setState(s: State): Unit = lastState.set(s)
private def newNetworkName: String = s"network-${nextChannelId.incrementAndGet()}"
private[sbt] def removeChannel(c: CommandChannel): Unit = {
channelBufferLock.synchronized {
Util.ignoreResult(channelBuffer -= c)
}
commandQueue.removeIf(_.source.map(_.channelName) == Some(c.name))
currentExec.filter(_.source.map(_.channelName) == Some(c.name)).foreach { e =>
Util.ignoreResult(NetworkChannel.cancel(e.execId, e.execId.getOrElse("0")))
}
if (ContinuousCommands.isInWatch(c)) {
try commandQueue.put(Exec(s"${ContinuousCommands.stopWatch} ${c.name}", None))
catch { case _: InterruptedException => }
}
}
private[this] def mkAskUser(
name: String,
): (State, CommandChannel) => UITask = { (state, channel) =>
ContinuousCommands
.watchUITaskFor(channel)
.getOrElse(new UITask.AskUserTask(state, channel))
}
private[sbt] def currentExec = Option(currentExecRef.get)
/**
* Check if a server instance is running already, and start one if it isn't.
*/
@ -121,22 +188,23 @@ private[sbt] final class CommandExchange {
lazy val auth: Set[ServerAuthentication] =
s.get(serverAuthentication).getOrElse(Set(ServerAuthentication.Token))
lazy val connectionType = s.get(serverConnectionType).getOrElse(ConnectionType.Tcp)
lazy val level = s.get(serverLogLevel).orElse(s.get(logLevel)).getOrElse(Level.Warn)
lazy val handlers = s.get(fullServerHandlers).getOrElse(Nil)
lazy val win32Level = s.get(windowsServerSecurityLevel).getOrElse(2)
def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = {
val name = newNetworkName
if (needToFinishPromptLine()) ConsoleOut.systemOut.println("")
s.log.info(s"new client connected: $name")
val logger: Logger = {
val log = LogExchange.logger(name, None, None)
LogExchange.unbindLoggerAppenders(name)
val appender = MainAppender.defaultScreen(s.globalLogging.console)
LogExchange.bindLoggerAppenders(name, List(appender -> level))
log
}
Terminal.consoleLog(s"new client connected: $name")
val channel =
new NetworkChannel(name, socket, Project structure s, auth, instance, handlers, logger)
new NetworkChannel(
name,
socket,
Project structure s,
auth,
instance,
handlers,
s.log,
mkAskUser(name)
)
subscribe(channel)
}
if (server.isEmpty && firstInstance.get) {
@ -158,7 +226,8 @@ private[sbt] final class CommandExchange {
socketfile,
pipeName,
bspConnectionFile,
s.configuration
s.configuration,
win32Level,
)
val serverInstance = Server.start(connection, onIncomingSocket, s.log)
// don't throw exception when it times out
@ -183,12 +252,16 @@ private[sbt] final class CommandExchange {
server = None
firstInstance.set(false)
}
Terminal.setBootStreams(null, null)
if (s.get(BasicKeys.closeIOStreams).getOrElse(false)) Terminal.close()
s.get(Keys.bootServerSocket).foreach(_.close())
}
s
s.remove(Keys.bootServerSocket)
}
def shutdown(): Unit = {
channels foreach (_.shutdown())
fastTrackThread.close()
channels foreach (_.shutdown(true))
// interrupt and kill the thread
server.foreach(_.shutdown())
server = None
@ -235,11 +308,10 @@ private[sbt] final class CommandExchange {
// This is an interface to directly notify events.
private[sbt] def notifyEvent[A: JsonFormat](method: String, params: A): Unit = {
channels
.collect { case c: NetworkChannel => c }
.foreach {
tryTo(_.notifyEvent(method, params))
}
channels.foreach {
case c: NetworkChannel => tryTo(_.notifyEvent(method, params))(c)
case _ =>
}
}
private def tryTo(f: NetworkChannel => Unit)(
@ -266,37 +338,25 @@ private[sbt] final class CommandExchange {
tryTo(_.respondError(code, event.message.getOrElse(""), event.execId))(channel)
}
}
tryTo(_.respond(event, event.execId))(channel)
}
}
/**
* This publishes object events. The type information has been
* erased because it went through logging.
*/
private[sbt] def respondObjectEvent(event: ObjectEvent[_]): Unit = {
for {
source <- event.channelName
channel <- channels.collectFirst {
case c: NetworkChannel if c.name == source => c
}
} tryTo(_.respond(event))(channel)
}
private[sbt] def setExec(exec: Option[Exec]): Unit = currentExecRef.set(exec.orNull)
def prompt(event: ConsolePromptEvent): Unit = {
activePrompt.set(Terminal.systemInIsAttached)
channels
.collect { case c: ConsoleChannel => c }
.foreach { _.prompt(event) }
currentExecRef.set(null)
channels.foreach {
case c if ContinuousCommands.isInWatch(c) =>
case c => c.prompt(event)
}
}
def unprompt(event: ConsoleUnpromptEvent): Unit = channels.foreach(_.unprompt(event))
def logMessage(event: LogEvent): Unit = {
channels
.collect { case c: NetworkChannel => c }
.foreach {
tryTo(_.notifyEvent(event))
}
channels.foreach {
case c: NetworkChannel => tryTo(_.notifyEvent(event))(c)
case _ =>
}
}
def notifyStatus(event: ExecStatusEvent): Unit = {
@ -308,5 +368,110 @@ private[sbt] final class CommandExchange {
} tryTo(_.notifyEvent(event))(channel)
}
private[this] def needToFinishPromptLine(): Boolean = activePrompt.compareAndSet(true, false)
private[sbt] def killChannel(channel: String): Unit = {
channels.find(_.name == channel).foreach(_.shutdown(false))
}
private[sbt] def updateProgress(pe: ProgressEvent): Unit = {
val newPE = currentExec match {
case Some(e) if !e.commandLine.startsWith(networkExecPrefix) =>
pe.withCommand(currentExec.map(_.commandLine))
.withExecId(currentExec.flatMap(_.execId))
.withChannelName(currentExec.flatMap(_.source.map(_.channelName)))
case _ => pe
}
if (channels.isEmpty) addConsoleChannel()
channels.foreach(c => ProgressState.updateProgressState(newPE, c.terminal))
}
/**
* When a reboot is initiated by a network client, we need to communicate
* to it which
*
* @param state
*/
private[sbt] def reboot(state: State): Unit = state.source match {
case Some(s) if s.channelName.startsWith("network") =>
channels.foreach {
case nc: NetworkChannel if nc.name == s.channelName =>
val remainingCommands =
state.remainingCommands
.takeWhile(!_.commandLine.startsWith(CompleteExec))
.map(_.commandLine)
.filterNot(_.startsWith("sbtReboot"))
.mkString(";")
val execId = state.remainingCommands.collectFirst {
case e if e.commandLine.startsWith(CompleteExec) =>
e.commandLine.split(CompleteExec).last.trim
}
nc.shutdown(true, execId.map(_ -> remainingCommands))
case nc: NetworkChannel => nc.shutdown(true, Some(("", "")))
case _ =>
}
case _ =>
}
private[sbt] def shutdown(name: String): Unit = {
Option(currentExecRef.get).foreach(cancel)
commandQueue.clear()
val exit = Exec(Shutdown, Some(Exec.newExecId), Some(CommandSource(name)))
commandQueue.add(exit)
()
}
private[this] def cancel(e: Exec): Unit = {
if (e.commandLine.startsWith("console")) {
val terminal = Terminal.get
terminal.write(13, 13, 13, 4)
terminal.printStream.println("\nconsole session killed by remote sbt client")
} else {
Util.ignoreResult(NetworkChannel.cancel(e.execId, e.execId.getOrElse("0")))
}
}
private[this] class FastTrackThread
extends Thread("sbt-command-exchange-fastTrack")
with AutoCloseable {
setDaemon(true)
start()
private[this] val isStopped = new AtomicBoolean(false)
override def run(): Unit = {
def exit(mt: FastTrackTask): Unit = {
mt.channel.shutdown(false)
if (mt.channel.name.contains("console")) shutdown(mt.channel.name)
}
@tailrec def impl(): Unit = {
fastTrackChannelQueue.take match {
case null =>
case mt: FastTrackTask =>
mt.task match {
case `attach` => mt.channel.prompt(ConsolePromptEvent(lastState.get))
case `Cancel` => Option(currentExecRef.get).foreach(cancel)
case t if t.startsWith(ContinuousCommands.stopWatch) =>
ContinuousCommands.stopWatchImpl(mt.channel.name)
mt.channel match {
case c: NetworkChannel if !c.isInteractive => exit(mt)
case _ => mt.channel.prompt(ConsolePromptEvent(lastState.get))
}
commandQueue.add(Exec(t, None, None))
case `TerminateAction` => exit(mt)
case `Shutdown` =>
channels.find(_.name == mt.channel.name) match {
case Some(c: NetworkChannel) => c.shutdown(false)
case _ =>
}
shutdown(mt.channel.name)
case _ =>
}
}
if (!isStopped.get) impl()
}
try impl()
catch { case _: InterruptedException => }
}
override def close(): Unit = if (isStopped.compareAndSet(false, true)) {
interrupt()
}
}
private[sbt] def channelForName(channelName: String): Option[CommandChannel] =
channels.find(_.name == channelName)
private[this] val fastTrackThread = new FastTrackThread
}

View File

@ -28,7 +28,7 @@ object ConsoleProject {
extracted.runTask(Keys.scalaCompilerBridgeBinaryJar.in(Keys.consoleProject), state1)
val scalaInstance = {
val scalaProvider = state.configuration.provider.scalaProvider
ScalaInstance(scalaProvider.version, scalaProvider.launcher)
ScalaInstance(scalaProvider.version, scalaProvider)
}
val g = BuildPaths.getGlobalBase(state)
val zincDir = BuildPaths.getZincDirectory(state, g)
@ -61,13 +61,15 @@ object ConsoleProject {
val importString = imports.mkString("", ";\n", ";\n\n")
val initCommands = importString + extra
Terminal.withCanonicalIn {
val terminal = Terminal.get
terminal.withCanonicalIn {
// TODO - Hook up dsl classpath correctly...
(new Console(compiler))(
unit.classpath,
options,
initCommands,
cleanupCommands
cleanupCommands,
terminal
)(Some(unit.loader), bindings).get
}
()

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
package sbt
package internal
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicReference
@ -29,7 +30,7 @@ private[internal] trait DeprecatedContinuous {
}
private[this] val legacyWatchState =
AttributeKey[AtomicReference[WS]]("legacy-watch-state", Int.MaxValue)
protected def addLegacyWatchSetting(state: State): State = {
private[sbt] def addLegacyWatchSetting(state: State): State = {
val legacyState = new AtomicReference[WS](WS.empty(Nil).withCount(1))
state
.put(
@ -60,8 +61,9 @@ private[internal] trait DeprecatedContinuous {
@silent
private[sbt] object DeprecatedContinuous {
private[sbt] val taskDefinitions = Seq(
private[sbt] val taskDefinitions: Seq[Def.Setting[_]] = Seq(
sbt.Keys.watchTransitiveSources := sbt.Defaults.watchTransitiveSourcesTask.value,
sbt.Keys.watch := sbt.Defaults.watchSetting.value,
sbt.nio.Keys.watchTasks := Continuous.continuousTask.evaluated,
)
}

View File

@ -10,22 +10,13 @@ package internal
import java.io.PrintWriter
import Def.ScopedKey
import Scope.GlobalScope
import Keys.{ logLevel, logManager, persistLogLevel, persistTraceLevel, sLog, traceLevel }
import sbt.internal.util.{
AttributeKey,
ConsoleAppender,
ConsoleOut,
MainAppender,
ManagedLogger,
ProgressState,
Settings,
SuppressedTraceContext
}
import MainAppender._
import sbt.util.{ Level, LogExchange, Logger }
import org.apache.logging.log4j.core.Appender
import sbt.Def.ScopedKey
import sbt.Keys._
import sbt.Scope.GlobalScope
import sbt.internal.util.MainAppender._
import sbt.internal.util._
import sbt.util.{ Level, LogExchange, Logger }
sealed abstract class LogManager {
def apply(
@ -142,9 +133,7 @@ object LogManager {
val screenTrace = getOr(traceLevel.key, data, scope, state, defaultTraceLevel(state))
val backingTrace = getOr(persistTraceLevel.key, data, scope, state, Int.MaxValue)
val extraBacked = state.globalLogging.backed :: relay :: Nil
val ps = Project.extract(state).get(sbt.Keys.progressState in ThisBuild)
val consoleOpt = consoleLocally(state, console)
ps.foreach(ProgressState.set)
val config = MainAppender.MainAppenderConfig(
consoleOpt,
backed,
@ -163,9 +152,9 @@ object LogManager {
case Some(x: Exec) =>
x.source match {
// TODO: Fix this stringliness
case Some(x: CommandSource) if x.channelName == "console0" => Option(console)
case Some(_: CommandSource) => None
case _ => Option(console)
case Some(x: CommandSource) if x.channelName == ConsoleChannel.defaultName =>
Option(console)
case _ => Option(console)
}
case _ => Option(console)
}
@ -254,7 +243,8 @@ object LogManager {
s1
}
def progressLogger(appender: Appender): ManagedLogger = {
@deprecated("No longer used.", "1.4.0")
private[sbt] def progressLogger(appender: Appender): ManagedLogger = {
val log = LogExchange.logger("progress", None, None)
LogExchange.unbindLoggerAppenders("progress")
LogExchange.bindLoggerAppenders(

View File

@ -44,7 +44,7 @@ class RelayAppender(name: String)
def appendEvent(event: AnyRef): Unit =
event match {
case x: StringEvent => exchange.logMessage(LogEvent(level = x.level, message = x.message))
case x: ObjectEvent[_] => exchange.respondObjectEvent(x)
case x: ObjectEvent[_] => // ignore object events
case _ =>
println(s"appendEvent: ${event.getClass}")
()

View File

@ -82,7 +82,7 @@ object Graph {
// [info] |
// [info] +-quux
def toAscii[A](top: A, children: A => Seq[A], display: A => String, defaultWidth: Int): String = {
val maxColumn = math.max(Terminal.getWidth, defaultWidth) - 8
val maxColumn = math.max(Terminal.get.getWidth, defaultWidth) - 8
val twoSpaces = " " + " " // prevent accidentally being converted into a tab
def limitLine(s: String): String =
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."

View File

@ -79,6 +79,7 @@ object SysProp {
def allowRootDir: Boolean = getOrFalse("sbt.rootdir")
def legacyTestReport: Boolean = getOrFalse("sbt.testing.legacyreport")
def semanticdb: Boolean = getOrFalse("sbt.semanticdb")
def forceServerStart: Boolean = getOrFalse("sbt.server.forcestart")
def watchMode: String =
sys.props.get("sbt.watch.mode").getOrElse("auto")

View File

@ -12,41 +12,51 @@ import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger, AtomicReferen
import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit }
import sbt.internal.util._
import sbt.util.Level
import scala.annotation.tailrec
import scala.concurrent.duration._
object TaskProgress extends TaskProgress
/**
* implements task progress display on the shell.
*/
private[sbt] final class TaskProgress(log: ManagedLogger)
private[sbt] class TaskProgress private ()
extends AbstractTaskExecuteProgress
with ExecuteProgress[Task] {
private[this] val lastTaskCount = new AtomicInteger(0)
private[this] val currentProgressThread = new AtomicReference[Option[ProgressThread]](None)
private[this] val sleepDuration = SysProp.supershellSleep.millis
private[this] val threshold = 10.millis
private[this] val tasks = new LinkedBlockingQueue[Task[_]]
private[this] final class ProgressThread
extends Thread("task-progress-report-thread")
with AutoCloseable {
private[this] val isClosed = new AtomicBoolean(false)
private[this] val firstTime = new AtomicBoolean(true)
private[this] val tasks = new LinkedBlockingQueue[Task[_]]
private[this] val hasReported = new AtomicBoolean(false)
private[this] def doReport(): Unit = { hasReported.set(true); report() }
setDaemon(true)
start()
private def resetThread(): Unit =
currentProgressThread.synchronized {
currentProgressThread.getAndSet(None) match {
case Some(t) if t != this => currentProgressThread.set(Some(t))
case _ =>
}
}
@tailrec override def run(): Unit = {
if (!isClosed.get()) {
if (!isClosed.get() && (!hasReported.get || active.nonEmpty)) {
try {
report()
if (activeExceedingThreshold.nonEmpty) doReport()
val duration =
if (firstTime.compareAndSet(true, activeExceedingThreshold.nonEmpty)) threshold
if (firstTime.compareAndSet(true, activeExceedingThreshold.isEmpty)) threshold
else sleepDuration
val limit = duration.fromNow
while (Deadline.now < limit) {
var task = tasks.poll((limit - Deadline.now).toMillis, TimeUnit.MILLISECONDS)
while (task != null) {
if (containsSkipTasks(Vector(task)) || lastTaskCount.get == 0) report()
if (containsSkipTasks(Vector(task)) || lastTaskCount.get == 0) doReport()
task = tasks.poll
}
}
@ -54,9 +64,12 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
case _: InterruptedException =>
isClosed.set(true)
// One last report after close in case the last one hadn't gone through yet.
report()
doReport()
}
run()
} else {
resetThread()
}
}
@ -65,21 +78,22 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
override def close(): Unit = {
isClosed.set(true)
interrupt()
report()
appendProgress(ProgressEvent("Info", Vector(), None, None, None))
resetThread()
}
}
override def initial(): Unit = ()
override def beforeWork(task: Task[_]): Unit = {
maybeStartThread()
super.beforeWork(task)
currentProgressThread.get match {
case Some(t) => t.addTask(task)
case _ => maybeStartThread()
}
tasks.put(task)
}
override def afterReady(task: Task[_]): Unit = ()
override def afterReady(task: Task[_]): Unit = maybeStartThread()
override def afterCompleted[A](task: Task[A], result: Result[A]): Unit = ()
override def afterCompleted[A](task: Task[A], result: Result[A]): Unit = maybeStartThread()
override def stop(): Unit = currentProgressThread.synchronized {
currentProgressThread.getAndSet(None).foreach(_.close())
@ -113,10 +127,8 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
case _ =>
}
}
private[this] def appendProgress(event: ProgressEvent): Unit = {
import sbt.internal.util.codec.JsonProtocol._
log.logEvent(Level.Info, event)
}
private[this] def appendProgress(event: ProgressEvent): Unit =
StandardMain.exchange.updateProgress(event)
private[this] def active: Vector[Task[_]] = activeTasks.toVector.filterNot(Def.isDummy)
private[this] def activeExceedingThreshold: Vector[(Task[_], Long)] = active.flatMap { task =>
val elapsed = timings.get(task).currentElapsedMicros
@ -133,18 +145,13 @@ private[sbt] final class TaskProgress(log: ManagedLogger)
.sortBy(_.elapsedMicros),
Some(ltc),
None,
None
None,
None,
Some(containsSkipTasks(active))
)
if (active.nonEmpty) maybeStartThread()
if (containsSkipTasks(active)) {
if (ltc > 0) {
lastTaskCount.set(0)
appendProgress(event(Vector.empty))
}
} else {
lastTaskCount.set(currentTasksCount)
appendProgress(event(currentTasks))
}
lastTaskCount.set(currentTasksCount)
appendProgress(event(currentTasks))
}
private[this] def containsSkipTasks(tasks: Vector[Task[_]]): Boolean = {

View File

@ -9,7 +9,7 @@ package sbt.internal
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URL
import java.net.{ URL, URLClassLoader }
import java.util.concurrent.{ ExecutorService, Executors }
import ClassLoaderClose.close
@ -58,10 +58,21 @@ private[internal] object ClassLoaderWarmup {
*/
private[sbt] class XMainConfiguration {
def run(moduleName: String, configuration: xsbti.AppConfiguration): xsbti.MainResult = {
val topLoader = configuration.provider.scalaProvider.launcher.topLoader
val updatedConfiguration =
if (configuration.provider.scalaProvider.launcher.topLoader.getClass.getCanonicalName
.contains("TestInterfaceLoader")) {
configuration
if (topLoader.getClass.getCanonicalName.contains("TestInterfaceLoader")) {
topLoader match {
case u: URLClassLoader =>
val urls = u.getURLs
var i = 0
while (i < urls.length && i >= 0) {
if (urls(i).toString.contains("jline")) i = -2
else i += 1
}
if (i < 0) configuration
else makeConfiguration(configuration)
case _ => configuration
}
} else {
makeConfiguration(configuration)
}

View File

@ -10,7 +10,7 @@ package internal.nio
import java.nio.file.Path
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import sbt.BasicCommandStrings.{ RebootCommand, TerminateAction }
import sbt.BasicCommandStrings.{ RebootCommand, Shutdown, TerminateAction }
import sbt.Keys.{ baseDirectory, pollInterval, state }
import sbt.Scope.Global
import sbt.SlashSyntax0._
@ -83,11 +83,16 @@ private[sbt] class CheckBuildSources extends AutoCloseable {
previousStamps.set(getStamps(force = true))
}
}
private def needCheck(cmd: String): Boolean = {
val commands = cmd.split(";").flatMap(_.trim.split(" ").headOption).filterNot(_.isEmpty)
val res = !commands.exists { c =>
c == LoadProject || c == RebootCommand || c == TerminateAction || c == "shutdown"
}
private def needCheck(state: State, cmd: String): Boolean = {
val allCmds = state.remainingCommands
.map(_.commandLine)
.dropWhile(!_.startsWith(BasicCommandStrings.MapExec)) :+ cmd
val commands =
allCmds.flatMap(_.split(";").flatMap(_.trim.split(" ").headOption).filterNot(_.isEmpty))
val filter = (c: String) =>
c == LoadProject || c == RebootCommand || c == TerminateAction || c == Shutdown ||
c.startsWith("sbtReboot")
val res = !commands.exists(filter)
if (!res) {
previousStamps.set(getStamps(force = true))
needUpdate.set(false)
@ -96,7 +101,7 @@ private[sbt] class CheckBuildSources extends AutoCloseable {
}
@inline private def forceCheck = fileTreeRepository.isEmpty
private[sbt] def needsReload(state: State, cmd: String) = {
(needCheck(cmd) && (forceCheck || needUpdate.compareAndSet(true, false))) && {
(needCheck(state, cmd) && (forceCheck || needUpdate.compareAndSet(true, false))) && {
val extracted = Project.extract(state)
val onChanges = extracted.get(Global / onChangedBuildSource)
val logger = state.globalLogging.full

View File

@ -11,6 +11,7 @@ package server
import java.net.URI
import sbt.BasicCommandStrings.Shutdown
import sbt.BuildSyntax._
import sbt.Def._
import sbt.Keys._
@ -132,7 +133,7 @@ object BuildServerProtocol {
semanticdbVersion: String
): ServerHandler = ServerHandler { callback =>
ServerIntent(
{
onRequest = {
case r: JsonRpcRequestMessage if r.method == "build/initialize" =>
val params = Converter.fromJson[InitializeBuildParams](json(r)).get
checkMetalsCompatibility(semanticdbEnabled, semanticdbVersion, params, callback.log)
@ -153,7 +154,7 @@ object BuildServerProtocol {
()
case r: JsonRpcRequestMessage if r.method == "build/exit" =>
val _ = callback.appendExec("shutdown", Some(r.id))
val _ = callback.appendExec(Shutdown, Some(r.id))
case r: JsonRpcRequestMessage if r.method == "buildTarget/sources" =>
val param = Converter.fromJson[SourcesParams](json(r)).get
@ -180,7 +181,8 @@ object BuildServerProtocol {
val command = Keys.bspBuildTargetScalacOptions.key
val _ = callback.appendExec(s"$command $targets", Some(r.id))
},
PartialFunction.empty
onResponse = PartialFunction.empty,
onNotification = PartialFunction.empty,
)
}

View File

@ -45,23 +45,23 @@ private[sbt] object LanguageServerProtocol {
def handler(converter: FileConverter): ServerHandler = ServerHandler { callback =>
import callback._
ServerIntent(
{
onRequest = {
case r: JsonRpcRequestMessage if r.method == "initialize" =>
if (authOptions(ServerAuthentication.Token)) {
val param = Converter.fromJson[InitializeParams](json(r)).get
val optionJson = param.initializationOptions.getOrElse(
throw LangServerError(
ErrorCodes.InvalidParams,
"initializationOptions is expected on 'initialize' param."
)
val param = Converter.fromJson[InitializeParams](json(r)).get
val optionJson = param.initializationOptions.getOrElse(
throw LangServerError(
ErrorCodes.InvalidParams,
"initializationOptions is expected on 'initialize' param."
)
val opt = Converter.fromJson[InitializeOption](optionJson).get
)
val opt = Converter.fromJson[InitializeOption](optionJson).get
if (authOptions(ServerAuthentication.Token)) {
val token = opt.token.getOrElse(sys.error("'token' is missing."))
if (authenticate(token)) ()
else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token")
} else ()
setInitialized(true)
appendExec("collectAnalyses", None)
if (!opt.skipAnalysis.getOrElse(false)) appendExec("collectAnalyses", None)
jsonRpcRespond(InitializeResult(serverCapabilities), Some(r.id))
case r: JsonRpcRequestMessage if r.method == "textDocument/definition" =>
@ -86,7 +86,9 @@ private[sbt] object LanguageServerProtocol {
val param = Converter.fromJson[CP](json(r)).get
onCompletionRequest(Option(r.id), param)
}, {
},
onResponse = PartialFunction.empty,
onNotification = {
case n: JsonRpcNotificationMessage if n.method == "textDocument/didSave" =>
val _ = appendExec(";Test/compile; collectAnalyses", None)
}

View File

@ -9,27 +9,40 @@ package sbt
package internal
package server
import java.io.{ IOException, InputStream, OutputStream }
import java.net.{ Socket, SocketTimeoutException }
import java.util.concurrent.atomic.AtomicBoolean
import java.nio.channels.ClosedChannelException
import java.util.concurrent.{ ConcurrentHashMap, LinkedBlockingQueue }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import sbt.BasicCommandStrings.{ Shutdown, TerminateAction }
import sbt.internal.langserver.{ CancelRequestParams, ErrorCodes, LogMessageParams, MessageType }
import sbt.internal.langserver.{ CancelRequestParams, ErrorCodes }
import sbt.internal.protocol.{
JsonRpcNotificationMessage,
JsonRpcRequestMessage,
JsonRpcResponseError,
JsonRpcResponseMessage
}
import sbt.internal.util.ObjectEvent
import sbt.internal.util.complete.Parser
import sbt.internal.ui.{ UITask, UserThread }
import sbt.internal.util.{ Prompt, ReadJsonFromInputStream, Terminal, Util }
import sbt.internal.util.Terminal.TerminalImpl
import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.protocol._
import sbt.util.Logger
import sjsonnew._
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter }
import scala.annotation.tailrec
import scala.collection.mutable
import scala.concurrent.duration._
import scala.util.Try
import scala.util.control.NonFatal
import Serialization.attach
import sjsonnew._
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter }
import BasicJsonProtocol._
import Serialization.{ attach, promptChannel }
final class NetworkChannel(
val name: String,
@ -38,21 +51,64 @@ final class NetworkChannel(
auth: Set[ServerAuthentication],
instance: ServerInstance,
handlers: Seq[ServerHandler],
val log: Logger
val log: Logger,
mkUIThreadImpl: (State, CommandChannel) => UITask
) extends CommandChannel { self =>
import NetworkChannel._
def this(
name: String,
connection: Socket,
structure: BuildStructure,
auth: Set[ServerAuthentication],
instance: ServerInstance,
handlers: Seq[ServerHandler],
log: Logger
) =
this(
name,
connection,
structure,
auth,
instance,
handlers,
log,
new UITask.AskUserTask(_, _)
)
private val running = new AtomicBoolean(true)
private val delimiter: Byte = '\n'.toByte
private val RetByte = '\r'.toByte
private val out = connection.getOutputStream
private var initialized = false
private val Curly = '{'.toByte
private val ContentLength = """^Content\-Length\:\s*(\d+)""".r
private val ContentType = """^Content\-Type\:\s*(.+)""".r
private var _contentType: String = ""
private val pendingRequests: mutable.Map[String, JsonRpcRequestMessage] = mutable.Map()
private[this] val inputBuffer = new LinkedBlockingQueue[Byte]()
private[this] val pendingWrites = new LinkedBlockingQueue[(Array[Byte], Boolean)]()
private[this] val attached = new AtomicBoolean(false)
private[this] val alive = new AtomicBoolean(true)
private[sbt] def isInteractive = interactive.get
private[this] val interactive = new AtomicBoolean(false)
private[sbt] def setInteractive(id: String, value: Boolean) = {
terminalHolder.getAndSet(new NetworkTerminal) match {
case null =>
case t => t.close()
}
interactive.set(value)
if (!isInteractive) terminal.setPrompt(Prompt.Batch)
attached.set(true)
pendingRequests.remove(id)
jsonRpcRespond("", id)
addFastTrackTask(attach)
}
private[sbt] def prompt(): Unit = {
terminal.setPrompt(Prompt.Running)
interactive.set(true)
jsonRpcNotify(promptChannel, "")
}
private[sbt] def write(byte: Byte) = inputBuffer.add(byte)
private[this] val terminalHolder = new AtomicReference(Terminal.NullTerminal)
override private[sbt] def terminal: Terminal = terminalHolder.get
override val userThread: UserThread = new UserThread(this)
private lazy val callback: ServerCallback = new ServerCallback {
def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit =
self.respondResult(event, execId)
@ -81,119 +137,53 @@ final class NetworkChannel(
self.onCancellationRequest(execId, crp)
}
def setContentType(ct: String): Unit = synchronized { _contentType = ct }
def contentType: String = _contentType
protected def authenticate(token: String): Boolean = instance.authenticate(token)
protected def setInitialized(value: Boolean): Unit = initialized = value
protected def authOptions: Set[ServerAuthentication] = auth
val thread = new Thread(s"sbt-networkchannel-${connection.getPort}") {
var contentLength: Int = 0
var state: ChannelState = SingleLine
override def run(): Unit = {
try {
val readBuffer = new Array[Byte](4096)
val in = connection.getInputStream
connection.setSoTimeout(5000)
var buffer: Vector[Byte] = Vector.empty
var bytesRead = 0
def resetChannelState(): Unit = {
contentLength = 0
state = SingleLine
}
def tillEndOfLine: Option[Vector[Byte]] = {
val delimPos = buffer.indexOf(delimiter)
if (delimPos > 0) {
val chunk0 = buffer.take(delimPos)
buffer = buffer.drop(delimPos + 1)
// remove \r at the end of line.
if (chunk0.size > 0 && chunk0.indexOf(RetByte) == chunk0.size - 1)
Some(chunk0.dropRight(1))
else Some(chunk0)
} else None // no EOL yet, so skip this turn.
}
def tillContentLength: Option[Vector[Byte]] = {
if (contentLength <= buffer.size) {
val chunk = buffer.take(contentLength)
buffer = buffer.drop(contentLength)
resetChannelState()
Some(chunk)
} else None // have not read enough yet, so skip this turn.
}
@tailrec def process(): Unit = {
// handle un-framing
state match {
case SingleLine =>
val line = tillEndOfLine
line match {
case Some(chunk) =>
chunk.headOption match {
case None => // ignore blank line
case Some(Curly) =>
// When Content-Length header is not found, interpret the line as JSON message.
handleBody(chunk)
process()
case Some(_) =>
val str = (new String(chunk.toArray, "UTF-8")).trim
handleHeader(str) match {
case Some(_) =>
state = InHeader
process()
case _ =>
val msg = s"got invalid chunk from client: $str"
log.error(msg)
logMessage("error", msg)
}
}
case _ => ()
}
case InHeader =>
tillEndOfLine match {
case Some(chunk) =>
val str = (new String(chunk.toArray, "UTF-8")).trim
if (str == "") {
state = InBody
process()
} else
handleHeader(str) match {
case Some(_) => process()
case _ =>
log.error("Got invalid header from client: " + str)
resetChannelState()
}
case _ => ()
}
case InBody =>
tillContentLength match {
case Some(chunk) =>
handleBody(chunk)
process()
case _ => ()
}
override def mkUIThread: (State, CommandChannel) => UITask = (state, command) => {
if (interactive.get || ContinuousCommands.isInWatch(this)) mkUIThreadImpl(state, command)
else
new UITask {
override private[sbt] def channel = NetworkChannel.this
override def reader: UITask.Reader = () => {
try {
this.synchronized(this.wait)
Left(TerminateAction)
} catch {
case _: InterruptedException => Right("")
}
}
}
}
val thread = new Thread(s"sbt-networkchannel-${connection.getPort}") {
private val ct = "Content-Type: "
private val x1 = "application/sbt-x1"
override def run(): Unit = {
try {
connection.setSoTimeout(5000)
val in = connection.getInputStream
// keep going unless the socket has closed
while (bytesRead != -1 && running.get) {
while (running.get) {
try {
bytesRead = in.read(readBuffer)
// log.debug(s"bytesRead: $bytesRead")
if (bytesRead > 0) {
buffer = buffer ++ readBuffer.toVector.take(bytesRead)
val onHeader: String => Unit = line => {
if (line.startsWith(ct) && line.contains(x1)) {
logMessage("error", s"server protocol $x1 is no longer supported")
}
}
process()
val content = ReadJsonFromInputStream(in, running, Some(onHeader))
if (content.nonEmpty) handleBody(content)
} catch {
case _: SocketTimeoutException => // its ok
case _: SocketTimeoutException => // its ok
case _: IOException | _: InterruptedException => running.set(false)
}
} // while
} finally {
shutdown()
shutdown(false)
}
}
@ -207,13 +197,17 @@ final class NetworkChannel(
intents.foldLeft(PartialFunction.empty[JsonRpcRequestMessage, Unit]) {
case (f, i) => f orElse i.onRequest
}
lazy val onResponseMessage: PartialFunction[JsonRpcResponseMessage, Unit] =
intents.foldLeft(PartialFunction.empty[JsonRpcResponseMessage, Unit]) {
case (f, i) => f orElse i.onResponse
}
lazy val onNotification: PartialFunction[JsonRpcNotificationMessage, Unit] =
intents.foldLeft(PartialFunction.empty[JsonRpcNotificationMessage, Unit]) {
case (f, i) => f orElse i.onNotification
}
def handleBody(chunk: Vector[Byte]): Unit = {
def handleBody(chunk: Seq[Byte]): Unit = {
Serialization.deserializeJsonMessage(chunk) match {
case Right(req: JsonRpcRequestMessage) =>
try {
@ -224,6 +218,8 @@ final class NetworkChannel(
log.debug(s"sending error: $code: $message")
respondError(code, message, Some(req.id))
}
case Right(res: JsonRpcResponseMessage) =>
onResponseMessage(res)
case Right(ntf: JsonRpcNotificationMessage) =>
try {
onNotification(ntf)
@ -240,22 +236,6 @@ final class NetworkChannel(
logMessage("error", msg)
}
}
def handleHeader(str: String): Option[Unit] = {
val sbtX1Protocol = "application/sbt-x1"
str match {
case ContentLength(len) =>
contentLength = len.toInt
Some(())
case ContentType(ct) =>
if (ct == sbtX1Protocol) {
logMessage("error", s"server protocol $ct is no longer supported")
}
setContentType(ct)
Some(())
case _ => None
}
}
}
thread.start()
@ -272,12 +252,23 @@ final class NetworkChannel(
err: JsonRpcResponseError,
execId: Option[String]
): Unit = this.synchronized {
def respond(id: String) = {
pendingRequests -= id
jsonRpcRespondError(id, err)
}
def error(): Unit = logMessage("error", s"Error ${err.code}: ${err.message}")
execId match {
case Some(id) if pendingRequests.contains(id) =>
pendingRequests -= id
jsonRpcRespondError(id, err)
case _ =>
logMessage("error", s"Error ${err.code}: ${err.message}")
case Some(id) if pendingRequests.contains(id) => respond(id)
// This handles multi commands from the network that were remapped to a different
// exec id for reporting purposes.
case Some(id) if id.startsWith(BasicCommandStrings.networkExecPrefix) =>
StandardMain.exchange.withState { s =>
s.get(BasicCommands.execMap).flatMap(_.collectFirst { case (k, `id`) => k }) match {
case Some(id) if pendingRequests.contains(id) => respond(id)
case _ => error()
}
}
case _ => error()
}
}
@ -293,14 +284,27 @@ final class NetworkChannel(
event: A,
execId: Option[String]
): Unit = this.synchronized {
def error(): Unit = {
val msg =
s"unmatched json response for requestId $execId: ${CompactPrinter(Converter.toJsonUnsafe(event))}"
log.debug(msg)
}
def respond(id: String): Unit = {
pendingRequests -= id
jsonRpcRespond(event, id)
}
execId match {
case Some(id) if pendingRequests.contains(id) =>
pendingRequests -= id
jsonRpcRespond(event, id)
case _ =>
log.debug(
s"unmatched json response for requestId $execId: ${CompactPrinter(Converter.toJsonUnsafe(event))}"
)
case Some(id) if pendingRequests.contains(id) => respond(id)
// This handles multi commands from the network that were remapped to a different
// exec id for reporting purposes.
case Some(id) if id.startsWith(BasicCommandStrings.networkExecPrefix) =>
StandardMain.exchange.withState { s =>
s.get(BasicCommands.execMap).flatMap(_.collectFirst { case (k, `id`) => k }) match {
case Some(id) if pendingRequests.contains(id) => respond(id)
case _ => error()
}
}
case _ => error()
}
}
@ -310,7 +314,7 @@ final class NetworkChannel(
def respond[A: JsonFormat](event: A): Unit = respond(event, None)
def respond[A: JsonFormat](event: A, execId: Option[String]): Unit = {
def respond[A: JsonFormat](event: A, execId: Option[String]): Unit = if (alive.get) {
respondResult(event, execId)
}
@ -322,23 +326,45 @@ final class NetworkChannel(
}
}
/**
* This publishes object events. The type information has been
* erased because it went through logging.
*/
private[sbt] def respond(event: ObjectEvent[_]): Unit = {
onObjectEvent(event)
}
def publishBytes(event: Array[Byte]): Unit = publishBytes(event, false)
def publishBytes(event: Array[Byte], delimit: Boolean): Unit = {
out.write(event)
if (delimit) {
out.write(delimiter.toInt)
/*
* Do writes on a background thread because otherwise the client socket can get blocked.
*/
private[this] val writeThread = new Thread(() => {
@tailrec def impl(): Unit = {
val (event, delimit) =
try pendingWrites.take
catch {
case _: InterruptedException =>
alive.set(false)
(Array.empty[Byte], false)
}
if (alive.get) {
try {
out.write(event)
if (delimit) {
out.write(delimiter.toInt)
}
out.flush()
} catch {
case _: IOException =>
alive.set(false)
shutdown(true)
case _: InterruptedException =>
alive.set(false)
}
impl()
}
}
out.flush()
}
impl()
}, s"sbt-$name-write-thread")
writeThread.setDaemon(true)
writeThread.start()
def publishBytes(event: Array[Byte], delimit: Boolean): Unit =
try pendingWrites.put(event -> delimit)
catch { case _: InterruptedException => }
def onCommand(command: CommandMessage): Unit = command match {
case x: InitCommand => onInitCommand(x)
@ -391,19 +417,47 @@ final class NetworkChannel(
try {
Option(EvaluateTask.lastEvaluatedState.get) match {
case Some(sstate) =>
val completionItems =
import sbt.protocol.codec.JsonProtocol._
def completionItems(s: State) = {
Parser
.completions(sstate.combinedParser, cp.query, 9)
.completions(s.combinedParser, cp.query, cp.level.getOrElse(9))
.get
.flatMap { c =>
if (!c.isEmpty) Some(c.append.replaceAll("\n", " "))
else None
}
.map(c => cp.query + c)
import sbt.protocol.codec.JsonProtocol._
}
val (items, cachedMainClassNames, cachedTestNames) = StandardMain.exchange.withState {
s =>
val scopedKeyParser: Parser[Seq[Def.ScopedKey[_]]] =
Act.aggregatedKeyParser(s) <~ Parsers.any.*
Parser.parse(cp.query, scopedKeyParser) match {
case Right(keys) =>
val testKeys =
keys.filter(k => k.key.label == "testOnly" || k.key.label == "testQuick")
val (testState, cachedTestNames) = testKeys.foldLeft((s, true)) {
case ((st, allCached), k) =>
SessionVar.loadAndSet(sbt.Keys.definedTestNames in k.scope, st, true) match {
case (nst, d) => (nst, allCached && d.isDefined)
}
}
val runKeys = keys.filter(_.key.label == "runMain")
val (runState, cachedMainClassNames) = runKeys.foldLeft((testState, true)) {
case ((st, allCached), k) =>
SessionVar.loadAndSet(sbt.Keys.discoveredMainClasses in k.scope, st, true) match {
case (nst, d) => (nst, allCached && d.isDefined)
}
}
(completionItems(runState), cachedMainClassNames, cachedTestNames)
case _ => (completionItems(s), true, true)
}
}
respondResult(
CompletionResponse(
items = completionItems.toVector
items = items.toVector,
cachedMainClassNames = cachedMainClassNames,
cachedTestNames = cachedTestNames
),
execId
)
@ -440,6 +494,11 @@ final class NetworkChannel(
Option(EvaluateTask.currentlyRunningEngine.get) match {
case Some((state, runningEngine)) =>
val runningExecId = state.currentExecId.getOrElse("")
val expected = StandardMain.exchange.withState(
_.get(BasicCommands.execMap)
.flatMap(s => s.get(crp.id) orElse s.get("\u2668" + crp.id))
.getOrElse(crp.id)
)
def checkId(): Boolean = {
if (runningExecId.startsWith("\u2668")) {
@ -450,12 +509,13 @@ final class NetworkChannel(
case (Some(id), Some(eid)) => id == eid
case _ => false
}
} else runningExecId == crp.id
} else runningExecId == expected
}
// direct comparison on strings and
// remove hotspring unicode added character for numbers
if (checkId) {
if (checkId || (crp.id == Serialization.CancelAll &&
StandardMain.exchange.currentExec.exists(_.source.exists(_.channelName == name)))) {
runningEngine.cancelAndShutdown()
import sbt.protocol.codec.JsonProtocol._
@ -477,7 +537,7 @@ final class NetworkChannel(
errorRespond("No tasks under execution")
}
} catch {
case NonFatal(e) =>
case NonFatal(_) =>
errorRespond("Cancel request failed")
}
} else {
@ -485,32 +545,38 @@ final class NetworkChannel(
}
}
def shutdown(): Unit = {
log.info("Shutting down client connection")
running.set(false)
out.close()
@deprecated("Use variant that takes logShutdown parameter", "1.4.0")
override def shutdown(): Unit = {
shutdown(true)
}
import sjsonnew.BasicJsonProtocol.BooleanJsonFormat
override def shutdown(logShutdown: Boolean): Unit =
shutdown(logShutdown, remainingCommands = None)
/**
* This reacts to various events that happens inside sbt, sometime
* in response to the previous requests.
* The type information has been erased because it went through logging.
* Closes down the channel. Before closing the socket, it sends a notification to
* the client to shutdown. If the client initiated the shutdown, we don't want the
* client to display an error or return a non-zero exit code so we send it a
* notification that tells it whether or not to log the shutdown. This can't
* easily be done client side because when the client is in interactive session,
* it doesn't know commands it has sent to the server.
*/
protected def onObjectEvent(event: ObjectEvent[_]): Unit = {
// import sbt.internal.langserver.codec.JsonProtocol._
val msgContentType = event.contentType
msgContentType match {
// LanguageServerReporter sends PublishDiagnosticsParams
case "sbt.internal.langserver.PublishDiagnosticsParams" =>
// val p = event.message.asInstanceOf[PublishDiagnosticsParams]
// jsonRpcNotify("textDocument/publishDiagnostics", p)
case "xsbti.Problem" =>
() // ignore
case _ =>
// log.debug(event)
()
}
private[sbt] def shutdown(
logShutdown: Boolean,
remainingCommands: Option[(String, String)]
): Unit = {
terminal.close()
StandardMain.exchange.removeChannel(this)
super.shutdown(logShutdown)
if (logShutdown) Terminal.consoleLog(s"shutting down client connection $name")
VirtualTerminal.cancelRequests(name)
try jsonRpcNotify(Shutdown, (logShutdown, remainingCommands))
catch { case _: IOException => }
running.set(false)
out.close()
thread.interrupt()
writeThread.interrupt()
}
/** Respond back to Language Server's client. */
@ -550,6 +616,15 @@ final class NetworkChannel(
publishBytes(bytes)
}
/** Notify to Language Server's client. */
private[sbt] def jsonRpcRequest[A: JsonFormat](id: String, method: String, params: A): Unit = {
val m =
JsonRpcRequestMessage("2.0", id, method, Option(Converter.toJson[A](params).get))
log.debug(s"jsonRpcRequest: $m")
val bytes = Serialization.serializeRequestMessage(m)
publishBytes(bytes)
}
def logMessage(level: String, message: String): Unit = {
import sbt.internal.langserver.codec.JsonProtocol._
jsonRpcNotify(
@ -557,6 +632,146 @@ final class NetworkChannel(
LogMessageParams(MessageType.fromLevelString(level), message)
)
}
private[this] lazy val inputStream: InputStream = new InputStream {
override def read(): Int = {
try {
inputBuffer.take & 0xFF match {
case -1 => throw new ClosedChannelException()
case b => b
}
} catch { case _: IOException => -1 }
}
override def available(): Int = inputBuffer.size
}
import sjsonnew.BasicJsonProtocol._
import scala.collection.JavaConverters._
private[this] lazy val outputStream: OutputStream = new OutputStream {
private[this] val buffer = new LinkedBlockingQueue[Byte]()
override def write(b: Int): Unit = buffer.put(b.toByte)
override def flush(): Unit = {
jsonRpcNotify(Serialization.systemOut, buffer.asScala)
buffer.clear()
}
override def write(b: Array[Byte]): Unit = write(b, 0, b.length)
override def write(b: Array[Byte], off: Int, len: Int): Unit = {
var i = off
while (i < len) {
buffer.put(b(i))
i += 1
}
}
}
private class NetworkTerminal extends TerminalImpl(inputStream, outputStream, name) {
private[this] val pending = new AtomicBoolean(false)
private[this] val closed = new AtomicBoolean(false)
private[this] val properties = new AtomicReference[TerminalPropertiesResponse]
private[this] val lastUpdate = new AtomicReference[Deadline]
private def empty = TerminalPropertiesResponse(0, 0, false, false, false, false)
def getProperties(block: Boolean): Unit = {
if (alive.get) {
if (!pending.get && Option(lastUpdate.get).fold(true)(d => (d + 1.second).isOverdue)) {
pending.set(true)
val queue = VirtualTerminal.sendTerminalPropertiesQuery(name, jsonRpcRequest)
val update: Runnable = () => {
queue.poll(5, java.util.concurrent.TimeUnit.SECONDS) match {
case null =>
case t => properties.set(t)
}
pending.synchronized {
lastUpdate.set(Deadline.now)
pending.set(false)
pending.notifyAll()
}
}
new Thread(update, s"network-terminal-$name-update") {
setDaemon(true)
}.start()
}
while (block && properties.get == null) pending.synchronized(pending.wait())
()
} else throw new InterruptedException
}
private def withThread[R](f: => R, default: R): R = {
val t = Thread.currentThread
try {
blockedThreads.synchronized(blockedThreads.add(t))
f
} catch { case _: InterruptedException => default } finally {
Util.ignoreResult(blockedThreads.synchronized(blockedThreads.remove(t)))
}
}
def getProperty[T](f: TerminalPropertiesResponse => T, default: T): Option[T] = {
if (closed.get || !isAttached) None
else
withThread({
getProperties(true);
Some(f(Option(properties.get).getOrElse(empty)))
}, None)
}
private[this] def waitForPending(f: TerminalPropertiesResponse => Boolean): Boolean = {
if (closed.get || !isAttached) false
withThread(
{
if (pending.get) pending.synchronized(pending.wait())
Option(properties.get).map(f).getOrElse(false)
},
false
)
}
private[this] val blockedThreads = ConcurrentHashMap.newKeySet[Thread]
override def getWidth: Int = getProperty(_.width, 0).getOrElse(0)
override def getHeight: Int = getProperty(_.height, 0).getOrElse(0)
override def isAnsiSupported: Boolean = getProperty(_.isAnsiSupported, false).getOrElse(false)
override def isEchoEnabled: Boolean = waitForPending(_.isEchoEnabled)
override def isSuccessEnabled: Boolean =
prompt != Prompt.Batch || ContinuousCommands.isInWatch(NetworkChannel.this)
override lazy val isColorEnabled: Boolean = waitForPending(_.isColorEnabled)
override lazy val isSupershellEnabled: Boolean = waitForPending(_.isSupershellEnabled)
getProperties(false)
private def getCapability[T](
query: TerminalCapabilitiesQuery,
result: TerminalCapabilitiesResponse => T
): Option[T] = {
if (closed.get) None
else {
import sbt.protocol.codec.JsonProtocol._
val queue = VirtualTerminal.sendTerminalCapabilitiesQuery(name, jsonRpcRequest, query)
Some(result(queue.take))
}
}
override def getBooleanCapability(capability: String): Boolean =
getCapability(
TerminalCapabilitiesQuery(boolean = Some(capability), numeric = None, string = None),
_.boolean.getOrElse(false)
).getOrElse(false)
override def getNumericCapability(capability: String): Int =
getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = Some(capability), string = None),
_.numeric.getOrElse(-1)
).getOrElse(-1)
override def getStringCapability(capability: String): String =
getCapability(
TerminalCapabilitiesQuery(boolean = None, numeric = None, string = Some(capability)),
_.string.flatMap {
case "null" => None
case s => Some(s)
}.orNull
).getOrElse("")
override def toString: String = s"NetworkTerminal($name)"
override def close(): Unit = if (closed.compareAndSet(false, true)) {
val threads = blockedThreads.synchronized {
val t = blockedThreads.asScala.toVector
blockedThreads.clear()
t
}
threads.foreach(_.interrupt())
super.close()
}
}
private[sbt] def isAttached: Boolean = attached.get
}
object NetworkChannel {
@ -564,4 +779,47 @@ object NetworkChannel {
case object SingleLine extends ChannelState
case object InHeader extends ChannelState
case object InBody extends ChannelState
private[sbt] def cancel(
execID: Option[String],
id: String
): Either[String, String] = {
Option(EvaluateTask.currentlyRunningEngine.get) match {
case Some((state, runningEngine)) =>
val runningExecId = state.currentExecId.getOrElse("")
def checkId(): Boolean = {
if (runningExecId.startsWith("\u2668")) {
(
Try { id.toLong }.toOption,
Try { runningExecId.substring(1).toLong }.toOption
) match {
case (Some(id), Some(eid)) => id == eid
case _ => false
}
} else runningExecId == id
}
// direct comparison on strings and
// remove hotspring unicode added character for numbers
if (checkId) {
runningEngine.cancelAndShutdown()
Right(runningExecId)
} else {
Left("Task ID not matched")
}
case None =>
Left("No tasks under execution")
}
}
private[sbt] val disconnect: Command =
Command.arb { s =>
val dncParser: Parser[String] = BasicCommandStrings.DisconnectNetworkChannel
dncParser.examples() ~> Parsers.Space.examples() ~> Parsers.any.*.examples()
} { (st, channel) =>
StandardMain.exchange.killChannel(channel.mkString)
st
}
}

View File

@ -0,0 +1,122 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
package server
import java.util.concurrent.{ ArrayBlockingQueue, ConcurrentHashMap }
import java.util.UUID
import sbt.internal.protocol.{
JsonRpcNotificationMessage,
JsonRpcRequestMessage,
JsonRpcResponseMessage
}
import sbt.protocol.Serialization.{
attach,
systemIn,
terminalCapabilities,
terminalPropertiesQuery,
}
import sjsonnew.support.scalajson.unsafe.Converter
import sbt.protocol.{
Attach,
TerminalCapabilitiesQuery,
TerminalCapabilitiesResponse,
TerminalPropertiesResponse
}
object VirtualTerminal {
private[this] val pendingTerminalProperties =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalPropertiesResponse]]()
private[this] val pendingTerminalCapabilities =
new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalCapabilitiesResponse]]
private[sbt] def sendTerminalPropertiesQuery(
channelName: String,
jsonRpcRequest: (String, String, String) => Unit
): ArrayBlockingQueue[TerminalPropertiesResponse] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[TerminalPropertiesResponse](1)
pendingTerminalProperties.put((channelName, id), queue)
jsonRpcRequest(id, terminalPropertiesQuery, "")
queue
}
private[sbt] def sendTerminalCapabilitiesQuery(
channelName: String,
jsonRpcRequest: (String, String, TerminalCapabilitiesQuery) => Unit,
query: TerminalCapabilitiesQuery,
): ArrayBlockingQueue[TerminalCapabilitiesResponse] = {
val id = UUID.randomUUID.toString
val queue = new ArrayBlockingQueue[TerminalCapabilitiesResponse](1)
pendingTerminalCapabilities.put((channelName, id), queue)
jsonRpcRequest(id, terminalCapabilities, query)
queue
}
private[sbt] def cancelRequests(name: String): Unit = {
pendingTerminalCapabilities.forEach {
case (k @ (`name`, _), q) =>
pendingTerminalCapabilities.remove(k)
q.put(TerminalCapabilitiesResponse(None, None, None))
case _ =>
}
pendingTerminalProperties.forEach {
case (k @ (`name`, _), q) =>
pendingTerminalProperties.remove(k)
q.put(TerminalPropertiesResponse(0, 0, false, false, false, false))
case _ =>
}
}
val handler = ServerHandler { cb =>
ServerIntent(requestHandler(cb), responseHandler(cb), notificationHandler(cb))
}
type Handler[R] = ServerCallback => PartialFunction[R, Unit]
private val requestHandler: Handler[JsonRpcRequestMessage] =
callback => {
case r if r.method == attach =>
import sbt.protocol.codec.JsonProtocol.AttachFormat
val isInteractive = r.params
.flatMap(Converter.fromJson[Attach](_).toOption.map(_.interactive))
.exists(identity)
StandardMain.exchange.channelForName(callback.name) match {
case Some(nc: NetworkChannel) => nc.setInteractive(r.id, isInteractive)
case _ =>
}
}
private val responseHandler: Handler[JsonRpcResponseMessage] =
callback => {
case r if pendingTerminalProperties.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response =
r.result.flatMap(Converter.fromJson[TerminalPropertiesResponse](_).toOption)
pendingTerminalProperties.remove((callback.name, r.id)) match {
case null =>
case buffer => response.foreach(buffer.put)
}
case r if pendingTerminalCapabilities.get((callback.name, r.id)) != null =>
import sbt.protocol.codec.JsonProtocol._
val response =
r.result.flatMap(
Converter.fromJson[TerminalCapabilitiesResponse](_).toOption
)
pendingTerminalCapabilities.remove((callback.name, r.id)) match {
case null =>
case buffer =>
buffer.put(response.getOrElse(TerminalCapabilitiesResponse(None, None, None)))
}
}
private val notificationHandler: Handler[JsonRpcNotificationMessage] =
callback => {
case n if n.method == systemIn =>
import sjsonnew.BasicJsonProtocol._
n.params.flatMap(Converter.fromJson[Byte](_).toOption).foreach { byte =>
StandardMain.exchange.channelForName(callback.name) match {
case Some(nc: NetworkChannel) => nc.write(byte)
case _ =>
}
}
}
}

View File

@ -114,6 +114,7 @@ object Keys {
"The message to show when triggered execution waits for sources to change. The parameters are the current watch iteration count, the current project name and the tasks that are being run with each build."
).withRank(DSetting)
// The watchTasks key should really be named watch, but that is already taken by the deprecated watch key. I'd be surprised if there are any plugins that use it so I think we should consider breaking binary compatibility to rename this task.
@deprecated("The watch input task no longer has any effect.", "1.4.0")
val watchTasks = InputKey[StateTransform](
"watch",
"Watch a task (or multiple tasks) and rebuild when its file inputs change or user input is received. The semantics are more or less the same as the `~` command except that it cannot transform the state on exit. This means that it cannot be used to reload the build."

View File

@ -13,9 +13,8 @@ import java.time.{ Instant, ZoneId, ZonedDateTime }
import java.util.Locale
import java.util.concurrent.TimeUnit
import sbt.BasicCommandStrings.ContinuousExecutePrefix
import sbt.BasicCommandStrings.{ ContinuousExecutePrefix, TerminateAction }
import sbt._
import sbt.internal.Continuous
import sbt.internal.LabeledFunctions._
import sbt.internal.nio.FileEvent
import sbt.internal.util.complete.Parser
@ -121,9 +120,9 @@ object Watch {
}
/**
* This trait is used to control the state of [[Watch.apply]]. The [[Watch.Trigger]] action
* indicates that [[Watch.apply]] should re-run the input task. The [[Watch.CancelWatch]]
* actions indicate that [[Watch.apply]] should exit and return the [[Watch.CancelWatch]]
* This trait is used to control the state of Watch. The [[Watch.Trigger]] action
* indicates that Watch should re-run the input task. The [[Watch.CancelWatch]]
* actions indicate that Watch should exit and return the [[Watch.CancelWatch]]
* instance that caused the function to exit. The [[Watch.Ignore]] action is used to indicate
* that the method should keep polling for new actions.
*/
@ -199,6 +198,12 @@ object Watch {
case _: HandleError => 0
case _ => -1
}
case Prompt =>
right match {
case Prompt => 0
case CancelWatch | Reload | (_: Run) => -1
case _ => 1
}
case _: Run =>
right match {
case _: Run => 0
@ -228,6 +233,9 @@ object Watch {
override def hashCode: Int = throwable.hashCode
override def toString: String = s"HandleError($throwable)"
}
object HandleError {
def unapply(h: HandleError): Option[Throwable] = Some(h.throwable)
}
/**
* Action that indicates that an error has occurred. The watch will be terminated when this action
@ -237,6 +245,9 @@ object Watch {
extends HandleError(throwable) {
override def toString: String = s"HandleUnexpectedError($throwable)"
}
object HandleUnexpectedError {
def unapply(h: HandleUnexpectedError): Option[Throwable] = Some(h.throwable)
}
/**
* Action that indicates that the watch should continue as though nothing happened. This may be
@ -278,6 +289,8 @@ object Watch {
def unapply(r: Run): Option[List[Exec]] = Some(r.commands.toList.map(Exec(_, None)))
}
case object Prompt extends CancelWatch
/**
* Action that indicates that the watch process should re-run the command.
*/
@ -285,7 +298,7 @@ object Watch {
/**
* A user defined Action. It is not sealed so that the user can create custom instances. If
* the onStart or nextAction function passed into [[Watch.apply]] returns [[Watch.Custom]], then
* the onStart or nextAction function passed into Watch returns [[Watch.Custom]], then
* the watch will terminate.
*/
trait Custom extends CancelWatch
@ -329,7 +342,17 @@ object Watch {
new impl(input, display, description, action)
}
private type NextAction = () => Watch.Action
private type NextAction = Int => Watch.Action
@deprecated(
"Unused in sbt but left for binary compatibility. Use five argument version instead.",
"1.4.0"
)
def apply(
task: () => Unit,
onStart: () => Watch.Action,
nextAction: () => Watch.Action,
): Watch.Action = apply(0, _ => task(), _ => onStart(), _ => nextAction(), recursive = true)
/**
* Runs a task and then blocks until the task is ready to run again or we no longer wish to
@ -341,33 +364,49 @@ object Watch {
* @return the exit [[Watch.Action]] that can be used to potentially modify the build state and
* the count of the number of iterations that were run. If
*/
def apply(task: () => Unit, onStart: NextAction, nextAction: NextAction): Watch.Action = {
def safeNextAction(delegate: NextAction): Watch.Action =
try delegate()
def apply(
initialCount: Int,
task: Int => Unit,
onStart: NextAction,
nextAction: NextAction,
recursive: Boolean
): Watch.Action = {
def safeNextAction(count: Int, delegate: NextAction): Watch.Action =
try delegate(count)
catch {
case NonFatal(t) =>
System.err.println(s"Watch caught unexpected error:")
t.printStackTrace(System.err)
new HandleError(t)
}
@tailrec def next(): Watch.Action = safeNextAction(nextAction) match {
@tailrec def next(count: Int): Watch.Action = safeNextAction(count, nextAction) match {
// This should never return Ignore due to this condition.
case Ignore => next()
case Ignore => next(count)
case action => action
}
@tailrec def impl(): Watch.Action = {
task()
safeNextAction(onStart) match {
@tailrec def impl(count: Int): Watch.Action = {
task(count)
safeNextAction(count, onStart) match {
case Ignore =>
next() match {
case Trigger => impl()
case action => action
next(count) match {
case Trigger =>
if (recursive) impl(count + 1)
else {
task(count)
Watch.Trigger
}
case action => action
}
case Trigger => impl()
case a => a
case Trigger =>
if (recursive) impl(count + 1)
else {
task(count)
Watch.Trigger
}
case a => a
}
}
try impl()
try impl(initialCount)
catch { case NonFatal(t) => new HandleError(t) }
}
@ -391,9 +430,7 @@ object Watch {
* are non empty.
*/
@inline
private[sbt] def aggregate(
events: Seq[(Action, Event)]
): Option[(Action, Event)] =
private[sbt] def aggregate(events: Seq[(Action, Event)]): Option[(Action, Event)] =
if (events.isEmpty) None else Some(events.minBy(_._1))
private implicit class StringToExec(val s: String) extends AnyVal {
@ -446,11 +483,11 @@ object Watch {
case Seq(h, rest @ _*) => rest.foldLeft(h.parser)(_ | _.parser)
}
final val defaultInputOptions: Seq[Watch.InputOption] = Seq(
Watch.InputOption("<enter>", "interrupt (exits sbt in batch mode)", Run(""), '\n', '\r'),
Watch.InputOption(4.toChar, "<ctrl-d>", "interrupt (exits sbt in batch mode)", Run("")),
Watch.InputOption("<enter>", "interrupt (exits sbt in batch mode)", CancelWatch, '\n', '\r'),
Watch.InputOption(4.toChar, "<ctrl-d>", "interrupt (exits sbt in batch mode)", CancelWatch),
Watch.InputOption('r', "re-run the command", Trigger),
Watch.InputOption('s', "return to shell", Run("iflast shell")),
Watch.InputOption('q', "quit sbt", Run("exit")),
Watch.InputOption('s', "return to shell", Prompt),
Watch.InputOption('q', "quit sbt", Run(TerminateAction)),
Watch.InputOption('?', "print options", ShowOptions)
)
@ -572,8 +609,6 @@ object Watch {
sbt.Keys.watchService :== Watched.newWatchService,
watchInputOptions :== Watch.defaultInputOptions,
watchStartMessage :== Watch.defaultStartWatch,
watchTasks := Continuous.continuousTask.evaluated,
sbt.Keys.aggregate in watchTasks :== false,
watchTriggeredMessage :== Watch.defaultOnTriggerMessage,
watchForceTriggerOnAnyChange :== false,
watchPersistFileStamps := (sbt.Keys.turbo in ThisBuild).value,

View File

@ -17,7 +17,8 @@ import sbt.internal.util.{
ConsoleOut,
GlobalLogging,
MainAppender,
Settings
Settings,
Terminal
}
object PluginCommandTestPlugin0 extends AutoPlugin { override def requires = empty }
@ -72,19 +73,21 @@ object PluginCommandTest extends Specification {
object FakeState {
def processCommand(input: String, enabledPlugins: AutoPlugin*): String = {
val previousOut = System.out
val outBuffer = new ByteArrayOutputStream
val logFile = File.createTempFile("sbt", ".log")
try {
System.setOut(new PrintStream(outBuffer, true))
val state = FakeState(enabledPlugins: _*)
MainLoop.processCommand(Exec(input, None), state)
val state = FakeState(logFile, enabledPlugins: _*)
Terminal.withOut(new PrintStream(outBuffer, true)) {
MainLoop.processCommand(Exec(input, None), state)
}
new String(outBuffer.toByteArray)
} finally {
System.setOut(previousOut)
logFile.delete()
()
}
}
def apply(plugins: AutoPlugin*) = {
def apply(logFile: File, plugins: AutoPlugin*) = {
val base = new File("").getAbsoluteFile
val testProject = Project("test-project", base).setAutoPlugins(plugins)
@ -154,9 +157,9 @@ object FakeState {
State.newHistory,
attributes,
GlobalLogging.initial(
MainAppender.globalDefault(ConsoleOut.systemOut),
File.createTempFile("sbt", ".log"),
ConsoleOut.systemOut
MainAppender.globalDefault(ConsoleOut.globalProxy),
logFile,
ConsoleOut.globalProxy
),
None,
State.Continue

View File

@ -1,171 +0,0 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import java.io.{ File, InputStream }
import java.nio.file.{ Files, Path }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import org.scalatest.{ FlatSpec, Matchers }
import sbt.WatchSpec._
import sbt.internal.nio.{ FileEvent, FileEventMonitor, FileTreeRepository }
import sbt.io._
import sbt.nio.Watch
import sbt.nio.Watch.{ NullLogger, _ }
import sbt.nio.file.{ FileAttributes, Glob, RecursiveGlob }
import sbt.nio.file.syntax._
import sbt.util.Logger
import scala.collection.mutable
import scala.concurrent.duration._
class WatchSpec extends FlatSpec with Matchers {
private type NextAction = () => Watch.Action
private def watch(task: Task, callbacks: (NextAction, NextAction)): Watch.Action =
Watch(task, callbacks._1, callbacks._2)
object TestDefaults {
def callbacks(
inputs: Seq[Glob],
fileEventMonitor: Option[FileEventMonitor[FileEvent[FileAttributes]]] = None,
logger: Logger = NullLogger,
parseEvent: () => Watch.Action = () => Ignore,
onStartWatch: () => Watch.Action = () => CancelWatch: Watch.Action,
onWatchEvent: FileEvent[FileAttributes] => Watch.Action = _ => Ignore,
triggeredMessage: FileEvent[FileAttributes] => Option[String] = _ => None,
watchingMessage: () => Option[String] = () => None
): (NextAction, NextAction) = {
val monitor: FileEventMonitor[FileEvent[FileAttributes]] = fileEventMonitor.getOrElse {
val fileTreeRepository = FileTreeRepository.default
inputs.foreach(fileTreeRepository.register)
FileEventMonitor.antiEntropy(
fileTreeRepository,
50.millis,
m => logger.debug(m.toString),
50.millis,
10.minutes
)
}
val onTrigger: FileEvent[FileAttributes] => Unit = event => {
triggeredMessage(event).foreach(logger.info(_))
}
val onStart: () => Watch.Action = () => {
watchingMessage().foreach(logger.info(_))
onStartWatch()
}
val nextAction: NextAction = () => {
val inputAction = parseEvent()
val fileActions = monitor.poll(10.millis).map { e: FileEvent[FileAttributes] =>
onWatchEvent(e) match {
case Trigger => onTrigger(e); Trigger
case action => action
}
}
(inputAction +: fileActions).min
}
(onStart, nextAction)
}
}
object NullInputStream extends InputStream {
override def available(): Int = 0
override def read(): Int = -1
}
private class Task extends (() => Unit) {
private val count = new AtomicInteger(0)
override def apply(): Unit = {
count.incrementAndGet()
()
}
def getCount: Int = count.get()
}
"Watch" should "stop" in IO.withTemporaryDirectory { dir =>
val task = new Task
watch(task, TestDefaults.callbacks(inputs = Seq(dir.toRealPath.toGlob / RecursiveGlob))) shouldBe CancelWatch
}
it should "trigger" in IO.withTemporaryDirectory { dir =>
val triggered = new AtomicBoolean(false)
val task = new Task
val callbacks = TestDefaults.callbacks(
inputs = Seq(dir.toRealPath.toGlob / RecursiveGlob),
onStartWatch = () => if (task.getCount == 2) CancelWatch else Ignore,
onWatchEvent = _ => { triggered.set(true); Trigger },
watchingMessage = () => {
new File(dir, "file").createNewFile; None
}
)
watch(task, callbacks) shouldBe CancelWatch
assert(triggered.get())
}
it should "filter events" in IO.withTemporaryDirectory { dir =>
val realDir = dir.toRealPath
val queue = new mutable.Queue[Path]
val foo = realDir.toPath.resolve("foo")
val bar = realDir.toPath.resolve("bar")
val task = new Task
val callbacks = TestDefaults.callbacks(
inputs = Seq(realDir.toGlob / RecursiveGlob),
onStartWatch = () => if (task.getCount == 2) CancelWatch else Ignore,
onWatchEvent = e => if (e.path == foo) Trigger else Ignore,
triggeredMessage = e => { queue += e.path; None },
watchingMessage = () => {
IO.touch(bar.toFile); Thread.sleep(5); IO.touch(foo.toFile)
None
}
)
watch(task, callbacks) shouldBe CancelWatch
queue.toIndexedSeq shouldBe Seq(foo)
}
it should "enforce anti-entropy" in IO.withTemporaryDirectory { dir =>
val realDir = dir.toRealPath
val queue = new mutable.Queue[Path]
val foo = realDir.toPath.resolve("foo")
val bar = realDir.toPath.resolve("bar")
val task = new Task
val callbacks = TestDefaults.callbacks(
inputs = Seq(realDir.toGlob / RecursiveGlob),
onStartWatch = () => if (task.getCount == 3) CancelWatch else Ignore,
onWatchEvent = e => if (e.path != realDir.toPath) Trigger else Ignore,
triggeredMessage = e => { queue += e.path; None },
watchingMessage = () => {
task.getCount match {
case 1 => Files.createFile(bar)
case 2 =>
bar.toFile.setLastModified(5000)
Files.createFile(foo)
case _ =>
}
None
}
)
watch(task, callbacks) shouldBe CancelWatch
queue.toIndexedSeq shouldBe Seq(bar, foo)
}
it should "halt on error" in IO.withTemporaryDirectory { dir =>
val exception = new IllegalStateException("halt")
val task = new Task { override def apply(): Unit = throw exception }
val callbacks = TestDefaults.callbacks(
Seq(dir.toRealPath.toGlob / RecursiveGlob),
)
watch(task, callbacks) shouldBe new HandleError(exception)
}
it should "reload" in IO.withTemporaryDirectory { dir =>
val task = new Task
val callbacks = TestDefaults.callbacks(
inputs = Seq(dir.toRealPath.toGlob / RecursiveGlob),
onStartWatch = () => Ignore,
onWatchEvent = _ => Watch.Reload,
watchingMessage = () => { new File(dir, "file").createNewFile(); None }
)
watch(task, callbacks) shouldBe Watch.Reload
}
}
object WatchSpec {
implicit class FileOps(val f: File) {
def toRealPath: File = f.toPath.toRealPath().toFile
}
}

View File

@ -25,7 +25,7 @@ object Dependencies {
val launcherInterface = "org.scala-sbt" % "launcher-interface" % launcherVersion
val rawLauncher = "org.scala-sbt" % "launcher" % launcherVersion
val testInterface = "org.scala-sbt" % "test-interface" % "1.0"
val ipcSocket = "org.scala-sbt.ipcsocket" % "ipcsocket" % "1.0.1"
val ipcSocket = "org.scala-sbt.ipcsocket" % "ipcsocket" % "1.1.0"
private val compilerInterface = "org.scala-sbt" % "compiler-interface" % zincVersion
private val compilerClasspath = "org.scala-sbt" %% "zinc-classpath" % zincVersion
@ -83,7 +83,8 @@ object Dependencies {
val sjsonNewScalaJson = sjsonNew("sjson-new-scalajson")
val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash")
val jline = "jline" % "jline" % "2.14.6"
val jline = "org.scala-sbt.jline" % "jline" % "2.14.7-sbt-5e51b9d4f9631ebfa29753ce4accc57808e7fd6b"
val jansi = "org.fusesource.jansi" % "jansi" % "1.12"
val scalatest = "org.scalatest" %% "scalatest" % "3.0.8"
val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0"
val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1"

View File

@ -1 +1 @@
sbt.version=1.3.8
sbt.version=1.3.10

View File

@ -9,4 +9,4 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "3.0.2")
addSbtPlugin("com.lightbend" % "sbt-whitesource" % "0.1.14")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.6.1")
addSbtPlugin("com.swoval" % "sbt-java-format" % "0.2.3")
addSbtPlugin("com.swoval" % "sbt-java-format" % "0.3.1")

View File

@ -5,22 +5,23 @@
// DO NOT EDIT MANUALLY
package sbt.internal.protocol
final class InitializeOption private (
val token: Option[String]) extends Serializable {
val token: Option[String],
val skipAnalysis: Option[Boolean]) extends Serializable {
private def this(token: Option[String]) = this(token, None)
override def equals(o: Any): Boolean = o match {
case x: InitializeOption => (this.token == x.token)
case x: InitializeOption => (this.token == x.token) && (this.skipAnalysis == x.skipAnalysis)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (17 + "sbt.internal.protocol.InitializeOption".##) + token.##)
37 * (37 * (37 * (17 + "sbt.internal.protocol.InitializeOption".##) + token.##) + skipAnalysis.##)
}
override def toString: String = {
"InitializeOption(" + token + ")"
"InitializeOption(" + token + ", " + skipAnalysis + ")"
}
private[this] def copy(token: Option[String] = token): InitializeOption = {
new InitializeOption(token)
private[this] def copy(token: Option[String] = token, skipAnalysis: Option[Boolean] = skipAnalysis): InitializeOption = {
new InitializeOption(token, skipAnalysis)
}
def withToken(token: Option[String]): InitializeOption = {
copy(token = token)
@ -28,9 +29,17 @@ final class InitializeOption private (
def withToken(token: String): InitializeOption = {
copy(token = Option(token))
}
def withSkipAnalysis(skipAnalysis: Option[Boolean]): InitializeOption = {
copy(skipAnalysis = skipAnalysis)
}
def withSkipAnalysis(skipAnalysis: Boolean): InitializeOption = {
copy(skipAnalysis = Option(skipAnalysis))
}
}
object InitializeOption {
def apply(token: Option[String]): InitializeOption = new InitializeOption(token)
def apply(token: String): InitializeOption = new InitializeOption(Option(token))
def apply(token: Option[String], skipAnalysis: Option[Boolean]): InitializeOption = new InitializeOption(token, skipAnalysis)
def apply(token: String, skipAnalysis: Boolean): InitializeOption = new InitializeOption(Option(token), Option(skipAnalysis))
}

View File

@ -12,8 +12,9 @@ implicit lazy val InitializeOptionFormat: JsonFormat[sbt.internal.protocol.Initi
case Some(__js) =>
unbuilder.beginObject(__js)
val token = unbuilder.readField[Option[String]]("token")
val skipAnalysis = unbuilder.readField[Option[Boolean]]("skipAnalysis")
unbuilder.endObject()
sbt.internal.protocol.InitializeOption(token)
sbt.internal.protocol.InitializeOption(token, skipAnalysis)
case None =>
deserializationError("Expected JsObject but found None")
}
@ -21,6 +22,7 @@ implicit lazy val InitializeOptionFormat: JsonFormat[sbt.internal.protocol.Initi
override def write[J](obj: sbt.internal.protocol.InitializeOption, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("token", obj.token)
builder.addField("skipAnalysis", obj.skipAnalysis)
builder.endObject()
}
}

View File

@ -0,0 +1,32 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class Attach private (
val interactive: Boolean) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: Attach => (this.interactive == x.interactive)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (17 + "sbt.protocol.Attach".##) + interactive.##)
}
override def toString: String = {
"Attach(" + interactive + ")"
}
private[this] def copy(interactive: Boolean = interactive): Attach = {
new Attach(interactive)
}
def withInteractive(interactive: Boolean): Attach = {
copy(interactive = interactive)
}
}
object Attach {
def apply(interactive: Boolean): Attach = new Attach(interactive)
}

View File

@ -5,28 +5,37 @@
// DO NOT EDIT MANUALLY
package sbt.protocol
final class CompletionParams private (
val query: String) extends Serializable {
val query: String,
val level: Option[Int]) extends Serializable {
private def this(query: String) = this(query, None)
override def equals(o: Any): Boolean = o match {
case x: CompletionParams => (this.query == x.query)
case x: CompletionParams => (this.query == x.query) && (this.level == x.level)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (17 + "sbt.protocol.CompletionParams".##) + query.##)
37 * (37 * (37 * (17 + "sbt.protocol.CompletionParams".##) + query.##) + level.##)
}
override def toString: String = {
"CompletionParams(" + query + ")"
"CompletionParams(" + query + ", " + level + ")"
}
private[this] def copy(query: String = query): CompletionParams = {
new CompletionParams(query)
private[this] def copy(query: String = query, level: Option[Int] = level): CompletionParams = {
new CompletionParams(query, level)
}
def withQuery(query: String): CompletionParams = {
copy(query = query)
}
def withLevel(level: Option[Int]): CompletionParams = {
copy(level = level)
}
def withLevel(level: Int): CompletionParams = {
copy(level = Option(level))
}
}
object CompletionParams {
def apply(query: String): CompletionParams = new CompletionParams(query)
def apply(query: String, level: Option[Int]): CompletionParams = new CompletionParams(query, level)
def apply(query: String, level: Int): CompletionParams = new CompletionParams(query, Option(level))
}

View File

@ -5,28 +5,44 @@
// DO NOT EDIT MANUALLY
package sbt.protocol
final class CompletionResponse private (
val items: Vector[String]) extends Serializable {
val items: Vector[String],
val cachedMainClassNames: Option[Boolean],
val cachedTestNames: Option[Boolean]) extends Serializable {
private def this(items: Vector[String]) = this(items, None, None)
override def equals(o: Any): Boolean = o match {
case x: CompletionResponse => (this.items == x.items)
case x: CompletionResponse => (this.items == x.items) && (this.cachedMainClassNames == x.cachedMainClassNames) && (this.cachedTestNames == x.cachedTestNames)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (17 + "sbt.protocol.CompletionResponse".##) + items.##)
37 * (37 * (37 * (37 * (17 + "sbt.protocol.CompletionResponse".##) + items.##) + cachedMainClassNames.##) + cachedTestNames.##)
}
override def toString: String = {
"CompletionResponse(" + items + ")"
"CompletionResponse(" + items + ", " + cachedMainClassNames + ", " + cachedTestNames + ")"
}
private[this] def copy(items: Vector[String] = items): CompletionResponse = {
new CompletionResponse(items)
private[this] def copy(items: Vector[String] = items, cachedMainClassNames: Option[Boolean] = cachedMainClassNames, cachedTestNames: Option[Boolean] = cachedTestNames): CompletionResponse = {
new CompletionResponse(items, cachedMainClassNames, cachedTestNames)
}
def withItems(items: Vector[String]): CompletionResponse = {
copy(items = items)
}
def withCachedMainClassNames(cachedMainClassNames: Option[Boolean]): CompletionResponse = {
copy(cachedMainClassNames = cachedMainClassNames)
}
def withCachedMainClassNames(cachedMainClassNames: Boolean): CompletionResponse = {
copy(cachedMainClassNames = Option(cachedMainClassNames))
}
def withCachedTestNames(cachedTestNames: Option[Boolean]): CompletionResponse = {
copy(cachedTestNames = cachedTestNames)
}
def withCachedTestNames(cachedTestNames: Boolean): CompletionResponse = {
copy(cachedTestNames = Option(cachedTestNames))
}
}
object CompletionResponse {
def apply(items: Vector[String]): CompletionResponse = new CompletionResponse(items)
def apply(items: Vector[String], cachedMainClassNames: Option[Boolean], cachedTestNames: Option[Boolean]): CompletionResponse = new CompletionResponse(items, cachedMainClassNames, cachedTestNames)
def apply(items: Vector[String], cachedMainClassNames: Boolean, cachedTestNames: Boolean): CompletionResponse = new CompletionResponse(items, Option(cachedMainClassNames), Option(cachedTestNames))
}

View File

@ -6,22 +6,23 @@
package sbt.protocol
final class InitCommand private (
val token: Option[String],
val execId: Option[String]) extends sbt.protocol.CommandMessage() with Serializable {
val execId: Option[String],
val skipAnalysis: Option[Boolean]) extends sbt.protocol.CommandMessage() with Serializable {
private def this(token: Option[String], execId: Option[String]) = this(token, execId, None)
override def equals(o: Any): Boolean = o match {
case x: InitCommand => (this.token == x.token) && (this.execId == x.execId)
case x: InitCommand => (this.token == x.token) && (this.execId == x.execId) && (this.skipAnalysis == x.skipAnalysis)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (17 + "sbt.protocol.InitCommand".##) + token.##) + execId.##)
37 * (37 * (37 * (37 * (17 + "sbt.protocol.InitCommand".##) + token.##) + execId.##) + skipAnalysis.##)
}
override def toString: String = {
"InitCommand(" + token + ", " + execId + ")"
"InitCommand(" + token + ", " + execId + ", " + skipAnalysis + ")"
}
private[this] def copy(token: Option[String] = token, execId: Option[String] = execId): InitCommand = {
new InitCommand(token, execId)
private[this] def copy(token: Option[String] = token, execId: Option[String] = execId, skipAnalysis: Option[Boolean] = skipAnalysis): InitCommand = {
new InitCommand(token, execId, skipAnalysis)
}
def withToken(token: Option[String]): InitCommand = {
copy(token = token)
@ -35,9 +36,17 @@ final class InitCommand private (
def withExecId(execId: String): InitCommand = {
copy(execId = Option(execId))
}
def withSkipAnalysis(skipAnalysis: Option[Boolean]): InitCommand = {
copy(skipAnalysis = skipAnalysis)
}
def withSkipAnalysis(skipAnalysis: Boolean): InitCommand = {
copy(skipAnalysis = Option(skipAnalysis))
}
}
object InitCommand {
def apply(token: Option[String], execId: Option[String]): InitCommand = new InitCommand(token, execId)
def apply(token: String, execId: String): InitCommand = new InitCommand(Option(token), Option(execId))
def apply(token: Option[String], execId: Option[String], skipAnalysis: Option[Boolean]): InitCommand = new InitCommand(token, execId, skipAnalysis)
def apply(token: String, execId: String, skipAnalysis: Boolean): InitCommand = new InitCommand(Option(token), Option(execId), Option(skipAnalysis))
}

View File

@ -0,0 +1,50 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalCapabilitiesQuery private (
val boolean: Option[String],
val numeric: Option[String],
val string: Option[String]) extends sbt.protocol.CommandMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalCapabilitiesQuery => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesQuery".##) + boolean.##) + numeric.##) + string.##)
}
override def toString: String = {
"TerminalCapabilitiesQuery(" + boolean + ", " + numeric + ", " + string + ")"
}
private[this] def copy(boolean: Option[String] = boolean, numeric: Option[String] = numeric, string: Option[String] = string): TerminalCapabilitiesQuery = {
new TerminalCapabilitiesQuery(boolean, numeric, string)
}
def withBoolean(boolean: Option[String]): TerminalCapabilitiesQuery = {
copy(boolean = boolean)
}
def withBoolean(boolean: String): TerminalCapabilitiesQuery = {
copy(boolean = Option(boolean))
}
def withNumeric(numeric: Option[String]): TerminalCapabilitiesQuery = {
copy(numeric = numeric)
}
def withNumeric(numeric: String): TerminalCapabilitiesQuery = {
copy(numeric = Option(numeric))
}
def withString(string: Option[String]): TerminalCapabilitiesQuery = {
copy(string = string)
}
def withString(string: String): TerminalCapabilitiesQuery = {
copy(string = Option(string))
}
}
object TerminalCapabilitiesQuery {
def apply(boolean: Option[String], numeric: Option[String], string: Option[String]): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string)
def apply(boolean: String, numeric: String, string: String): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(string))
}

View File

@ -0,0 +1,50 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalCapabilitiesResponse private (
val boolean: Option[Boolean],
val numeric: Option[Int],
val string: Option[String]) extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalCapabilitiesResponse => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesResponse".##) + boolean.##) + numeric.##) + string.##)
}
override def toString: String = {
"TerminalCapabilitiesResponse(" + boolean + ", " + numeric + ", " + string + ")"
}
private[this] def copy(boolean: Option[Boolean] = boolean, numeric: Option[Int] = numeric, string: Option[String] = string): TerminalCapabilitiesResponse = {
new TerminalCapabilitiesResponse(boolean, numeric, string)
}
def withBoolean(boolean: Option[Boolean]): TerminalCapabilitiesResponse = {
copy(boolean = boolean)
}
def withBoolean(boolean: Boolean): TerminalCapabilitiesResponse = {
copy(boolean = Option(boolean))
}
def withNumeric(numeric: Option[Int]): TerminalCapabilitiesResponse = {
copy(numeric = numeric)
}
def withNumeric(numeric: Int): TerminalCapabilitiesResponse = {
copy(numeric = Option(numeric))
}
def withString(string: Option[String]): TerminalCapabilitiesResponse = {
copy(string = string)
}
def withString(string: String): TerminalCapabilitiesResponse = {
copy(string = Option(string))
}
}
object TerminalCapabilitiesResponse {
def apply(boolean: Option[Boolean], numeric: Option[Int], string: Option[String]): TerminalCapabilitiesResponse = new TerminalCapabilitiesResponse(boolean, numeric, string)
def apply(boolean: Boolean, numeric: Int, string: String): TerminalCapabilitiesResponse = new TerminalCapabilitiesResponse(Option(boolean), Option(numeric), Option(string))
}

View File

@ -0,0 +1,52 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.protocol
final class TerminalPropertiesResponse private (
val width: Int,
val height: Int,
val isAnsiSupported: Boolean,
val isColorEnabled: Boolean,
val isSupershellEnabled: Boolean,
val isEchoEnabled: Boolean) extends sbt.protocol.EventMessage() with Serializable {
override def equals(o: Any): Boolean = o match {
case x: TerminalPropertiesResponse => (this.width == x.width) && (this.height == x.height) && (this.isAnsiSupported == x.isAnsiSupported) && (this.isColorEnabled == x.isColorEnabled) && (this.isSupershellEnabled == x.isSupershellEnabled) && (this.isEchoEnabled == x.isEchoEnabled)
case _ => false
}
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalPropertiesResponse".##) + width.##) + height.##) + isAnsiSupported.##) + isColorEnabled.##) + isSupershellEnabled.##) + isEchoEnabled.##)
}
override def toString: String = {
"TerminalPropertiesResponse(" + width + ", " + height + ", " + isAnsiSupported + ", " + isColorEnabled + ", " + isSupershellEnabled + ", " + isEchoEnabled + ")"
}
private[this] def copy(width: Int = width, height: Int = height, isAnsiSupported: Boolean = isAnsiSupported, isColorEnabled: Boolean = isColorEnabled, isSupershellEnabled: Boolean = isSupershellEnabled, isEchoEnabled: Boolean = isEchoEnabled): TerminalPropertiesResponse = {
new TerminalPropertiesResponse(width, height, isAnsiSupported, isColorEnabled, isSupershellEnabled, isEchoEnabled)
}
def withWidth(width: Int): TerminalPropertiesResponse = {
copy(width = width)
}
def withHeight(height: Int): TerminalPropertiesResponse = {
copy(height = height)
}
def withIsAnsiSupported(isAnsiSupported: Boolean): TerminalPropertiesResponse = {
copy(isAnsiSupported = isAnsiSupported)
}
def withIsColorEnabled(isColorEnabled: Boolean): TerminalPropertiesResponse = {
copy(isColorEnabled = isColorEnabled)
}
def withIsSupershellEnabled(isSupershellEnabled: Boolean): TerminalPropertiesResponse = {
copy(isSupershellEnabled = isSupershellEnabled)
}
def withIsEchoEnabled(isEchoEnabled: Boolean): TerminalPropertiesResponse = {
copy(isEchoEnabled = isEchoEnabled)
}
}
object TerminalPropertiesResponse {
def apply(width: Int, height: Int, isAnsiSupported: Boolean, isColorEnabled: Boolean, isSupershellEnabled: Boolean, isEchoEnabled: Boolean): TerminalPropertiesResponse = new TerminalPropertiesResponse(width, height, isAnsiSupported, isColorEnabled, isSupershellEnabled, isEchoEnabled)
}

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

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 =>
implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat3[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery]("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 =>
implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat5[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery]("type")
}

View File

@ -12,8 +12,9 @@ implicit lazy val CompletionParamsFormat: JsonFormat[sbt.protocol.CompletionPara
case Some(__js) =>
unbuilder.beginObject(__js)
val query = unbuilder.readField[String]("query")
val level = unbuilder.readField[Option[Int]]("level")
unbuilder.endObject()
sbt.protocol.CompletionParams(query)
sbt.protocol.CompletionParams(query, level)
case None =>
deserializationError("Expected JsObject but found None")
}
@ -21,6 +22,7 @@ implicit lazy val CompletionParamsFormat: JsonFormat[sbt.protocol.CompletionPara
override def write[J](obj: sbt.protocol.CompletionParams, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("query", obj.query)
builder.addField("level", obj.level)
builder.endObject()
}
}

View File

@ -12,8 +12,10 @@ implicit lazy val CompletionResponseFormat: JsonFormat[sbt.protocol.CompletionRe
case Some(__js) =>
unbuilder.beginObject(__js)
val items = unbuilder.readField[Vector[String]]("items")
val cachedMainClassNames = unbuilder.readField[Option[Boolean]]("cachedMainClassNames")
val cachedTestNames = unbuilder.readField[Option[Boolean]]("cachedTestNames")
unbuilder.endObject()
sbt.protocol.CompletionResponse(items)
sbt.protocol.CompletionResponse(items, cachedMainClassNames, cachedTestNames)
case None =>
deserializationError("Expected JsObject but found None")
}
@ -21,6 +23,8 @@ implicit lazy val CompletionResponseFormat: JsonFormat[sbt.protocol.CompletionRe
override def write[J](obj: sbt.protocol.CompletionResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("items", obj.items)
builder.addField("cachedMainClassNames", obj.cachedMainClassNames)
builder.addField("cachedTestNames", obj.cachedTestNames)
builder.endObject()
}
}

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 =>
implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat5[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure]("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 =>
implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat7[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure, sbt.protocol.TerminalPropertiesResponse, sbt.protocol.TerminalCapabilitiesResponse]("type")
}

View File

@ -13,8 +13,9 @@ implicit lazy val InitCommandFormat: JsonFormat[sbt.protocol.InitCommand] = new
unbuilder.beginObject(__js)
val token = unbuilder.readField[Option[String]]("token")
val execId = unbuilder.readField[Option[String]]("execId")
val skipAnalysis = unbuilder.readField[Option[Boolean]]("skipAnalysis")
unbuilder.endObject()
sbt.protocol.InitCommand(token, execId)
sbt.protocol.InitCommand(token, execId, skipAnalysis)
case None =>
deserializationError("Expected JsObject but found None")
}
@ -23,6 +24,7 @@ implicit lazy val InitCommandFormat: JsonFormat[sbt.protocol.InitCommand] = new
builder.beginObject()
builder.addField("token", obj.token)
builder.addField("execId", obj.execId)
builder.addField("skipAnalysis", obj.skipAnalysis)
builder.endObject()
}
}

View File

@ -8,6 +8,8 @@ trait JsonProtocol extends 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.CommandMessageFormats
with sbt.protocol.codec.CompletionParamsFormats
with sbt.protocol.codec.ChannelAcceptedEventFormats
@ -16,6 +18,8 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
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.EventMessageFormats
with sbt.protocol.codec.SettingQueryResponseFormats
with sbt.protocol.codec.CompletionResponseFormats

View File

@ -0,0 +1,31 @@
/**
* 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 TerminalCapabilitiesQueryFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalCapabilitiesQueryFormat: JsonFormat[sbt.protocol.TerminalCapabilitiesQuery] = new JsonFormat[sbt.protocol.TerminalCapabilitiesQuery] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalCapabilitiesQuery = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val boolean = unbuilder.readField[Option[String]]("boolean")
val numeric = unbuilder.readField[Option[String]]("numeric")
val string = unbuilder.readField[Option[String]]("string")
unbuilder.endObject()
sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalCapabilitiesQuery, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("boolean", obj.boolean)
builder.addField("numeric", obj.numeric)
builder.addField("string", obj.string)
builder.endObject()
}
}
}

View File

@ -0,0 +1,31 @@
/**
* 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 TerminalCapabilitiesResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalCapabilitiesResponseFormat: JsonFormat[sbt.protocol.TerminalCapabilitiesResponse] = new JsonFormat[sbt.protocol.TerminalCapabilitiesResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalCapabilitiesResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val boolean = unbuilder.readField[Option[Boolean]]("boolean")
val numeric = unbuilder.readField[Option[Int]]("numeric")
val string = unbuilder.readField[Option[String]]("string")
unbuilder.endObject()
sbt.protocol.TerminalCapabilitiesResponse(boolean, numeric, string)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalCapabilitiesResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("boolean", obj.boolean)
builder.addField("numeric", obj.numeric)
builder.addField("string", obj.string)
builder.endObject()
}
}
}

View File

@ -0,0 +1,37 @@
/**
* 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 TerminalPropertiesResponseFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val TerminalPropertiesResponseFormat: JsonFormat[sbt.protocol.TerminalPropertiesResponse] = new JsonFormat[sbt.protocol.TerminalPropertiesResponse] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalPropertiesResponse = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val width = unbuilder.readField[Int]("width")
val height = unbuilder.readField[Int]("height")
val isAnsiSupported = unbuilder.readField[Boolean]("isAnsiSupported")
val isColorEnabled = unbuilder.readField[Boolean]("isColorEnabled")
val isSupershellEnabled = unbuilder.readField[Boolean]("isSupershellEnabled")
val isEchoEnabled = unbuilder.readField[Boolean]("isEchoEnabled")
unbuilder.endObject()
sbt.protocol.TerminalPropertiesResponse(width, height, isAnsiSupported, isColorEnabled, isSupershellEnabled, isEchoEnabled)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.protocol.TerminalPropertiesResponse, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("width", obj.width)
builder.addField("height", obj.height)
builder.addField("isAnsiSupported", obj.isAnsiSupported)
builder.addField("isColorEnabled", obj.isColorEnabled)
builder.addField("isSupershellEnabled", obj.isSupershellEnabled)
builder.addField("isEchoEnabled", obj.isEchoEnabled)
builder.endObject()
}
}
}

View File

@ -18,4 +18,5 @@ type TokenFile {
type InitializeOption {
token: String
skipAnalysis: Boolean @since("1.4.0")
}

View File

@ -10,6 +10,7 @@ interface CommandMessage {
type InitCommand implements CommandMessage {
token: String
execId: String
skipAnalysis: Boolean @since("1.4.0")
}
## Command to execute sbt command.
@ -22,8 +23,13 @@ type SettingQuery implements CommandMessage {
setting: String!
}
type Attach implements CommandMessage {
interactive: Boolean!
}
type CompletionParams {
query: String!
level: Int @since("1.4.0")
}
## Message for events.
@ -63,6 +69,8 @@ type SettingQueryFailure implements SettingQueryResponse {
type CompletionResponse {
items: [String]
cachedMainClassNames: Boolean @since("1.4.0")
cachedTestNames: Boolean @since("1.4.0")
}
# enum Status {
@ -75,3 +83,24 @@ type ExecutionEvent {
success: String!
commandLine: String!
}
type TerminalPropertiesResponse implements EventMessage {
width: Int!
height: Int!
isAnsiSupported: Boolean!
isColorEnabled: Boolean!
isSupershellEnabled: Boolean!
isEchoEnabled: Boolean!
}
type TerminalCapabilitiesQuery implements CommandMessage {
boolean: String
numeric: String
string: String
}
type TerminalCapabilitiesResponse implements EventMessage {
boolean: Boolean
numeric: Int
string: String
}

View File

@ -21,9 +21,10 @@ import org.scalasbt.ipcsocket._
object ClientSocket {
private lazy val fileFormats = new BasicJsonProtocol with PortFileFormats with TokenFileFormats {}
def socket(portfile: File): (Socket, Option[String]) = {
def socket(portfile: File): (Socket, Option[String]) = socket(portfile, false)
def socket(portfile: File, useJNI: Boolean): (Socket, Option[String]) = {
import fileFormats._
val json: JValue = Parser.parseFromFile(portfile).get
val json: JValue = Parser.parseFromString(sbt.io.IO.read(portfile)).get
val p = Converter.fromJson[PortFile](json).get
val uri = new URI(p.uri)
// println(uri)
@ -34,12 +35,13 @@ object ClientSocket {
t.token
}
val sk = uri.getScheme match {
case "local" if isWindows =>
(new Win32NamedPipeSocket("""\\.\pipe\""" + uri.getSchemeSpecificPart): Socket)
case "local" => (new UnixDomainSocket(uri.getSchemeSpecificPart): Socket)
case "local" => localSocket(uri.getSchemeSpecificPart, useJNI)
case "tcp" => new Socket(InetAddress.getByName(uri.getHost), uri.getPort)
case _ => sys.error(s"Unsupported uri: $uri")
}
(sk, token)
}
def localSocket(name: String, useJNI: Boolean): Socket =
if (isWindows) new Win32NamedPipeSocket(s"\\\\.\\pipe\\$name", useJNI)
else new UnixDomainSocket(name, useJNI)
}

View File

@ -24,6 +24,17 @@ import sbt.internal.protocol.{
object Serialization {
private[sbt] val VsCode = "application/vscode-jsonrpc; charset=utf-8"
val systemIn = "sbt/systemIn"
val systemOut = "sbt/systemOut"
val terminalPropertiesQuery = "sbt/terminalPropertiesQuery"
val terminalPropertiesResponse = "sbt/terminalPropertiesResponse"
val terminalCapabilities = "sbt/terminalCapabilities"
val terminalCapabilitiesResponse = "sbt/terminalCapabilitiesResponse"
val attach = "sbt/attach"
val attachResponse = "sbt/attachResponse"
val cancelRequest = "sbt/cancelRequest"
val promptChannel = "sbt/promptChannel"
val CancelAll = "__CancelAll"
@deprecated("unused", since = "1.4.0")
def serializeEvent[A: JsonFormat](event: A): Array[Byte] = {
@ -44,12 +55,13 @@ object Serialization {
command match {
case x: InitCommand =>
val execId = x.execId.getOrElse(UUID.randomUUID.toString)
val analysis = s""""skipAnalysis" : ${x.skipAnalysis.getOrElse(false)}"""
val opt = x.token match {
case Some(t) =>
val json: JValue = Converter.toJson[String](t).get
val v = CompactPrinter(json)
s"""{ "token": $v }"""
case None => "{}"
s"""{ "token": $v, $analysis }"""
case None => s"{ $analysis }"
}
s"""{ "jsonrpc": "2.0", "id": "$execId", "method": "initialize", "params": { "initializationOptions": $opt } }"""
case x: ExecCommand =>
@ -62,6 +74,13 @@ object Serialization {
val json: JValue = Converter.toJson[String](x.setting).get
val v = CompactPrinter(json)
s"""{ "jsonrpc": "2.0", "id": "$execId", "method": "sbt/setting", "params": { "setting": $v } }"""
case x: Attach =>
val execId = UUID.randomUUID.toString
val json: JValue = Converter.toJson[Boolean](x.interactive).get
val v = CompactPrinter(json)
s"""{ "jsonrpc": "2.0", "id": "$execId", "method": "$attach", "params": { "interactive": $v } }"""
}
}
@ -77,6 +96,12 @@ object Serialization {
serializeResponse(message)
}
/** This formats the message according to JSON-RPC. https://www.jsonrpc.org/specification */
private[sbt] def serializeRequestMessage(message: JsonRpcRequestMessage): Array[Byte] = {
import sbt.internal.protocol.codec.JsonRPCProtocol._
serializeResponse(message)
}
/** This formats the message according to JSON-RPC. https://www.jsonrpc.org/specification */
private[sbt] def serializeNotificationMessage(
message: JsonRpcNotificationMessage,

View File

@ -7,7 +7,7 @@
package sbt
import sbt.internal.util.ConsoleAppender
import sbt.internal.util.ConsoleAppender.ClearScreenAfterCursor
import sbt.internal.util.Util.{ AnyOps, none }
object SelectMainClass {
@ -25,9 +25,9 @@ object SelectMainClass {
val classes = multiple.zipWithIndex
.map { case (className, index) => s" [${index + 1}] $className" }
.mkString("\n")
println(ConsoleAppender.ClearScreenAfterCursor + header + classes)
println(ClearScreenAfterCursor + header + classes + "\n")
val line = trim(prompt("\nEnter number: "))
val line = trim(prompt("Enter number: "))
toInt(line, multiple.length) map multiple.apply
}
}

View File

@ -16,6 +16,7 @@ object Build {
val (stringFile, string) = ("foo.txt", "bar")
val absoluteFile = baseDirectory.value.toPath.resolve(stringFile).toFile
IO.write(absoluteFile, string)
println(s"wrote to $absoluteFile")
}
def checkStringValueImpl: Def.Initialize[InputTask[Unit]] = Def.inputTask {
val Seq(stringFile, string) = Def.spaceDelimited().parsed
@ -37,9 +38,5 @@ object Build {
}.value,
checkStringValue := checkStringValueImpl.evaluated,
watchOnFileInputEvent := { (_, _) => Watch.CancelWatch },
watchTasks := Def.inputTask {
watchTasks.evaluated
StateTransform(_.fail)
}.evaluated
)
}

View File

@ -2,6 +2,6 @@
# In the build, watchOnEvent should return CancelWatch which should be successful, but we
# override watchTasks to fail the state instead
-> watch root / setStringValue
> ~ root / setStringValue
> checkStringValue foo.txt bar

Some files were not shown because too many files have changed in this diff Show More