mirror of https://github.com/sbt/sbt.git
Merge pull request #5620 from eatkins/wip-sbt-instant-startup
Nearly instantaneous sbt startup with remote client
This commit is contained in:
commit
0941415420
173
.appveyor.yml
173
.appveyor.yml
|
|
@ -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
120
build.sbt
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
#compdef sbtc
|
||||
|
||||
COMPLETE="--completions=${words[@]}"
|
||||
COMPLETIONS=($(sbtc --no-tab ${COMPLETE}))
|
||||
_alternative 'arguments:custom arg:($COMPLETIONS)'
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
_do_sbtc_completions() {
|
||||
COMPREPLY=($(sbtc "--completions=${COMP_LINE}"))
|
||||
}
|
||||
|
||||
complete -F _do_sbtc_completions sbtc
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
function __sbtcomp
|
||||
sbtc --completions="$argv"
|
||||
end
|
||||
complete --command sbtc -f --arguments '(__sbtcomp (commandline -cp))'
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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":[] }]
|
||||
}
|
||||
]
|
||||
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; () })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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] =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 _ => "> "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
()
|
||||
|
|
|
|||
|
|
@ -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) + ".."
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
sbt.version=1.3.8
|
||||
sbt.version=1.3.10
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
50
protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesQuery.scala
generated
Normal file
50
protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesQuery.scala
generated
Normal 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))
|
||||
}
|
||||
50
protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesResponse.scala
generated
Normal file
50
protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesResponse.scala
generated
Normal 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))
|
||||
}
|
||||
52
protocol/src/main/contraband-scala/sbt/protocol/TerminalPropertiesResponse.scala
generated
Normal file
52
protocol/src/main/contraband-scala/sbt/protocol/TerminalPropertiesResponse.scala
generated
Normal 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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesQueryFormats.scala
generated
Normal file
31
protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesQueryFormats.scala
generated
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
31
protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesResponseFormats.scala
generated
Normal file
31
protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesResponseFormats.scala
generated
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
37
protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalPropertiesResponseFormats.scala
generated
Normal file
37
protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalPropertiesResponseFormats.scala
generated
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,4 +18,5 @@ type TokenFile {
|
|||
|
||||
type InitializeOption {
|
||||
token: String
|
||||
skipAnalysis: Boolean @since("1.4.0")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue