mirror of https://github.com/sbt/sbt.git
Merge pull request #2737 from eed3si9n/topic/server_reboot
[sbt 1.0] server reboot
This commit is contained in:
commit
4765990995
|
|
@ -43,7 +43,7 @@ def commonSettings: Seq[Setting[_]] = Seq[SettingsDefinition](
|
|||
testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"),
|
||||
javacOptions in compile ++= Seq("-target", "6", "-source", "6", "-Xlint", "-Xlint:-serial"),
|
||||
incOptions := incOptions.value.withNameHashing(true),
|
||||
crossScalaVersions := Seq(scala211, scala210),
|
||||
crossScalaVersions := Seq(scala211),
|
||||
bintrayPackage := (bintrayPackage in ThisBuild).value,
|
||||
bintrayRepository := (bintrayRepository in ThisBuild).value,
|
||||
mimaDefaultSettings,
|
||||
|
|
@ -176,11 +176,13 @@ lazy val actionsProj = (project in file("main-actions")).
|
|||
|
||||
// General command support and core commands not specific to a build system
|
||||
lazy val commandProj = (project in file("main-command")).
|
||||
enablePlugins(DatatypePlugin, JsonCodecPlugin).
|
||||
settings(
|
||||
testedBaseSettings,
|
||||
name := "Command",
|
||||
libraryDependencies ++= Seq(launcherInterface, compilerInterface,
|
||||
sbtIO, utilLogging, utilCompletion, compilerClasspath)
|
||||
sbtIO, utilLogging, utilCompletion, compilerClasspath, sjsonNewScalaJson),
|
||||
sourceManaged in (Compile, generateDatatypes) := baseDirectory.value / "src" / "main" / "datatype-scala"
|
||||
)
|
||||
|
||||
// Fixes scope=Scope for Setting (core defined in collectionProj) to define the settings system used in build definitions
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* This code is generated using sbt-datatype.
|
||||
*/
|
||||
|
||||
// DO NOT EDIT MANUALLY
|
||||
package sbt.internal.server
|
||||
final class CommandMessage(
|
||||
val `type`: String,
|
||||
val commandLine: Option[String]) extends Serializable {
|
||||
|
||||
def this(`type`: String) = this(`type`, None)
|
||||
|
||||
override def equals(o: Any): Boolean = o match {
|
||||
case x: CommandMessage => (this.`type` == x.`type`) && (this.commandLine == x.commandLine)
|
||||
case _ => false
|
||||
}
|
||||
override def hashCode: Int = {
|
||||
37 * (37 * (17 + `type`.##) + commandLine.##)
|
||||
}
|
||||
override def toString: String = {
|
||||
"CommandMessage(" + `type` + ", " + commandLine + ")"
|
||||
}
|
||||
def copy(`type`: String): CommandMessage = {
|
||||
new CommandMessage(`type`, commandLine)
|
||||
}
|
||||
def copy(`type`: String = `type`, commandLine: Option[String] = commandLine): CommandMessage = {
|
||||
new CommandMessage(`type`, commandLine)
|
||||
}
|
||||
def withType(`type`: String): CommandMessage = {
|
||||
copy(`type` = `type`)
|
||||
}
|
||||
def withCommandLine(commandLine: Option[String]): CommandMessage = {
|
||||
copy(commandLine = commandLine)
|
||||
}
|
||||
}
|
||||
object CommandMessage {
|
||||
def apply(`type`: String): CommandMessage = new CommandMessage(`type`, None)
|
||||
def apply(`type`: String, commandLine: Option[String]): CommandMessage = new CommandMessage(`type`, commandLine)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* This code is generated using sbt-datatype.
|
||||
*/
|
||||
|
||||
// DO NOT EDIT MANUALLY
|
||||
package sbt.internal.server
|
||||
final class EventMessage(
|
||||
val `type`: String,
|
||||
val status: Option[String],
|
||||
val commandQueue: Vector[String],
|
||||
val level: Option[String],
|
||||
val message: Option[String],
|
||||
val success: Option[Boolean],
|
||||
val commandLine: Option[String]) extends Serializable {
|
||||
|
||||
def this(`type`: String) = this(`type`, None, Vector(), None, None, None, None)
|
||||
|
||||
override def equals(o: Any): Boolean = o match {
|
||||
case x: EventMessage => (this.`type` == x.`type`) && (this.status == x.status) && (this.commandQueue == x.commandQueue) && (this.level == x.level) && (this.message == x.message) && (this.success == x.success) && (this.commandLine == x.commandLine)
|
||||
case _ => false
|
||||
}
|
||||
override def hashCode: Int = {
|
||||
37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + `type`.##) + status.##) + commandQueue.##) + level.##) + message.##) + success.##) + commandLine.##)
|
||||
}
|
||||
override def toString: String = {
|
||||
"EventMessage(" + `type` + ", " + status + ", " + commandQueue + ", " + level + ", " + message + ", " + success + ", " + commandLine + ")"
|
||||
}
|
||||
def copy(`type`: String): EventMessage = {
|
||||
new EventMessage(`type`, status, commandQueue, level, message, success, commandLine)
|
||||
}
|
||||
def copy(`type`: String = `type`, status: Option[String] = status, commandQueue: Vector[String] = commandQueue, level: Option[String] = level, message: Option[String] = message, success: Option[Boolean] = success, commandLine: Option[String] = commandLine): EventMessage = {
|
||||
new EventMessage(`type`, status, commandQueue, level, message, success, commandLine)
|
||||
}
|
||||
def withType(`type`: String): EventMessage = {
|
||||
copy(`type` = `type`)
|
||||
}
|
||||
def withStatus(status: Option[String]): EventMessage = {
|
||||
copy(status = status)
|
||||
}
|
||||
def withCommandQueue(commandQueue: Vector[String]): EventMessage = {
|
||||
copy(commandQueue = commandQueue)
|
||||
}
|
||||
def withLevel(level: Option[String]): EventMessage = {
|
||||
copy(level = level)
|
||||
}
|
||||
def withMessage(message: Option[String]): EventMessage = {
|
||||
copy(message = message)
|
||||
}
|
||||
def withSuccess(success: Option[Boolean]): EventMessage = {
|
||||
copy(success = success)
|
||||
}
|
||||
def withCommandLine(commandLine: Option[String]): EventMessage = {
|
||||
copy(commandLine = commandLine)
|
||||
}
|
||||
}
|
||||
object EventMessage {
|
||||
def apply(`type`: String): EventMessage = new EventMessage(`type`, None, Vector(), None, None, None, None)
|
||||
def apply(`type`: String, status: Option[String], commandQueue: Vector[String], level: Option[String], message: Option[String], success: Option[Boolean], commandLine: Option[String]): EventMessage = new EventMessage(`type`, status, commandQueue, level, message, success, commandLine)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* This code is generated using sbt-datatype.
|
||||
*/
|
||||
|
||||
// DO NOT EDIT MANUALLY
|
||||
package sbt.internal.server.codec
|
||||
import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder }
|
||||
trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol =>
|
||||
implicit lazy val CommandMessageFormat: JsonFormat[sbt.internal.server.CommandMessage] = new JsonFormat[sbt.internal.server.CommandMessage] {
|
||||
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.server.CommandMessage = {
|
||||
jsOpt match {
|
||||
case Some(js) =>
|
||||
unbuilder.beginObject(js)
|
||||
val `type` = unbuilder.readField[String]("type")
|
||||
val commandLine = unbuilder.readField[Option[String]]("commandLine")
|
||||
unbuilder.endObject()
|
||||
new sbt.internal.server.CommandMessage(`type`, commandLine)
|
||||
case None =>
|
||||
deserializationError("Expected JsObject but found None")
|
||||
}
|
||||
}
|
||||
override def write[J](obj: sbt.internal.server.CommandMessage, builder: Builder[J]): Unit = {
|
||||
builder.beginObject()
|
||||
builder.addField("type", obj.`type`)
|
||||
builder.addField("commandLine", obj.commandLine)
|
||||
builder.endObject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* This code is generated using sbt-datatype.
|
||||
*/
|
||||
|
||||
// DO NOT EDIT MANUALLY
|
||||
package sbt.internal.server.codec
|
||||
import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder }
|
||||
trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol =>
|
||||
implicit lazy val EventMessageFormat: JsonFormat[sbt.internal.server.EventMessage] = new JsonFormat[sbt.internal.server.EventMessage] {
|
||||
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.server.EventMessage = {
|
||||
jsOpt match {
|
||||
case Some(js) =>
|
||||
unbuilder.beginObject(js)
|
||||
val `type` = unbuilder.readField[String]("type")
|
||||
val status = unbuilder.readField[Option[String]]("status")
|
||||
val commandQueue = unbuilder.readField[Vector[String]]("commandQueue")
|
||||
val level = unbuilder.readField[Option[String]]("level")
|
||||
val message = unbuilder.readField[Option[String]]("message")
|
||||
val success = unbuilder.readField[Option[Boolean]]("success")
|
||||
val commandLine = unbuilder.readField[Option[String]]("commandLine")
|
||||
unbuilder.endObject()
|
||||
new sbt.internal.server.EventMessage(`type`, status, commandQueue, level, message, success, commandLine)
|
||||
case None =>
|
||||
deserializationError("Expected JsObject but found None")
|
||||
}
|
||||
}
|
||||
override def write[J](obj: sbt.internal.server.EventMessage, builder: Builder[J]): Unit = {
|
||||
builder.beginObject()
|
||||
builder.addField("type", obj.`type`)
|
||||
builder.addField("status", obj.status)
|
||||
builder.addField("commandQueue", obj.commandQueue)
|
||||
builder.addField("level", obj.level)
|
||||
builder.addField("message", obj.message)
|
||||
builder.addField("success", obj.success)
|
||||
builder.addField("commandLine", obj.commandLine)
|
||||
builder.endObject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"codecNamespace": "sbt.internal.server.codec",
|
||||
"types": [
|
||||
{
|
||||
"name": "CommandMessage",
|
||||
"namespace": "sbt.internal.server",
|
||||
"type": "record",
|
||||
"target": "Scala",
|
||||
"fields": [
|
||||
{
|
||||
"name": "type",
|
||||
"type": "String",
|
||||
"since": "0.0.0"
|
||||
},
|
||||
{
|
||||
"name": "commandLine",
|
||||
"type": "String?",
|
||||
"default": "None",
|
||||
"since": "0.1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "EventMessage",
|
||||
"namespace": "sbt.internal.server",
|
||||
"type": "record",
|
||||
"target": "Scala",
|
||||
"fields": [
|
||||
{
|
||||
"name": "type",
|
||||
"type": "String",
|
||||
"since": "0.0.0"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"type": "String?",
|
||||
"default": "None",
|
||||
"since": "0.1.0"
|
||||
},
|
||||
{
|
||||
"name": "commandQueue",
|
||||
"type": "String*",
|
||||
"default": "Vector()",
|
||||
"since": "0.1.0"
|
||||
},
|
||||
{
|
||||
"name": "level",
|
||||
"type": "String?",
|
||||
"default": "None",
|
||||
"since": "0.1.0"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"type": "String?",
|
||||
"default": "None",
|
||||
"since": "0.1.0"
|
||||
},
|
||||
{
|
||||
"name": "success",
|
||||
"type": "boolean?",
|
||||
"default": "None",
|
||||
"since": "0.1.0"
|
||||
},
|
||||
{
|
||||
"name": "commandLine",
|
||||
"type": "String?",
|
||||
"default": "None",
|
||||
"since": "0.1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -149,6 +149,9 @@ object BasicCommandStrings {
|
|||
def Shell = "shell"
|
||||
def ShellDetailed = "Provides an interactive prompt from which commands can be run."
|
||||
|
||||
def Server = "server"
|
||||
def ServerDetailed = "Provides a network server and an interactive prompt from which commands can be run."
|
||||
|
||||
def StashOnFailure = "sbtStashOnFailure"
|
||||
def PopOnFailure = "sbtPopOnFailure"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import sbt.internal.util.complete.{ Completion, Completions, DefaultParsers, His
|
|||
import sbt.internal.util.Types.{ const, idFun }
|
||||
import sbt.internal.inc.classpath.ClasspathUtilities.toLoader
|
||||
import sbt.internal.inc.ModuleUtilities
|
||||
import sbt.internal.{ Exec, CommandSource, CommandStatus }
|
||||
import DefaultParsers._
|
||||
import Function.tupled
|
||||
import Command.applyEffect
|
||||
|
|
@ -15,11 +16,12 @@ import BasicKeys._
|
|||
|
||||
import java.io.File
|
||||
import sbt.io.IO
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
object BasicCommands {
|
||||
lazy val allBasicCommands = Seq(nop, ignore, help, completionsCommand, multi, ifLast, append, setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, reboot, call, early, exit, continuous, history, shell, read, alias) ++ compatCommands
|
||||
lazy val allBasicCommands = Seq(nop, ignore, help, completionsCommand, multi, ifLast, append, setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, reboot, call, early, exit, continuous, history, shell, server, read, alias) ++ compatCommands
|
||||
|
||||
def nop = Command.custom(s => success(() => s))
|
||||
def ignore = Command.command(FailureWall)(idFun)
|
||||
|
|
@ -191,6 +193,17 @@ object BasicCommands {
|
|||
}
|
||||
}
|
||||
|
||||
def server = Command.command(Server, Help.more(Server, ServerDetailed)) { s0 =>
|
||||
val exchange = State.exchange
|
||||
val s1 = exchange.run(s0)
|
||||
exchange.publishStatus(CommandStatus(s0, true), None)
|
||||
val Exec(source, line) = exchange.blockUntilNextExec
|
||||
val newState = s1.copy(onFailure = Some(Server), remainingCommands = line +: Server +: s1.remainingCommands).setInteractive(true)
|
||||
exchange.publishStatus(CommandStatus(newState, false), Some(source))
|
||||
if (line.trim.isEmpty) newState
|
||||
else newState.clearGlobalLog
|
||||
}
|
||||
|
||||
def read = Command.make(ReadCommand, Help.more(ReadCommand, ReadDetailed))(s => applyEffect(readParser(s))(doRead(s)))
|
||||
def readParser(s: State) =
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ object BasicKeys {
|
|||
val historyPath = AttributeKey[Option[File]]("history", "The location where command line history is persisted.", 40)
|
||||
val shellPrompt = AttributeKey[State => String]("shell-prompt", "The function that constructs the command prompt from the current build state.", 10000)
|
||||
val watch = AttributeKey[Watched]("watch", "Continuous execution configuration.", 1000)
|
||||
val serverPort = AttributeKey[Int]("server-port", "The port number used by server command.", 10000)
|
||||
private[sbt] val interactive = AttributeKey[Boolean]("interactive", "True if commands are currently being entered from an interactive environment.", 10)
|
||||
private[sbt] val classLoaderCache = AttributeKey[ClassLoaderCache]("class-loader-cache", "Caches class loaders based on the classpath entries and last modified times.", 10)
|
||||
private[sbt] val OnFailureStack = AttributeKey[List[Option[String]]]("on-failure-stack", "Stack that remembers on-failure handlers.", 10)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import java.util.concurrent.Callable
|
|||
import sbt.util.Logger
|
||||
import sbt.internal.util.{ AttributeKey, AttributeMap, ErrorHandling, ExitHook, ExitHooks, GlobalLogging }
|
||||
import sbt.internal.util.complete.HistoryCommands
|
||||
import sbt.internal.CommandExchange
|
||||
import sbt.internal.inc.classpath.ClassLoaderCache
|
||||
|
||||
/**
|
||||
|
|
@ -178,6 +179,8 @@ object State {
|
|||
new Reboot(app.scalaProvider.version, state.remainingCommands, app.id, state.configuration.baseDirectory)
|
||||
}
|
||||
|
||||
private[sbt] lazy val exchange = new CommandExchange()
|
||||
|
||||
/** Provides operations and transformations on State. */
|
||||
implicit def stateOps(s: State): StateOps = new StateOps {
|
||||
def process(f: (String, State) => State): State =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
/**
|
||||
* A command channel represents an IO device such as network socket or human
|
||||
* that can issue command or listen for some outputs.
|
||||
* We can think of a command channel to be an abstration of the terminal window.
|
||||
*/
|
||||
abstract class CommandChannel {
|
||||
private val commandQueue: ConcurrentLinkedQueue[Exec] = new ConcurrentLinkedQueue()
|
||||
def append(exec: Exec): Boolean =
|
||||
commandQueue.add(exec)
|
||||
def poll: Option[Exec] = Option(commandQueue.poll)
|
||||
|
||||
/** start listening for a command exec. */
|
||||
def run(s: State): State
|
||||
def publishStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit
|
||||
def shutdown(): Unit
|
||||
}
|
||||
|
||||
case class Exec(source: CommandSource, commandLine: String)
|
||||
|
||||
sealed trait CommandSource
|
||||
object CommandSource {
|
||||
case object Human extends CommandSource
|
||||
case object Network extends CommandSource
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a data that is passed on to the channels.
|
||||
* The canEnter paramter indicates that the console devise or UI
|
||||
* should stop listening.
|
||||
*/
|
||||
case class CommandStatus(state: State, canEnter: Boolean)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
/**
|
||||
* The command exchange merges multiple command channels (e.g. network and console),
|
||||
* and acts as the central multiplexing point.
|
||||
* Instead of blocking on JLine.readLine, the server commmand will block on
|
||||
* this exchange, which could serve command request from either of the channel.
|
||||
*/
|
||||
private[sbt] final class CommandExchange {
|
||||
private val commandQueue: ConcurrentLinkedQueue[Exec] = new ConcurrentLinkedQueue()
|
||||
private val channelBuffer: ListBuffer[CommandChannel] = new ListBuffer()
|
||||
def channels: List[CommandChannel] = channelBuffer.toList
|
||||
def subscribe(c: CommandChannel): Unit =
|
||||
channelBuffer.append(c)
|
||||
|
||||
subscribe(new ConsoleChannel())
|
||||
subscribe(new NetworkChannel())
|
||||
|
||||
// periodically move all messages from all the channels
|
||||
@tailrec def blockUntilNextExec: Exec =
|
||||
{
|
||||
@tailrec def slurpMessages(): Unit =
|
||||
(((None: Option[Exec]) /: channels) { _ orElse _.poll }) match {
|
||||
case Some(x) =>
|
||||
commandQueue.add(x)
|
||||
slurpMessages
|
||||
case _ => ()
|
||||
}
|
||||
slurpMessages()
|
||||
Option(commandQueue.poll) match {
|
||||
case Some(x) => x
|
||||
case _ =>
|
||||
Thread.sleep(50)
|
||||
blockUntilNextExec
|
||||
}
|
||||
}
|
||||
|
||||
// fanout run to all channels
|
||||
def run(s: State): State =
|
||||
(s /: channels) { (acc, c) => c.run(acc) }
|
||||
|
||||
// fanout publishStatus to all channels
|
||||
def publishStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit =
|
||||
channels foreach { c =>
|
||||
c.publishStatus(status, lastSource)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import sbt.internal.util._
|
||||
import BasicKeys._
|
||||
import java.io.File
|
||||
|
||||
private[sbt] final class ConsoleChannel extends CommandChannel {
|
||||
private var askUserThread: Option[Thread] = None
|
||||
def makeAskUserThread(status: CommandStatus): Thread = new Thread("ask-user-thread") {
|
||||
val s = status.state
|
||||
val history = (s get historyPath) getOrElse Some(new File(s.baseDir, ".history"))
|
||||
val prompt = (s get shellPrompt) match {
|
||||
case Some(pf) => pf(s)
|
||||
case None => "> "
|
||||
}
|
||||
val reader = new FullReader(history, s.combinedParser, JLine.HandleCONT, true)
|
||||
override def run(): Unit = {
|
||||
// This internally handles thread interruption and returns Some("")
|
||||
val line = reader.readLine(prompt)
|
||||
line match {
|
||||
case Some(cmd) => append(Exec(CommandSource.Human, cmd))
|
||||
case None => append(Exec(CommandSource.Human, "exit"))
|
||||
}
|
||||
askUserThread = None
|
||||
}
|
||||
}
|
||||
|
||||
def run(s: State): State = s
|
||||
|
||||
def publishStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit =
|
||||
if (status.canEnter) {
|
||||
askUserThread match {
|
||||
case Some(x) => //
|
||||
case _ =>
|
||||
val x = makeAskUserThread(status)
|
||||
askUserThread = Some(x)
|
||||
x.start
|
||||
}
|
||||
} else {
|
||||
lastSource match {
|
||||
case Some(src) if src != CommandSource.Human =>
|
||||
askUserThread match {
|
||||
case Some(x) =>
|
||||
shutdown()
|
||||
case _ =>
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
def shutdown(): Unit =
|
||||
askUserThread match {
|
||||
case Some(x) if x.isAlive =>
|
||||
x.interrupt
|
||||
askUserThread = None
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import sbt.internal.server._
|
||||
import BasicKeys._
|
||||
|
||||
private[sbt] final class NetworkChannel extends CommandChannel {
|
||||
private var server: Option[ServerInstance] = None
|
||||
|
||||
def run(s: State): State =
|
||||
{
|
||||
val port = (s get serverPort) match {
|
||||
case Some(x) => x
|
||||
case None => 5001
|
||||
}
|
||||
def onCommand(command: internal.server.Command): Unit = {
|
||||
command match {
|
||||
case Execution(cmd) => append(Exec(CommandSource.Network, cmd))
|
||||
}
|
||||
}
|
||||
server match {
|
||||
case Some(x) => // do nothing
|
||||
case _ =>
|
||||
server = Some(Server.start("127.0.0.1", port, onCommand, s.log))
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
def shutdown(): Unit =
|
||||
{
|
||||
// interrupt and kill the thread
|
||||
server.foreach(_.shutdown())
|
||||
server = None
|
||||
}
|
||||
|
||||
def publishStatus(cmdStatus: CommandStatus, lastSource: Option[CommandSource]): Unit = {
|
||||
server.foreach(server =>
|
||||
server.publish(
|
||||
if (cmdStatus.canEnter) StatusEvent(Ready)
|
||||
else StatusEvent(Processing("TODO current command", cmdStatus.state.remainingCommands))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2016 Lightbend Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
package sbt
|
||||
package internal
|
||||
package server
|
||||
|
||||
import java.net.{ SocketTimeoutException, Socket }
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
abstract class ClientConnection(connection: Socket) {
|
||||
|
||||
private val running = new AtomicBoolean(true)
|
||||
private val delimiter: Byte = '\n'.toByte
|
||||
|
||||
private val out = connection.getOutputStream
|
||||
|
||||
val thread = new Thread(s"sbt-client-${connection.getPort}") {
|
||||
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
|
||||
while (bytesRead != -1 && running.get) {
|
||||
try {
|
||||
bytesRead = in.read(readBuffer)
|
||||
val bytes = readBuffer.toVector.take(bytesRead)
|
||||
buffer = buffer ++ bytes
|
||||
|
||||
// handle un-framing
|
||||
val delimPos = bytes.indexOf(delimiter)
|
||||
if (delimPos > 0) {
|
||||
val chunk = buffer.take(delimPos)
|
||||
buffer = buffer.drop(delimPos + 1)
|
||||
|
||||
Serialization.deserialize(chunk).fold(
|
||||
errorDesc => println("Got invalid chunk from client: " + errorDesc),
|
||||
onCommand
|
||||
)
|
||||
}
|
||||
|
||||
} catch {
|
||||
case _: SocketTimeoutException => // its ok
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
thread.start()
|
||||
|
||||
def publish(event: Array[Byte]): Unit = {
|
||||
out.write(event)
|
||||
out.write(delimiter)
|
||||
out.flush()
|
||||
}
|
||||
|
||||
def onCommand(command: Command): Unit
|
||||
|
||||
def shutdown(): Unit = {
|
||||
println("Shutting down client connection")
|
||||
running.set(false)
|
||||
out.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (C) 2016 Lightbend Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
package sbt
|
||||
package internal
|
||||
package server
|
||||
|
||||
import sjsonnew.{ JsonFormat, BasicJsonProtocol }
|
||||
import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter }
|
||||
import scala.json.ast.unsafe.JValue
|
||||
import sjsonnew.support.scalajson.unsafe.Parser
|
||||
import java.nio.ByteBuffer
|
||||
import scala.util.{ Success, Failure }
|
||||
|
||||
object Serialization {
|
||||
|
||||
def serialize(event: Event): Array[Byte] =
|
||||
{
|
||||
import ServerCodec._
|
||||
val msg = toMessage(event)
|
||||
val json: JValue = Converter.toJson[EventMessage](msg).get
|
||||
CompactPrinter(json).getBytes("UTF-8")
|
||||
}
|
||||
def toMessage(event: Event): EventMessage =
|
||||
event match {
|
||||
case LogEvent(level, message) =>
|
||||
EventMessage(`type` = "logEvent",
|
||||
status = None, commandQueue = Vector(),
|
||||
level = Some(level), message = Some(message), success = None, commandLine = None)
|
||||
case StatusEvent(Ready) =>
|
||||
EventMessage(`type` = "statusEvent",
|
||||
status = Some("ready"), commandQueue = Vector(),
|
||||
level = None, message = None, success = None, commandLine = None)
|
||||
case StatusEvent(Processing(command, commandQueue)) =>
|
||||
EventMessage(`type` = "statusEvent",
|
||||
status = Some("processing"), commandQueue = commandQueue.toVector,
|
||||
level = None, message = None, success = None, commandLine = None)
|
||||
case ExecutionEvent(command, status) =>
|
||||
EventMessage(`type` = "executionEvent",
|
||||
status = None, commandQueue = Vector(),
|
||||
level = None, message = None, success = Some(status), commandLine = Some(command))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A command or an invalid input description
|
||||
*/
|
||||
def deserialize(bytes: Seq[Byte]): Either[String, Command] =
|
||||
{
|
||||
val buffer = ByteBuffer.wrap(bytes.toArray)
|
||||
Parser.parseFromByteBuffer(buffer) match {
|
||||
case Success(json) =>
|
||||
import ServerCodec._
|
||||
Converter.fromJson[CommandMessage](json) match {
|
||||
case Success(command) =>
|
||||
command.`type` match {
|
||||
case "exec" =>
|
||||
command.commandLine match {
|
||||
case Some(cmd) => Right(Execution(cmd))
|
||||
case None => Left("Missing or invalid command_line field")
|
||||
}
|
||||
case cmd => Left(s"Unknown command type $cmd")
|
||||
}
|
||||
case Failure(e) => Left(e.getMessage)
|
||||
}
|
||||
case Failure(e) =>
|
||||
Left(s"Parse error: ${e.getMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ServerCodec extends ServerCodec
|
||||
trait ServerCodec extends codec.EventMessageFormats with codec.CommandMessageFormats with BasicJsonProtocol
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (C) 2016 Lightbend Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
package sbt
|
||||
package internal
|
||||
package server
|
||||
|
||||
import java.net.{ SocketTimeoutException, InetAddress, ServerSocket, SocketException }
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import sbt.util.Logger
|
||||
import scala.collection.mutable
|
||||
|
||||
private[sbt] sealed trait ServerInstance {
|
||||
def shutdown(): Unit
|
||||
def publish(event: Event): Unit
|
||||
}
|
||||
|
||||
private[sbt] object Server {
|
||||
def start(host: String, port: Int, onIncommingCommand: Command => Unit, log: Logger): ServerInstance =
|
||||
new ServerInstance {
|
||||
|
||||
val lock = new AnyRef {}
|
||||
val clients: mutable.ListBuffer[ClientConnection] = mutable.ListBuffer.empty
|
||||
val running = new AtomicBoolean(true)
|
||||
|
||||
val serverThread = new Thread("sbt-socket-server") {
|
||||
|
||||
override def run(): Unit = {
|
||||
val serverSocket = new ServerSocket(port, 50, InetAddress.getByName(host))
|
||||
serverSocket.setSoTimeout(5000)
|
||||
|
||||
log.info(s"sbt server started at $host:$port")
|
||||
while (running.get()) {
|
||||
try {
|
||||
val socket = serverSocket.accept()
|
||||
log.info(s"new client connected from: ${socket.getPort}")
|
||||
|
||||
val connection = new ClientConnection(socket) {
|
||||
override def onCommand(command: Command): Unit = {
|
||||
onIncommingCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
lock.synchronized {
|
||||
clients += connection
|
||||
}
|
||||
|
||||
} catch {
|
||||
case _: SocketTimeoutException => // its ok
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
serverThread.start()
|
||||
|
||||
/** Publish an event to all connected clients */
|
||||
def publish(event: Event): Unit = {
|
||||
// TODO do not do this on the calling thread
|
||||
val bytes = Serialization.serialize(event)
|
||||
lock.synchronized {
|
||||
val toDel: mutable.ListBuffer[ClientConnection] = mutable.ListBuffer.empty
|
||||
clients.foreach { client =>
|
||||
try {
|
||||
client.publish(bytes)
|
||||
} catch {
|
||||
case e: SocketException =>
|
||||
log.debug(e.getMessage)
|
||||
toDel += client
|
||||
}
|
||||
}
|
||||
clients --= toDel.toList
|
||||
}
|
||||
}
|
||||
|
||||
override def shutdown(): Unit = {
|
||||
log.info("shutting down server")
|
||||
running.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2016 Lightbend Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
package sbt
|
||||
package internal
|
||||
package server
|
||||
|
||||
/*
|
||||
* These classes are the protocol for client-server interaction,
|
||||
* commands can come from the client side, while events are emitted
|
||||
* from sbt to inform the client of state changes etc.
|
||||
*/
|
||||
private[sbt] sealed trait Event
|
||||
|
||||
private[sbt] final case class LogEvent(level: String, message: String) extends Event
|
||||
|
||||
sealed trait Status
|
||||
private[sbt] final case object Ready extends Status
|
||||
private[sbt] final case class Processing(command: String, commandQueue: Seq[String]) extends Status
|
||||
|
||||
private[sbt] final case class StatusEvent(status: Status) extends Event
|
||||
private[sbt] final case class ExecutionEvent(command: String, success: Boolean) extends Event
|
||||
|
||||
private[sbt] sealed trait Command
|
||||
|
||||
private[sbt] final case class Execution(cmd: String) extends Command
|
||||
|
|
@ -41,7 +41,7 @@ import sbt.util.InterfaceUtil.{ f1, o2m }
|
|||
import sbt.internal.util.Types._
|
||||
|
||||
import sbt.internal.io.WatchState
|
||||
import sbt.io.{ AllPassFilter, FileFilter, GlobFilter, HiddenFileFilter, IO, NameFilter, NothingFilter, Path, PathFinder, SimpleFileFilter, DirectoryFilter }
|
||||
import sbt.io.{ AllPassFilter, FileFilter, GlobFilter, HiddenFileFilter, IO, NameFilter, NothingFilter, Path, PathFinder, SimpleFileFilter, DirectoryFilter, Hash }
|
||||
|
||||
import Path._
|
||||
import sbt.io.syntax._
|
||||
|
|
@ -200,7 +200,8 @@ object Defaults extends BuildCommon {
|
|||
fork :== false,
|
||||
initialize :== {},
|
||||
forcegc :== sys.props.get("sbt.task.forcegc").map(java.lang.Boolean.parseBoolean).getOrElse(GCUtil.defaultForceGarbageCollection),
|
||||
minForcegcInterval :== GCUtil.defaultMinForcegcInterval
|
||||
minForcegcInterval :== GCUtil.defaultMinForcegcInterval,
|
||||
serverPort := 5000 + (Hash.toHex(Hash(appConfiguration.value.baseDirectory.toString)).## % 1000)
|
||||
))
|
||||
def defaultTestTasks(key: Scoped): Seq[Setting[_]] = inTask(key)(Seq(
|
||||
tags := Seq(Tags.Test -> 1),
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ object Keys {
|
|||
// Command keys
|
||||
val historyPath = SettingKey(BasicKeys.historyPath)
|
||||
val shellPrompt = SettingKey(BasicKeys.shellPrompt)
|
||||
val serverPort = SettingKey(BasicKeys.serverPort)
|
||||
val analysis = AttributeKey[CompileAnalysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.", DSetting)
|
||||
val watch = SettingKey(BasicKeys.watch)
|
||||
val pollInterval = SettingKey[Int]("poll-interval", "Interval between checks for modified sources by the continuous execution command.", BMinusSetting)
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ object BuiltinCommands {
|
|||
def DefaultCommands: Seq[Command] = Seq(ignore, help, completionsCommand, about, tasks, settingsCommand, loadProject,
|
||||
projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion,
|
||||
Cross.crossRestoreSession, setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins,
|
||||
ifLast, multi, shell, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++
|
||||
ifLast, multi, shell, BasicCommands.server, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++
|
||||
compatCommands
|
||||
def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ import java.io.File
|
|||
import java.net.URI
|
||||
import java.util.Locale
|
||||
import Project._
|
||||
import Keys.{ stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings,
|
||||
shellPrompt, watch }
|
||||
import Keys.{ appConfiguration, stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, serverPort, thisProject, thisProjectRef, watch }
|
||||
import Scope.{ GlobalScope, ThisScope }
|
||||
import Def.{ Flattened, Initialize, ScopedKey, Setting }
|
||||
import sbt.internal.{ Load, BuildStructure, LoadedBuild, LoadedBuildUnit, SettingGraph, SettingCompletions, AddSettings, SessionSettings }
|
||||
|
|
@ -417,9 +416,11 @@ object Project extends ProjectExtra {
|
|||
val history = get(historyPath) flatMap idFun
|
||||
val prompt = get(shellPrompt)
|
||||
val watched = get(watch)
|
||||
val port: Option[Int] = get(serverPort)
|
||||
val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true))
|
||||
val newDefinedCommands = commandDefs ++ BasicCommands.removeTagged(s.definedCommands, projectCommand)
|
||||
val newAttrs = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history)
|
||||
val newAttrs0 = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history)
|
||||
val newAttrs = setCond(serverPort.key, port, newAttrs0)
|
||||
s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), definedCommands = newDefinedCommands)
|
||||
}
|
||||
def setCond[T](key: AttributeKey[T], vopt: Option[T], attributes: AttributeMap): AttributeMap =
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ object Dependencies {
|
|||
lazy val compilerClasspath = "org.scala-sbt" %% "zinc-classpath" % zincVersion
|
||||
lazy val compilerApiInfo = "org.scala-sbt" %% "zinc-apiinfo" % zincVersion
|
||||
lazy val compilerIvyIntegration = "org.scala-sbt" %% "zinc-ivy-integration" % zincVersion
|
||||
lazy val sjsonNewScalaJson = "com.eed3si9n" %% "sjson-new-scalajson" % "0.4.2"
|
||||
|
||||
lazy val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.11.4"
|
||||
lazy val specs2 = "org.specs2" %% "specs2" % "2.3.11"
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
addSbtPlugin("org.scala-sbt" % "sbt-datatype" % "0.2.6")
|
||||
Loading…
Reference in New Issue