mirror of https://github.com/sbt/sbt.git
Add multi-client ui to server
This commit makes it possible for the sbt server to render the same ui to multiple clients. The network client ui should look nearly identical to the console ui except for the log messages about the experimental client. The way that it works is that it associates a ui thread with each terminal. Whenever a command starts or completes, callbacks are invoked on the various channels to update their ui state. For example, if there are two clients and one of them runs compile, then the prompt is changed from AskUser to Running for the terminal that initiated the command while the other client remains in the AskUser state. Whenever the client changes uses ui states, the existing thread is terminated if it is running and a new thread is begun. The UITask formalizes this process. It is based on the AskUser class from older versions of sbt. In fact, there is an AskUserTask which is very similar. It uses jline to read input from the terminal (which could be a network terminal). When it gets a line, it submits it to the CommandExchange and exits. Once the next command is run (which may or may not be the command it submitted), the ui state will be reset. The debug, info, warn and error commands should work with the multi client ui. When run, they set the log level globally, not just for the client that set the level.
This commit is contained in:
parent
f0815edc7a
commit
ba345dd797
|
|
@ -955,6 +955,10 @@ 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"),
|
||||
)
|
||||
)
|
||||
.configure(
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -235,4 +235,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,7 +346,13 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@deprecated("Replaced by BuiltInCommands.continuous", "1.3.0")
|
||||
def continuous: Command =
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ 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
|
||||
|
||||
|
|
@ -35,6 +35,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 =
|
||||
|
|
|
|||
|
|
@ -9,9 +9,13 @@ 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._
|
||||
|
||||
/**
|
||||
|
|
@ -48,6 +52,8 @@ abstract class CommandChannel {
|
|||
private[sbt] final def initiateMaintenance(task: String): Unit = {
|
||||
maintenance.forEach(q => q.synchronized { q.add(new MaintenanceTask(this, task)); () })
|
||||
}
|
||||
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 && {
|
||||
|
|
@ -58,10 +64,29 @@ abstract class CommandChannel {
|
|||
}
|
||||
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
|
||||
|
|
@ -89,7 +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 MaintenanceTask(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.console,
|
||||
)
|
||||
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))
|
||||
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 private[sbt] def terminal = Terminal.console
|
||||
override val userThread: UserThread = new UserThread(this)
|
||||
private[sbt] def terminal = Terminal.console
|
||||
}
|
||||
private[sbt] object ConsoleChannel {
|
||||
private[sbt] def defaultName = "console0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ 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 in = connection.getInputStream
|
||||
|
|
@ -67,17 +69,22 @@ abstract class ServerConnection(connection: Socket) {
|
|||
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
|
||||
|
|
@ -85,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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ private[sbt] case class ServerConnection(
|
|||
socketfile: File,
|
||||
pipeName: String,
|
||||
bspConnectionFile: File,
|
||||
appConfiguration: AppConfiguration
|
||||
appConfiguration: AppConfiguration,
|
||||
) {
|
||||
def shortName: String = {
|
||||
connectionType match {
|
||||
|
|
|
|||
|
|
@ -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.ui
|
||||
|
||||
import java.io.File
|
||||
import java.nio.channels.ClosedChannelException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import jline.console.history.PersistentHistory
|
||||
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.onMaintenance(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 => Left("exit")
|
||||
case s: String =>
|
||||
lineReader.getHistory match {
|
||||
case p: PersistentHistory =>
|
||||
p.add(s)
|
||||
p.flush()
|
||||
case _ =>
|
||||
}
|
||||
s match {
|
||||
case "" => impl()
|
||||
case cmd @ ("shutdown" | "exit" | "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
|
||||
}
|
||||
|
|
@ -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] =
|
||||
|
|
|
|||
|
|
@ -407,7 +407,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})"
|
||||
|
|
@ -1495,13 +1495,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.get.withRawSystemIn {
|
||||
Terminal.read match {
|
||||
Terminal.get.inputStream.read match {
|
||||
case -1 => None
|
||||
case b =>
|
||||
val res = b.toChar.toString
|
||||
|
|
@ -2343,6 +2343,9 @@ object Classpaths {
|
|||
CrossVersion(scalaVersion, binVersion)(base).withCrossVersion(Disabled())
|
||||
},
|
||||
shellPrompt := shellPromptFromState,
|
||||
terminalShellPrompt := { (t, s) =>
|
||||
shellPromptFromState(t)(s)
|
||||
},
|
||||
dynamicDependency := { (): Unit },
|
||||
transitiveClasspathDependency := { (): Unit },
|
||||
transitiveDynamicInputs :== Nil,
|
||||
|
|
@ -3826,11 +3829,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 _ => "> "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ 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)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ 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.{ Shell, TemplateCommand, networkExecPrefix }
|
||||
import sbt.Project.LoadAction
|
||||
import sbt.compiler.EvalImports
|
||||
import sbt.internal.Aggregation.AnyKeys
|
||||
|
|
@ -24,6 +25,7 @@ import sbt.internal.client.BspClient
|
|||
import sbt.internal.inc.ScalaInstance
|
||||
import sbt.internal.io.Retry
|
||||
import sbt.internal.nio.CheckBuildSources
|
||||
import sbt.internal.server.NetworkChannel
|
||||
import sbt.internal.util.Types.{ const, idFun }
|
||||
import sbt.internal.util._
|
||||
import sbt.internal.util.complete.{ Parser, SizeParser }
|
||||
|
|
@ -34,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
|
||||
|
||||
/** This class is the entry point for sbt. */
|
||||
final class xMain extends xsbti.AppMain {
|
||||
|
|
@ -130,18 +134,24 @@ 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. */
|
||||
|
|
@ -254,6 +264,7 @@ object BuiltinCommands {
|
|||
act,
|
||||
continuous,
|
||||
clearCaches,
|
||||
NetworkChannel.disconnect,
|
||||
) ++ allBasicCommands
|
||||
|
||||
def DefaultBootCommands: Seq[String] =
|
||||
|
|
@ -904,6 +915,16 @@ object BuiltinCommands {
|
|||
Command.command(ClearCaches, help)(f)
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -914,18 +935,17 @@ object BuiltinCommands {
|
|||
.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}")
|
||||
}
|
||||
val exec: Exec = getExec(s1, minGCInterval)
|
||||
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
|
||||
val res =
|
||||
if (exec.commandLine.trim.isEmpty) newState
|
||||
else newState.clearGlobalLog
|
||||
res
|
||||
}
|
||||
|
||||
def startServer: Command =
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ package sbt
|
|||
import java.io.PrintWriter
|
||||
import java.util.Properties
|
||||
|
||||
import sbt.BasicCommandStrings.{ 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,16 +197,21 @@ 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)
|
||||
|
|
@ -273,7 +280,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,6 +19,7 @@ import Keys.{
|
|||
historyPath,
|
||||
projectCommand,
|
||||
sessionSettings,
|
||||
terminalShellPrompt,
|
||||
shellPrompt,
|
||||
templateResolverInfos,
|
||||
autoStartServer,
|
||||
|
|
@ -508,6 +509,7 @@ 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)
|
||||
|
|
@ -532,6 +534,7 @@ object Project extends ProjectExtra {
|
|||
.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(
|
||||
|
|
|
|||
|
|
@ -10,19 +10,21 @@ package sbt
|
|||
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.networkExecPrefix
|
||||
import sbt.BasicKeys._
|
||||
import sbt.nio.Watch.NullLogger
|
||||
import sbt.internal.protocol.JsonRpcResponseError
|
||||
import sbt.internal.server._
|
||||
import sbt.internal.util.{ ConsoleOut, MainAppender, ProgressEvent, ProgressState, 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 +32,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,23 +46,15 @@ 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 maintenanceChannelQueue = new LinkedBlockingQueue[MaintenanceTask]
|
||||
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]
|
||||
|
||||
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)
|
||||
|
|
@ -68,23 +64,39 @@ private[sbt] final class CommandExchange {
|
|||
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 = {
|
||||
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 = {
|
||||
@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()
|
||||
}
|
||||
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
|
||||
state.foreach(s => prompt(ConsolePromptEvent(s)))
|
||||
def poll: Option[Exec] =
|
||||
Option(deadline match {
|
||||
case Some(d: Deadline) =>
|
||||
commandQueue.poll(d.timeLeft.toMillis + 1, TimeUnit.MILLISECONDS)
|
||||
case _ => commandQueue.take
|
||||
})
|
||||
poll match {
|
||||
case Some(exec) if exec.source.fold(true)(s => channels.exists(_.name == s.channelName)) =>
|
||||
exec.commandLine match {
|
||||
case "shutdown" =>
|
||||
exec
|
||||
.withCommandLine("exit")
|
||||
.withSource(Some(CommandSource(ConsoleChannel.defaultName)))
|
||||
case "exit" 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(deadline)
|
||||
case _ => exec
|
||||
}
|
||||
case _ => exec
|
||||
}
|
||||
case None =>
|
||||
val newDeadline = if (deadline.fold(false)(_.isOverdue())) {
|
||||
GCUtil.forceGcWithInterval(interval, logger)
|
||||
|
|
@ -100,23 +112,38 @@ private[sbt] final class CommandExchange {
|
|||
})
|
||||
}
|
||||
|
||||
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")))
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def mkAskUser(
|
||||
name: String,
|
||||
): (State, CommandChannel) => UITask = { (state, channel) =>
|
||||
new UITask.AskUserTask(state, channel)
|
||||
}
|
||||
|
||||
private[sbt] def currentExec = Option(currentExecRef.get)
|
||||
|
||||
/**
|
||||
|
|
@ -128,22 +155,22 @@ 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)
|
||||
|
||||
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) {
|
||||
|
|
@ -165,7 +192,7 @@ private[sbt] final class CommandExchange {
|
|||
socketfile,
|
||||
pipeName,
|
||||
bspConnectionFile,
|
||||
s.configuration
|
||||
s.configuration,
|
||||
)
|
||||
val serverInstance = Server.start(connection, onIncomingSocket, s.log)
|
||||
// don't throw exception when it times out
|
||||
|
|
@ -196,7 +223,7 @@ private[sbt] final class CommandExchange {
|
|||
|
||||
def shutdown(): Unit = {
|
||||
maintenanceThread.close()
|
||||
channels foreach (_.shutdown())
|
||||
channels foreach (_.shutdown(true))
|
||||
// interrupt and kill the thread
|
||||
server.foreach(_.shutdown())
|
||||
server = None
|
||||
|
|
@ -243,11 +270,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)(
|
||||
|
|
@ -274,26 +300,22 @@ private[sbt] final class CommandExchange {
|
|||
tryTo(_.respondError(code, event.message.getOrElse(""), event.execId))(channel)
|
||||
}
|
||||
}
|
||||
|
||||
tryTo(_.respond(event, event.execId))(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(_.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 = {
|
||||
|
|
@ -305,19 +327,23 @@ 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) =>
|
||||
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))
|
||||
}
|
||||
|
||||
private[sbt] def shutdown(name: String): Unit = {
|
||||
Option(currentExecRef.get).foreach(cancel)
|
||||
commandQueue.clear()
|
||||
val exit =
|
||||
Exec("shutdown", Some(Exec.newExecId), Some(CommandSource(name)))
|
||||
|
|
@ -333,7 +359,7 @@ private[sbt] final class CommandExchange {
|
|||
private[this] val isStopped = new AtomicBoolean(false)
|
||||
override def run(): Unit = {
|
||||
def exit(mt: MaintenanceTask): Unit = {
|
||||
mt.channel.shutdown()
|
||||
mt.channel.shutdown(false)
|
||||
if (mt.channel.name.contains("console")) shutdown(mt.channel.name)
|
||||
}
|
||||
@tailrec def impl(): Unit = {
|
||||
|
|
@ -341,6 +367,8 @@ private[sbt] final class CommandExchange {
|
|||
case null =>
|
||||
case mt: MaintenanceTask =>
|
||||
mt.task match {
|
||||
case `attach` => mt.channel.prompt(ConsolePromptEvent(lastState.get))
|
||||
case "cancel" => Option(currentExecRef.get).foreach(cancel)
|
||||
case "exit" => exit(mt)
|
||||
case "shutdown" => shutdown(mt.channel.name)
|
||||
case _ =>
|
||||
|
|
|
|||
|
|
@ -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.get.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
|
||||
}
|
||||
()
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
|
|||
|
||||
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R =
|
||||
Terminal.get.withRawSystemIn {
|
||||
val wrapped = Terminal.get.inputStream
|
||||
val wrapped = Terminal.wrappedSystemIn
|
||||
if (Util.isNonCygwinWindows) {
|
||||
val inputStream: InputStream with AutoCloseable = new InputStream with AutoCloseable {
|
||||
private[this] val buffer = new java.util.LinkedList[Int]
|
||||
|
|
|
|||
|
|
@ -152,8 +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 _ => Option(console)
|
||||
case Some(x: CommandSource) if x.channelName == ConsoleChannel.defaultName =>
|
||||
Option(console)
|
||||
case _ => Option(console)
|
||||
}
|
||||
case _ => Option(console)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import scala.concurrent.duration._
|
|||
private[sbt] final class TaskProgress
|
||||
extends AbstractTaskExecuteProgress
|
||||
with ExecuteProgress[Task] {
|
||||
@deprecated("Use the constructor taking an ExecID.", "1.4.0")
|
||||
@deprecated("Use the no argument constructor.", "1.4.0")
|
||||
def this(log: ManagedLogger) = this()
|
||||
private[this] val lastTaskCount = new AtomicInteger(0)
|
||||
private[this] val currentProgressThread = new AtomicReference[Option[ProgressThread]](None)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,19 @@ import java.util.concurrent.{ ConcurrentHashMap, LinkedBlockingQueue }
|
|||
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
|
||||
|
||||
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.{ ReadJsonFromInputStream, Prompt, Terminal, Util }
|
||||
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
|
||||
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
|
||||
|
|
@ -37,6 +37,11 @@ import scala.util.Try
|
|||
import scala.util.control.NonFatal
|
||||
import Serialization.attach
|
||||
|
||||
import sjsonnew._
|
||||
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter }
|
||||
|
||||
import Serialization.attach
|
||||
|
||||
final class NetworkChannel(
|
||||
val name: String,
|
||||
connection: Socket,
|
||||
|
|
@ -44,8 +49,28 @@ final class NetworkChannel(
|
|||
auth: Set[ServerAuthentication],
|
||||
instance: ServerInstance,
|
||||
handlers: Seq[ServerHandler],
|
||||
val log: Logger
|
||||
val log: Logger,
|
||||
mkUIThreadImpl: (State, CommandChannel) => UITask
|
||||
) extends CommandChannel { self =>
|
||||
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
|
||||
|
|
@ -76,6 +101,7 @@ final class NetworkChannel(
|
|||
|
||||
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 =
|
||||
|
|
@ -111,6 +137,22 @@ final class NetworkChannel(
|
|||
|
||||
protected def authOptions: Set[ServerAuthentication] = auth
|
||||
|
||||
override def mkUIThread: (State, CommandChannel) => UITask = (state, command) => {
|
||||
if (interactive.get) mkUIThreadImpl(state, command)
|
||||
else
|
||||
new UITask {
|
||||
override private[sbt] def channel = NetworkChannel.this
|
||||
override def reader: UITask.Reader = () => {
|
||||
try {
|
||||
this.synchronized(this.wait)
|
||||
Left("exit")
|
||||
} catch {
|
||||
case _: InterruptedException => Right("")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val thread = new Thread(s"sbt-networkchannel-${connection.getPort}") {
|
||||
private val ct = "Content-Type: "
|
||||
private val x1 = "application/sbt-x1"
|
||||
|
|
@ -135,7 +177,7 @@ final class NetworkChannel(
|
|||
}
|
||||
} // while
|
||||
} finally {
|
||||
shutdown()
|
||||
shutdown(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +284,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)
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +320,7 @@ final class NetworkChannel(
|
|||
} catch {
|
||||
case _: IOException =>
|
||||
alive.set(false)
|
||||
shutdown()
|
||||
shutdown(true)
|
||||
case _: InterruptedException =>
|
||||
alive.set(false)
|
||||
}
|
||||
|
|
@ -431,7 +473,7 @@ final class NetworkChannel(
|
|||
errorRespond("No tasks under execution")
|
||||
}
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
case NonFatal(_) =>
|
||||
errorRespond("Cancel request failed")
|
||||
}
|
||||
} else {
|
||||
|
|
@ -439,10 +481,23 @@ final class NetworkChannel(
|
|||
}
|
||||
}
|
||||
|
||||
def shutdown(): Unit = {
|
||||
log.info("Shutting down client connection")
|
||||
@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 = {
|
||||
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)
|
||||
catch { case _: IOException => }
|
||||
running.set(false)
|
||||
out.close()
|
||||
thread.interrupt()
|
||||
writeThread.interrupt()
|
||||
}
|
||||
|
||||
/** Respond back to Language Server's client. */
|
||||
|
|
@ -643,4 +698,13 @@ object NetworkChannel {
|
|||
case object SingleLine extends ChannelState
|
||||
case object InHeader extends ChannelState
|
||||
case object InBody extends ChannelState
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 +1 @@
|
|||
sbt.version=1.3.8
|
||||
sbt.version=1.3.10
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
TaskKey[Unit]("willSucceed") := println("success")
|
||||
|
||||
TaskKey[Unit]("willFail") := { throw new Exception("failed") }
|
||||
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test"
|
||||
|
||||
TaskKey[Unit]("fooBar") := { () }
|
||||
|
|
@ -0,0 +1 @@
|
|||
object A
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package test.pkg
|
||||
|
||||
class FooSpec extends org.scalatest.FlatSpec
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
|
||||
object Main extends App {
|
||||
|
||||
while (true) {
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
try this.synchronized(this.wait)
|
||||
catch { case _: InterruptedException => }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,16 @@ ThisBuild / scalaVersion := "2.12.11"
|
|||
lazy val root = (project in file("."))
|
||||
.settings(
|
||||
Global / serverLog / logLevel := Level.Debug,
|
||||
|
||||
// custom handler
|
||||
Global / serverHandlers += ServerHandler({ callback =>
|
||||
import callback._
|
||||
import sjsonnew.BasicJsonProtocol._
|
||||
import sbt.internal.protocol.JsonRpcRequestMessage
|
||||
ServerIntent(
|
||||
{
|
||||
case r: JsonRpcRequestMessage if r.method == "lunar/helo" =>
|
||||
jsonRpcNotify("lunar/oleh", "")
|
||||
()
|
||||
},
|
||||
PartialFunction.empty
|
||||
)
|
||||
ServerIntent.request {
|
||||
case r: JsonRpcRequestMessage if r.method == "lunar/helo" =>
|
||||
jsonRpcNotify("lunar/oleh", "")
|
||||
()
|
||||
}
|
||||
}),
|
||||
|
||||
name := "handshake",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,54 +8,79 @@
|
|||
package testpkg
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import sbt.Exec
|
||||
|
||||
// starts svr using server-test/events and perform event related tests
|
||||
object EventsTest extends AbstractServerTest {
|
||||
override val testDirectory: String = "events"
|
||||
val currentID = new AtomicInteger(0)
|
||||
|
||||
test("report task failures in case of exceptions") { _ =>
|
||||
val id = currentID.getAndIncrement()
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id": 11, "method": "sbt/exec", "params": { "commandLine": "hello" } }"""
|
||||
s"""{ "jsonrpc": "2.0", "id": $id, "method": "sbt/exec", "params": { "commandLine": "hello" } }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
(s contains """"id":11""") && (s contains """"error":""")
|
||||
(s contains s""""id":$id""") && (s contains """"error":""")
|
||||
})
|
||||
}
|
||||
|
||||
test("return error if cancelling non-matched task id") { _ =>
|
||||
val id = currentID.getAndIncrement()
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
|
||||
s"""{ "jsonrpc": "2.0", "id":$id, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
s contains "Waiting for"
|
||||
})
|
||||
val cancelID = currentID.getAndIncrement()
|
||||
val invalidID = currentID.getAndIncrement()
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "55" } }"""
|
||||
s"""{ "jsonrpc": "2.0", "id":$cancelID, "method": "sbt/cancelRequest", "params": { "id": "$invalidID" } }"""
|
||||
)
|
||||
assert(svr.waitForString(20.seconds) { s =>
|
||||
(s contains """"error":{"code":-32800""")
|
||||
})
|
||||
svr.sendJsonRpc(
|
||||
s"""{ "jsonrpc": "2.0", "id":${currentID.getAndIncrement}, "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
s contains """"result":{"status":"Task cancelled""""
|
||||
})
|
||||
}
|
||||
|
||||
test("cancel on-going task with numeric id") { _ =>
|
||||
val id = currentID.getAndIncrement()
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id":12, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
|
||||
s"""{ "jsonrpc": "2.0", "id":$id, "method": "sbt/exec", "params": { "commandLine": "run" } }"""
|
||||
)
|
||||
assert(svr.waitForString(1.minute) { s =>
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id":13, "method": "sbt/cancelRequest", "params": { "id": "12" } }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
s contains "Waiting for"
|
||||
})
|
||||
val cancelID = currentID.getAndIncrement()
|
||||
svr.sendJsonRpc(
|
||||
s"""{ "jsonrpc": "2.0", "id":$cancelID, "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
s contains """"result":{"status":"Task cancelled""""
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
test("cancel on-going task with string id") { _ =>
|
||||
val id = Exec.newExecId
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id": "foo", "method": "sbt/exec", "params": { "commandLine": "run" } }"""
|
||||
s"""{ "jsonrpc": "2.0", "id": "$id", "method": "sbt/exec", "params": { "commandLine": "run" } }"""
|
||||
)
|
||||
assert(svr.waitForString(1.minute) { s =>
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id": "bar", "method": "sbt/cancelRequest", "params": { "id": "foo" } }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
s contains "Waiting for"
|
||||
})
|
||||
val cancelID = Exec.newExecId
|
||||
svr.sendJsonRpc(
|
||||
s"""{ "jsonrpc": "2.0", "id": "$cancelID", "method": "sbt/cancelRequest", "params": { "id": "$id" } }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
s contains """"result":{"status":"Task cancelled""""
|
||||
})
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ object ResponseTest extends AbstractServerTest {
|
|||
"""{ "jsonrpc": "2.0", "id": "10", "method": "foo/export", "params": {} }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
(s contains """"id":"10"""") &&
|
||||
(s contains "scala-library.jar")
|
||||
})
|
||||
|
|
@ -29,7 +29,7 @@ object ResponseTest extends AbstractServerTest {
|
|||
"""{ "jsonrpc": "2.0", "id": "11", "method": "foo/rootClasspath", "params": {} }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
(s contains """"id":"11"""") &&
|
||||
(s contains "scala-library.jar")
|
||||
})
|
||||
|
|
@ -40,7 +40,7 @@ object ResponseTest extends AbstractServerTest {
|
|||
"""{ "jsonrpc": "2.0", "id": "12", "method": "foo/fail", "params": {} }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
(s contains """"error":{"code":-33000,"message":"fail message"""")
|
||||
})
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ object ResponseTest extends AbstractServerTest {
|
|||
"""{ "jsonrpc": "2.0", "id": "13", "method": "foo/customfail", "params": {} }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
(s contains """"error":{"code":500,"message":"some error"""")
|
||||
})
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ object ResponseTest extends AbstractServerTest {
|
|||
"""{ "jsonrpc": "2.0", "id": "14", "method": "foo/notification", "params": {} }"""
|
||||
)
|
||||
assert(svr.waitForString(10.seconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
(s contains """{"jsonrpc":"2.0","method":"foo/something","params":"something"}""")
|
||||
})
|
||||
}
|
||||
|
|
@ -71,14 +71,14 @@ object ResponseTest extends AbstractServerTest {
|
|||
)
|
||||
assert {
|
||||
svr.waitForString(1.seconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
s contains "\"id\":\"15\""
|
||||
}
|
||||
}
|
||||
assert {
|
||||
// the second response should never be sent
|
||||
svr.neverReceive(500.milliseconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
s contains "\"id\":\"15\""
|
||||
}
|
||||
}
|
||||
|
|
@ -90,14 +90,14 @@ object ResponseTest extends AbstractServerTest {
|
|||
)
|
||||
assert {
|
||||
svr.waitForString(1.seconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
s contains "\"id\":\"16\""
|
||||
}
|
||||
}
|
||||
assert {
|
||||
// the second response (result or error) should never be sent
|
||||
svr.neverReceive(500.milliseconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
s contains "\"id\":\"16\""
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ object ResponseTest extends AbstractServerTest {
|
|||
)
|
||||
assert {
|
||||
svr.neverReceive(500.milliseconds) { s =>
|
||||
println(s)
|
||||
if (!s.contains("systemOut")) println(s)
|
||||
s contains "\"result\":\"notification result\""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package testpkg
|
||||
|
||||
import java.io.{ File, IOException }
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
import verify._
|
||||
|
|
@ -26,6 +27,7 @@ trait AbstractServerTest extends TestSuite[Unit] {
|
|||
private var temp: File = _
|
||||
var svr: TestServer = _
|
||||
def testDirectory: String
|
||||
def testPath: Path = temp.toPath.resolve(testDirectory)
|
||||
|
||||
private val targetDir: File = {
|
||||
val p0 = new File("..").getAbsoluteFile.getCanonicalFile / "target"
|
||||
|
|
@ -224,7 +226,7 @@ case class TestServer(
|
|||
def bye(): Unit = {
|
||||
hostLog("sending exit")
|
||||
sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id": 9, "method": "sbt/exec", "params": { "commandLine": "exit" } }"""
|
||||
"""{ "jsonrpc": "2.0", "id": 9, "method": "sbt/exec", "params": { "commandLine": "shutdown" } }"""
|
||||
)
|
||||
val deadline = 10.seconds.fromNow
|
||||
while (!deadline.isOverdue && process.isAlive) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue