diff --git a/build.sbt b/build.sbt index d753b42a8..b60629fef 100644 --- a/build.sbt +++ b/build.sbt @@ -175,9 +175,20 @@ lazy val actionsProj = (project in file("main-actions")). addSbtZinc, addSbtCompilerIvyIntegration, addSbtCompilerInterface, addSbtIO, addSbtUtilLogging, addSbtUtilRelation, addSbtLm, addSbtUtilTracking) +lazy val protocolProj = (project in file("protocol")). + enablePlugins(ContrabandPlugin, JsonCodecPlugin). + settings( + testedBaseSettings, + name := "Protocol", + libraryDependencies ++= Seq(sjsonNewScalaJson), + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala" + ). + configure(addSbtUtilLogging) + // General command support and core commands not specific to a build system lazy val commandProj = (project in file("main-command")). enablePlugins(ContrabandPlugin, JsonCodecPlugin). + dependsOn(protocolProj). settings( testedBaseSettings, name := "Command", @@ -248,7 +259,7 @@ lazy val myProvided = config("provided") intransitive def allProjects = Seq( testingProj, testAgentProj, taskProj, stdTaskProj, runProj, - scriptedSbtProj, scriptedPluginProj, + scriptedSbtProj, scriptedPluginProj, protocolProj, actionsProj, commandProj, mainSettingsProj, mainProj, sbtProj, bundledLauncherProj) def projectsWithMyProvided = allProjects.map(p => p.copy(configurations = (p.configurations.filter(_ != Provided)) :+ myProvided)) diff --git a/main-command/src/main/contraband-scala/CommandSourceFormats.scala b/main-command/src/main/contraband-scala/CommandSourceFormats.scala new file mode 100644 index 000000000..9944196b4 --- /dev/null +++ b/main-command/src/main/contraband-scala/CommandSourceFormats.scala @@ -0,0 +1,26 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait CommandSourceFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val CommandSourceFormat: JsonFormat[sbt.CommandSource] = new JsonFormat[sbt.CommandSource] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.CommandSource = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val channelName = unbuilder.readField[String]("channelName") + unbuilder.endObject() + sbt.CommandSource(channelName) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.CommandSource, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("channelName", obj.channelName) + builder.endObject() + } +} +} diff --git a/main-command/src/main/contraband-scala/ExecFormats.scala b/main-command/src/main/contraband-scala/ExecFormats.scala new file mode 100644 index 000000000..98a0961a4 --- /dev/null +++ b/main-command/src/main/contraband-scala/ExecFormats.scala @@ -0,0 +1,30 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait ExecFormats { self: CommandSourceFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val ExecFormat: JsonFormat[sbt.Exec] = new JsonFormat[sbt.Exec] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.Exec = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val commandLine = unbuilder.readField[String]("commandLine") + val execId = unbuilder.readField[Option[String]]("execId") + val source = unbuilder.readField[Option[sbt.CommandSource]]("source") + unbuilder.endObject() + sbt.Exec(commandLine, execId, source) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.Exec, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("commandLine", obj.commandLine) + builder.addField("execId", obj.execId) + builder.addField("source", obj.source) + builder.endObject() + } +} +} diff --git a/main-command/src/main/contraband-scala/sbt/CommandSource.scala b/main-command/src/main/contraband-scala/sbt/CommandSource.scala new file mode 100644 index 000000000..08b399ea1 --- /dev/null +++ b/main-command/src/main/contraband-scala/sbt/CommandSource.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt +final class CommandSource private ( + val channelName: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: CommandSource => (this.channelName == x.channelName) + case _ => false + } + override def hashCode: Int = { + 37 * (17 + channelName.##) + } + override def toString: String = { + "CommandSource(" + channelName + ")" + } + protected[this] def copy(channelName: String = channelName): CommandSource = { + new CommandSource(channelName) + } + def withChannelName(channelName: String): CommandSource = { + copy(channelName = channelName) + } +} +object CommandSource { + + def apply(channelName: String): CommandSource = new CommandSource(channelName) +} diff --git a/main-command/src/main/contraband-scala/sbt/Exec.scala b/main-command/src/main/contraband-scala/sbt/Exec.scala new file mode 100644 index 000000000..ac3323c35 --- /dev/null +++ b/main-command/src/main/contraband-scala/sbt/Exec.scala @@ -0,0 +1,49 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt +final class Exec private ( + val commandLine: String, + val execId: Option[String], + val source: Option[sbt.CommandSource]) extends Serializable { + + private def this(commandLine: String, source: Option[sbt.CommandSource]) = this(commandLine, None, source) + + override def equals(o: Any): Boolean = o match { + case x: Exec => (this.commandLine == x.commandLine) && (this.execId == x.execId) && (this.source == x.source) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + commandLine.##) + execId.##) + source.##) + } + override def toString: String = { + "Exec(" + commandLine + ", " + execId + ", " + source + ")" + } + protected[this] def copy(commandLine: String = commandLine, execId: Option[String] = execId, source: Option[sbt.CommandSource] = source): Exec = { + new Exec(commandLine, execId, source) + } + def withCommandLine(commandLine: String): Exec = { + copy(commandLine = commandLine) + } + def withExecId(execId: Option[String]): Exec = { + copy(execId = execId) + } + def withExecId(execId: String): Exec = { + copy(execId = Option(execId)) + } + def withSource(source: Option[sbt.CommandSource]): Exec = { + copy(source = source) + } + def withSource(source: sbt.CommandSource): Exec = { + copy(source = Option(source)) + } +} +object Exec { + def newExecId: String = java.util.UUID.randomUUID.toString + def apply(commandLine: String, source: Option[sbt.CommandSource]): Exec = new Exec(commandLine, None, source) + def apply(commandLine: String, source: sbt.CommandSource): Exec = new Exec(commandLine, None, Option(source)) + def apply(commandLine: String, execId: Option[String], source: Option[sbt.CommandSource]): Exec = new Exec(commandLine, execId, source) + def apply(commandLine: String, execId: String, source: sbt.CommandSource): Exec = new Exec(commandLine, Option(execId), Option(source)) +} diff --git a/main-command/src/main/contraband-scala/sbt/internal/server/CommandMessage.scala b/main-command/src/main/contraband-scala/sbt/internal/server/CommandMessage.scala deleted file mode 100644 index 3998b26e7..000000000 --- a/main-command/src/main/contraband-scala/sbt/internal/server/CommandMessage.scala +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This code is generated using sbt-datatype. - */ - -// DO NOT EDIT MANUALLY -package sbt.internal.server -final class CommandMessage private ( - val `type`: String, - val commandLine: Option[String]) extends Serializable { - - private 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 + ")" - } - protected[this] 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) -} diff --git a/main-command/src/main/contraband-scala/sbt/internal/server/EventMessage.scala b/main-command/src/main/contraband-scala/sbt/internal/server/EventMessage.scala deleted file mode 100644 index 7bc176ba7..000000000 --- a/main-command/src/main/contraband-scala/sbt/internal/server/EventMessage.scala +++ /dev/null @@ -1,57 +0,0 @@ -/** - * This code is generated using sbt-datatype. - */ - -// DO NOT EDIT MANUALLY -package sbt.internal.server -final class EventMessage private ( - val `type`: String, - val status: Option[String], - val commandQueue: scala.Vector[String], - val level: Option[String], - val message: Option[String], - val success: Option[Boolean], - val commandLine: Option[String]) extends Serializable { - - private 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 + ")" - } - protected[this] def copy(`type`: String = `type`, status: Option[String] = status, commandQueue: scala.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: scala.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: scala.Vector[String], level: Option[String], message: Option[String], success: Option[Boolean], commandLine: Option[String]): EventMessage = new EventMessage(`type`, status, commandQueue, level, message, success, commandLine) -} diff --git a/main-command/src/main/contraband-scala/sbt/internal/server/codec/CommandMessageFormats.scala b/main-command/src/main/contraband-scala/sbt/internal/server/codec/CommandMessageFormats.scala deleted file mode 100644 index 91ef66421..000000000 --- a/main-command/src/main/contraband-scala/sbt/internal/server/codec/CommandMessageFormats.scala +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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() - 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() - } -} -} diff --git a/main-command/src/main/contraband-scala/sbt/internal/server/codec/EventMessageFormats.scala b/main-command/src/main/contraband-scala/sbt/internal/server/codec/EventMessageFormats.scala deleted file mode 100644 index 7e2170046..000000000 --- a/main-command/src/main/contraband-scala/sbt/internal/server/codec/EventMessageFormats.scala +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 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[scala.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() - 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() - } -} -} diff --git a/main-command/src/main/contraband/server.json b/main-command/src/main/contraband/server.json deleted file mode 100644 index 4e9ed1fa7..000000000 --- a/main-command/src/main/contraband/server.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "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": "Option[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": "Option[String]", - "default": "None", - "since": "0.1.0" - }, - { - "name": "commandQueue", - "type": "scala.Vector[String]", - "default": "Vector()", - "since": "0.1.0" - }, - { - "name": "level", - "type": "Option[String]", - "default": "None", - "since": "0.1.0" - }, - { - "name": "message", - "type": "Option[String]", - "default": "None", - "since": "0.1.0" - }, - { - "name": "success", - "type": "Option[Boolean]", - "default": "None", - "since": "0.1.0" - }, - { - "name": "commandLine", - "type": "Option[String]", - "default": "None", - "since": "0.1.0" - } - ] - } - ] -} diff --git a/main-command/src/main/contraband/state.contra b/main-command/src/main/contraband/state.contra new file mode 100644 index 000000000..929523505 --- /dev/null +++ b/main-command/src/main/contraband/state.contra @@ -0,0 +1,14 @@ +package sbt +@target(Scala) + +type Exec { + commandLine: String! + execId: String @since("0.0.1") + source: sbt.CommandSource + + #xcompanion def newExecId: String = java.util.UUID.randomUUID.toString +} + +type CommandSource { + channelName: String! +} diff --git a/main-command/src/main/scala/sbt/BasicCommandStrings.scala b/main-command/src/main/scala/sbt/BasicCommandStrings.scala index bfd424a65..d99793259 100644 --- a/main-command/src/main/scala/sbt/BasicCommandStrings.scala +++ b/main-command/src/main/scala/sbt/BasicCommandStrings.scala @@ -152,6 +152,9 @@ object BasicCommandStrings { def Server = "server" def ServerDetailed = "Provides a network server and an interactive prompt from which commands can be run." + def Client = "client" + def ClientDetailed = "Provides an interactive prompt from which commands can be run on a server." + def StashOnFailure = "sbtStashOnFailure" def PopOnFailure = "sbtPopOnFailure" diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 12e0ef49b..e0f744758 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -6,7 +6,8 @@ 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 sbt.internal.{ ConsolePromptEvent, ConsoleUnpromptEvent } +import sbt.internal.client.NetworkClient import DefaultParsers._ import Function.tupled import Command.applyEffect @@ -16,12 +17,11 @@ 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, server, 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, client, read, alias) ++ compatCommands def nop = Command.custom(s => success(() => s)) def ignore = Command.command(FailureWall)(idFun) @@ -82,13 +82,13 @@ object BasicCommands { state } - def multiParser(s: State): Parser[Seq[String]] = + def multiParser(s: State): Parser[List[String]] = { val nonSemi = token(charClass(_ != ';').+, hide = const(true)) - (token(';' ~> OptSpace) flatMap { _ => matched((s.combinedParser & nonSemi) | nonSemi) <~ token(OptSpace) } map (_.trim)).+ + (token(';' ~> OptSpace) flatMap { _ => matched((s.combinedParser & nonSemi) | nonSemi) <~ token(OptSpace) } map (_.trim)).+ map { _.toList } } - def multiApplied(s: State) = + def multiApplied(s: State): Parser[() => State] = Command.applyEffect(multiParser(s))(_ ::: s) def multi = Command.custom(multiApplied, Help(Multi, MultiBrief, MultiDetailed)) @@ -101,11 +101,11 @@ object BasicCommands { if (s.remainingCommands.isEmpty) arg :: s else s } def append = Command(AppendCommand, Help.more(AppendCommand, AppendLastDetailed))(otherCommandParser) { (s, arg) => - s.copy(remainingCommands = s.remainingCommands :+ arg) + s.copy(remainingCommands = s.remainingCommands :+ Exec(arg, s.source)) } def setOnFailure = Command(OnFailure, Help.more(OnFailure, OnFailureDetailed))(otherCommandParser) { (s, arg) => - s.copy(onFailure = Some(arg)) + s.copy(onFailure = Some(Exec(arg, s.source))) } private[sbt] def compatCommands = Seq( Command.command(Compat.ClearOnFailure) { s => @@ -114,7 +114,7 @@ object BasicCommands { }, Command.arb(s => token(Compat.OnFailure, hide = const(true)).flatMap(x => otherCommandParser(s))) { (s, arg) => s.log.warn(Compat.OnFailureDeprecated) - s.copy(onFailure = Some(arg)) + s.copy(onFailure = Some(Exec(arg, s.source))) }, Command.command(Compat.FailureWall) { s => s.log.warn(Compat.FailureWallDeprecated) @@ -187,7 +187,7 @@ object BasicCommands { val line = reader.readLine(prompt) line match { case Some(line) => - val newState = s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands).setInteractive(true) + val newState = s.copy(onFailure = Some(Exec(Shell, None)), remainingCommands = Exec(line, s.source) +: Exec(Shell, None) +: s.remainingCommands).setInteractive(true) if (line.trim.isEmpty) newState else newState.clearGlobalLog case None => s.setInteractive(false) } @@ -196,14 +196,29 @@ 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 + exchange.publishEvent(ConsolePromptEvent(s0)) + val exec: Exec = exchange.blockUntilNextExec + val newState = s1.copy(onFailure = Some(Exec(Server, None)), remainingCommands = exec +: Exec(Server, None) +: s1.remainingCommands).setInteractive(true) + exchange.publishEvent(ConsoleUnpromptEvent(exec.source)) + if (exec.commandLine.trim.isEmpty) newState else newState.clearGlobalLog } + def client = Command.make(Client, Help.more(Client, ClientDetailed))(clientParser) + def clientParser(s0: State) = + { + val p = (token(Space) ~> repsep(StringBasic, token(Space))) | (token(EOF) map { case _ => Nil }) + applyEffect(p)({ inputArg => + val arguments = inputArg.toList ++ + (s0.remainingCommands.toList match { + case e :: Nil if e.commandLine == "shell" :: Nil => Nil + case xs => xs map { _.commandLine } + }) + NetworkClient.run(arguments) + "exit" :: s0.copy(remainingCommands = Nil) + }) + } + def read = Command.make(ReadCommand, Help.more(ReadCommand, ReadDetailed))(s => applyEffect(readParser(s))(doRead(s))) def readParser(s: State) = { @@ -217,7 +232,7 @@ object BasicCommands { val port = math.abs(portAndSuccess) val previousSuccess = portAndSuccess >= 0 readMessage(port, previousSuccess) match { - case Some(message) => (message :: (ReadCommand + " " + port) :: s).copy(onFailure = Some(ReadCommand + " " + (-port))) + case Some(message) => (message :: (ReadCommand + " " + port) :: s).copy(onFailure = Some(Exec(ReadCommand + " " + (-port), s.source))) case None => System.err.println("Connection closed.") s.fail @@ -225,7 +240,7 @@ object BasicCommands { case Right(from) => val notFound = notReadable(from) if (notFound.isEmpty) - readLines(from) ::: s // this means that all commands from all files are loaded, parsed, and inserted before any are executed + readLines(from).toList ::: s // this means that all commands from all files are loaded, parsed, and inserted before any are executed else { s.log.error("Command file(s) not readable: \n\t" + notFound.mkString("\n\t")) s diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index d42a1d381..3b174083e 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -11,6 +11,6 @@ object BasicKeys { 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) + private[sbt] val OnFailureStack = AttributeKey[List[Option[Exec]]]("on-failure-stack", "Stack that remembers on-failure handlers.", 10) private[sbt] val explicitGlobalLogLevels = AttributeKey[Boolean]("explicit-global-log-levels", "True if the global logging levels were explicitly set by the user.", 10) } diff --git a/main-command/src/main/scala/sbt/Command.scala b/main-command/src/main/scala/sbt/Command.scala index 9764697be..13321ab2a 100644 --- a/main-command/src/main/scala/sbt/Command.scala +++ b/main-command/src/main/scala/sbt/Command.scala @@ -7,6 +7,7 @@ import sbt.internal.inc.ReflectUtilities import sbt.internal.util.complete.{ DefaultParsers, EditDistance, Parser } import sbt.internal.util.Types.const import sbt.internal.util.{ AttributeKey, AttributeMap, Util } +import sbt.protocol.ExecStatusEvent sealed trait Command { def help: State => Help @@ -87,15 +88,20 @@ object Command { } } - def process(command: String, state: State): State = + /** This is the main function State transfer function of the sbt command processing, called by MainLoop.next, */ + def process(exec: Exec, state: State): State = { + val channelName = exec.source map { _.channelName } + State.exchange.publishEvent(ExecStatusEvent("Processing", channelName, exec.execId, Vector())) val parser = combine(state.definedCommands) - parse(command, parser(state)) match { + val newState = parse(exec.commandLine, parser(state)) match { case Right(s) => s() // apply command. command side effects happen here case Left(errMsg) => state.log.error(errMsg) state.fail } + State.exchange.publishEvent(ExecStatusEvent("Done", channelName, exec.execId, newState.remainingCommands.toVector map { _.commandLine })) + newState } def invalidValue(label: String, allowed: Iterable[String])(value: String): String = "Not a valid " + label + ": " + value + similar(value, allowed) diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index 0f217dc73..733d07a5b 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -27,11 +27,12 @@ final case class State( configuration: xsbti.AppConfiguration, definedCommands: Seq[Command], exitHooks: Set[ExitHook], - onFailure: Option[String], - remainingCommands: Seq[String], + onFailure: Option[Exec], + remainingCommands: List[Exec], history: State.History, attributes: AttributeMap, globalLogging: GlobalLogging, + source: Option[CommandSource], next: State.Next ) extends Identity { lazy val combinedParser = Command.combine(definedCommands)(this) @@ -45,14 +46,20 @@ trait Identity { /** Convenience methods for State transformations and operations. */ trait StateOps { - def process(f: (String, State) => State): State + def process(f: (Exec, State) => State): State /** Schedules `commands` to be run before any remaining commands.*/ - def :::(commands: Seq[String]): State + def :::(newCommands: List[String]): State + + /** Schedules `commands` to be run before any remaining commands.*/ + def ++:(newCommands: List[Exec]): State /** Schedules `command` to be run before any remaining commands.*/ def ::(command: String): State + /** Schedules `command` to be run before any remaining commands.*/ + def +:(command: Exec): State + /** Sets the next command processing action to be to continue processing the next command.*/ def continue: State @@ -69,6 +76,9 @@ trait StateOps { /** Sets the next command processing action to do.*/ def setNext(n: State.Next): State + /** Sets the current command source channel.*/ + def setSource(source: CommandSource): State + @deprecated("Use setNext", "0.11.0") def setResult(ro: Option[xsbti.MainResult]): State /** @@ -158,9 +168,9 @@ object State { * @param executed the list of the most recently executed commands, with the most recent command first. * @param maxSize the maximum number of commands to keep, or 0 to keep an unlimited number. */ - final class History private[State] (val executed: Seq[String], val maxSize: Int) { + final class History private[State] (val executed: Seq[Exec], val maxSize: Int) { /** Adds `command` as the most recently executed command.*/ - def ::(command: String): History = + def ::(command: Exec): History = { val prependTo = if (maxSize > 0 && executed.size >= maxSize) executed.take(maxSize - 1) else executed new History(command +: prependTo, maxSize) @@ -168,8 +178,8 @@ object State { /** Changes the maximum number of commands kept, adjusting the current history if necessary.*/ def setMaxSize(size: Int): History = new History(if (size <= 0) executed else executed.take(size), size) - def currentOption: Option[String] = executed.headOption - def previous: Option[String] = executed.drop(1).headOption + def currentOption: Option[Exec] = executed.headOption + def previous: Option[Exec] = executed.drop(1).headOption } /** Constructs an empty command History with a default, finite command limit.*/ def newHistory = new History(Vector.empty, HistoryCommands.MaxLines) @@ -177,29 +187,37 @@ object State { def defaultReload(state: State): Reboot = { val app = state.configuration.provider - new Reboot(app.scalaProvider.version, state.remainingCommands, app.id, state.configuration.baseDirectory) + new Reboot( + app.scalaProvider.version, + state.remainingCommands map { case e: Exec => e.commandLine }, + 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 = + def process(f: (Exec, State) => State): State = s.remainingCommands match { - case Seq() => exit(true) - case Seq(x, xs @ _*) => + case List() => exit(true) + case x :: xs => log.debug(s"> $x") f(x, s.copy(remainingCommands = xs, history = x :: s.history)) } - def :::(newCommands: Seq[String]): State = s.copy(remainingCommands = newCommands ++ s.remainingCommands) - def ::(command: String): State = (command :: Nil) ::: this + + def :::(newCommands: List[String]): State = ++:(newCommands map { Exec(_, s.source) }) + def ++:(newCommands: List[Exec]): State = s.copy(remainingCommands = newCommands ::: s.remainingCommands) + def ::(command: String): State = +:(Exec(command, s.source)) + def +:(command: Exec): State = (command :: Nil) ++: this def ++(newCommands: Seq[Command]): State = s.copy(definedCommands = (s.definedCommands ++ newCommands).distinct) def +(newCommand: Command): State = this ++ (newCommand :: Nil) def baseDir: File = s.configuration.baseDirectory def setNext(n: Next) = s.copy(next = n) + def setSource(x: CommandSource): State = s.copy(source = Some(x)) def setResult(ro: Option[xsbti.MainResult]) = ro match { case None => continue; case Some(r) => setNext(new Return(r)) } def continue = setNext(Continue) - def reboot(full: Boolean) = { runExitHooks(); throw new xsbti.FullReload(s.remainingCommands.toArray, full) } + def reboot(full: Boolean) = { runExitHooks(); throw new xsbti.FullReload((s.remainingCommands map { case e: Exec => e.commandLine }).toArray, full) } def reload = runExitHooks().setNext(new Return(defaultReload(s))) def clearGlobalLog = setNext(ClearGlobalLog) def keepLastLog = setNext(KeepLastLog) @@ -220,7 +238,7 @@ object State { else applyOnFailure(s, remaining, s.copy(remainingCommands = remaining)) } - private[this] def applyOnFailure(s: State, remaining: Seq[String], noHandler: => State): State = + private[this] def applyOnFailure(s: State, remaining: List[Exec], noHandler: => State): State = s.onFailure match { case Some(c) => s.copy(remainingCommands = c +: remaining, onFailure = None) case None => noHandler diff --git a/main-command/src/main/scala/sbt/internal/CommandChannel.scala b/main-command/src/main/scala/sbt/internal/CommandChannel.scala index f01d9ffbd..b18965987 100644 --- a/main-command/src/main/scala/sbt/internal/CommandChannel.scala +++ b/main-command/src/main/scala/sbt/internal/CommandChannel.scala @@ -2,6 +2,7 @@ package sbt package internal import java.util.concurrent.ConcurrentLinkedQueue +import sbt.protocol.EventMessage /** * A command channel represents an IO device such as network socket or human @@ -14,23 +15,22 @@ abstract class CommandChannel { 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 publishEvent(event: EventMessage): Unit + def publishBytes(bytes: Array[Byte]): Unit def shutdown(): Unit + def name: String } -case class Exec(source: CommandSource, commandLine: String) +// case class Exec(commandLine: String, source: Option[CommandSource]) -sealed trait CommandSource -object CommandSource { - case object Human extends CommandSource - case object Network extends CommandSource -} +// case class CommandSource(channelName: String) -/** - * This is a data that is passed on to the channels. - * The canEnter paramter indicates that the console devise or UI - * should stop listening. +/* + * This is a data passed specifically for local prompting console. */ -case class CommandStatus(state: State, canEnter: Boolean) +case class ConsolePromptEvent(state: State) extends EventMessage + +/* + * This is a data passed specifically for unprompting local console. + */ +case class ConsoleUnpromptEvent(lastSource: Option[CommandSource]) extends EventMessage diff --git a/main-command/src/main/scala/sbt/internal/CommandExchange.scala b/main-command/src/main/scala/sbt/internal/CommandExchange.scala index 6bce31b0f..f4b9b5358 100644 --- a/main-command/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main-command/src/main/scala/sbt/internal/CommandExchange.scala @@ -1,9 +1,15 @@ package sbt package internal -import scala.annotation.tailrec -import scala.collection.mutable.ListBuffer +import java.net.SocketException import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicInteger +import sbt.internal.server._ +import sbt.protocol.{ EventMessage, Serialization, ChannelAcceptedEvent } +import scala.collection.mutable.ListBuffer +import scala.annotation.tailrec +import BasicKeys.serverPort +import java.net.Socket /** * The command exchange merges multiple command channels (e.g. network and console), @@ -12,14 +18,17 @@ import java.util.concurrent.ConcurrentLinkedQueue * this exchange, which could serve command request from either of the channel. */ private[sbt] final class CommandExchange { + private val lock = new AnyRef {} + private var server: Option[ServerInstance] = None + private var consoleChannel: Option[ConsoleChannel] = None private val commandQueue: ConcurrentLinkedQueue[Exec] = new ConcurrentLinkedQueue() private val channelBuffer: ListBuffer[CommandChannel] = new ListBuffer() + private val nextChannelId: AtomicInteger = new AtomicInteger(0) def channels: List[CommandChannel] = channelBuffer.toList def subscribe(c: CommandChannel): Unit = - channelBuffer.append(c) - - subscribe(new ConsoleChannel()) - subscribe(new NetworkChannel()) + lock.synchronized { + channelBuffer.append(c) + } // periodically move all messages from all the channels @tailrec def blockUntilNextExec: Exec = @@ -40,13 +49,86 @@ private[sbt] final class CommandExchange { } } - // fanout run to all channels def run(s: State): State = - (s /: channels) { (acc, c) => c.run(acc) } + { + consoleChannel match { + case Some(x) => // do nothing + case _ => + val x = new ConsoleChannel("console0") + consoleChannel = Some(x) + subscribe(x) + } + runServer(s) + } - // fanout publishStatus to all channels - def publishStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit = - channels foreach { c => - c.publishStatus(status, lastSource) + private def newChannelName: String = s"channel-${nextChannelId.incrementAndGet()}" + + private def runServer(s: State): State = + { + val port = (s get serverPort) match { + case Some(x) => x + case None => 5001 + } + def onIncomingSocket(socket: Socket): Unit = + { + s.log.info(s"new client connected from: ${socket.getPort}") + val channel = new NetworkChannel(newChannelName, socket) + subscribe(channel) + channel.publishEvent(ChannelAcceptedEvent(channel.name)) + } + server match { + case Some(x) => // do nothing + case _ => + server = Some(Server.start("127.0.0.1", port, onIncomingSocket, s.log)) + } + s + } + + def shutdown(): Unit = + { + channels foreach { c => + c.shutdown() + } + // interrupt and kill the thread + server.foreach(_.shutdown()) + server = None + } + + // fanout publisEvent + def publishEvent(event: EventMessage): Unit = + { + val toDel: ListBuffer[CommandChannel] = ListBuffer.empty + event match { + // Special treatment for ConsolePromptEvent since it's hand coded without codec. + case e: ConsolePromptEvent => + channels collect { + case c: ConsoleChannel => c.publishEvent(e) + } + case e: ConsoleUnpromptEvent => + channels collect { + case c: ConsoleChannel => c.publishEvent(e) + } + case _ => + // TODO do not do this on the calling thread + val bytes = Serialization.serializeEvent(event) + channels.foreach { + case c: ConsoleChannel => + c.publishEvent(event) + case c: NetworkChannel => + try { + c.publishBytes(bytes) + } catch { + case e: SocketException => + toDel += c + } + } + } + toDel.toList match { + case Nil => // do nothing + case xs => + lock.synchronized { + channelBuffer --= xs + } + } } } diff --git a/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala b/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala index 5b0c99497..2a22b5b46 100644 --- a/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala +++ b/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala @@ -4,11 +4,11 @@ package internal import sbt.internal.util._ import BasicKeys._ import java.io.File +import sbt.protocol.EventMessage -private[sbt] final class ConsoleChannel extends CommandChannel { +private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel { private var askUserThread: Option[Thread] = None - def makeAskUserThread(status: CommandStatus): Thread = new Thread("ask-user-thread") { - val s = status.state + def makeAskUserThread(s: State): Thread = new Thread("ask-user-thread") { val history = (s get historyPath) getOrElse Some(new File(s.baseDir, ".history")) val prompt = (s get shellPrompt) match { case Some(pf) => pf(s) @@ -19,8 +19,8 @@ private[sbt] final class ConsoleChannel extends CommandChannel { // 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")) + case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name)))) + case None => append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name)))) } askUserThread = None } @@ -28,25 +28,29 @@ private[sbt] final class ConsoleChannel extends CommandChannel { 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 publishBytes(bytes: Array[Byte]): Unit = () + + def publishEvent(event: EventMessage): Unit = + event match { + case e: ConsolePromptEvent => + askUserThread match { + case Some(x) => // + case _ => + val x = makeAskUserThread(e.state) + askUserThread = Some(x) + x.start + } + case e: ConsoleUnpromptEvent => + e.lastSource match { + case Some(src) if src.channelName != name => + askUserThread match { + case Some(x) => + shutdown() + case _ => + } + case _ => + } + case _ => // } def shutdown(): Unit = diff --git a/main-command/src/main/scala/sbt/internal/NetworkChannel.scala b/main-command/src/main/scala/sbt/internal/NetworkChannel.scala deleted file mode 100644 index 5e21524ff..000000000 --- a/main-command/src/main/scala/sbt/internal/NetworkChannel.scala +++ /dev/null @@ -1,43 +0,0 @@ -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)) - )) - } -} diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala new file mode 100644 index 000000000..c66ad05e5 --- /dev/null +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 Lightbend Inc. + */ +package sbt +package internal +package client + +import java.net.{ URI, Socket, InetAddress, SocketException } +import java.util.UUID +import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } +import sbt.protocol._ +import sbt.internal.util.JLine +import scala.collection.mutable.ListBuffer +import scala.util.control.NonFatal + +class NetworkClient(arguments: List[String]) { self => + private val channelName = new AtomicReference("_") + private val status = new AtomicReference("Ready") + private val lock: AnyRef = new AnyRef {} + private val running = new AtomicBoolean(true) + private val pendingExecIds = ListBuffer.empty[String] + + def usageError = sys.error("Expecting: sbt client 127.0.0.1:port") + val connection = init() + start() + + def init(): ServerConnection = { + val u = arguments match { + case List(x) => + if (x contains "://") new URI(x) + else new URI("tcp://" + x) + case _ => usageError + } + val host = Option(u.getHost) match { + case None => usageError + case Some(x) => x + } + val port = Option(u.getPort) match { + case None => usageError + case Some(x) if x == -1 => usageError + case Some(x) => x + } + println(s"client on port $port") + val socket = new Socket(InetAddress.getByName(host), port) + new ServerConnection(socket) { + override def onEvent(event: EventMessage): Unit = self.onEvent(event) + override def onShutdown(): Unit = + { + running.set(false) + } + } + } + + def onEvent(event: EventMessage): Unit = + event match { + case e: ChannelAcceptedEvent => + channelName.set(e.channelName) + println(event) + case e: ExecStatusEvent => + status.set(e.status) + // println(event) + e.execId foreach { execId => + if (e.status == "Done" && (pendingExecIds contains execId)) { + lock.synchronized { + pendingExecIds -= execId + } + } + } + case e => println(e.toString) + } + + def start(): Unit = + { + val reader = JLine.simple(None, JLine.HandleCONT, injectThreadSleep = true) + while (running.get) { + reader.readLine("> ", None) match { + case Some("exit") => + running.set(false) + case Some(s) => + val execId = UUID.randomUUID.toString + publishCommand(ExecCommand(s, execId)) + lock.synchronized { + pendingExecIds += execId + } + while (pendingExecIds contains execId) { + Thread.sleep(100) + } + case _ => // + } + } + } + + def publishCommand(command: CommandMessage): Unit = + { + val bytes = Serialization.serializeCommand(command) + try { + connection.publish(bytes) + } catch { + case e: SocketException => + // log.debug(e.getMessage) + // toDel += client + } + lock.synchronized { + status.set("Processing") + } + } +} + +object NetworkClient { + def run(arguments: List[String]): Unit = + try { + new NetworkClient(arguments) + } catch { + case NonFatal(e) => println(e.getMessage) + } +} diff --git a/main-command/src/main/scala/sbt/internal/server/ClientConnection.scala b/main-command/src/main/scala/sbt/internal/client/ServerConnection.scala similarity index 62% rename from main-command/src/main/scala/sbt/internal/server/ClientConnection.scala rename to main-command/src/main/scala/sbt/internal/client/ServerConnection.scala index 26b8188fb..5cecc2aef 100644 --- a/main-command/src/main/scala/sbt/internal/server/ClientConnection.scala +++ b/main-command/src/main/scala/sbt/internal/client/ServerConnection.scala @@ -3,19 +3,20 @@ */ package sbt package internal -package server +package client import java.net.{ SocketTimeoutException, Socket } import java.util.concurrent.atomic.AtomicBoolean +import sbt.protocol._ -abstract class ClientConnection(connection: Socket) { +abstract class ServerConnection(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}") { + val thread = new Thread(s"sbt-serverconnection-${connection.getPort}") { override def run(): Unit = { try { val readBuffer = new Array[Byte](4096) @@ -26,18 +27,19 @@ abstract class ClientConnection(connection: Socket) { while (bytesRead != -1 && running.get) { try { bytesRead = in.read(readBuffer) - val bytes = readBuffer.toVector.take(bytesRead) - buffer = buffer ++ bytes - + buffer = buffer ++ readBuffer.toVector.take(bytesRead) // handle un-framing - val delimPos = bytes.indexOf(delimiter) + val delimPos = buffer.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 + Serialization.deserializeEvent(chunk).fold( + { errorDesc => + val s = new String(chunk.toArray, "UTF-8") + println(s"Got invalid chunk from server: $s \n" + errorDesc) + }, + onEvent ) } @@ -45,7 +47,6 @@ abstract class ClientConnection(connection: Socket) { case _: SocketTimeoutException => // its ok } } - } finally { shutdown() } @@ -53,18 +54,21 @@ abstract class ClientConnection(connection: Socket) { } thread.start() - def publish(event: Array[Byte]): Unit = { - out.write(event) - out.write(delimiter) + def publish(command: Array[Byte]): Unit = { + out.write(command) + out.write(delimiter.toInt) out.flush() } - def onCommand(command: Command): Unit + def onEvent(event: EventMessage): Unit + + def onShutdown(): Unit def shutdown(): Unit = { println("Shutting down client connection") running.set(false) out.close() + onShutdown } } diff --git a/main-command/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main-command/src/main/scala/sbt/internal/server/NetworkChannel.scala new file mode 100644 index 000000000..acdde4c50 --- /dev/null +++ b/main-command/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 Lightbend Inc. + */ +package sbt +package internal +package server + +import java.net.{ Socket, SocketTimeoutException } +import java.util.concurrent.atomic.AtomicBoolean +import sbt.protocol.{ Serialization, CommandMessage, ExecCommand, EventMessage } + +final class NetworkChannel(val name: String, connection: Socket) extends CommandChannel { + private val running = new AtomicBoolean(true) + private val delimiter: Byte = '\n'.toByte + private val out = connection.getOutputStream + + val thread = new Thread(s"sbt-networkchannel-${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) + buffer = buffer ++ readBuffer.toVector.take(bytesRead) + // handle un-framing + val delimPos = buffer.indexOf(delimiter) + if (delimPos > 0) { + val chunk = buffer.take(delimPos) + buffer = buffer.drop(delimPos + 1) + + Serialization.deserializeCommand(chunk).fold( + errorDesc => println("Got invalid chunk from client: " + errorDesc), + onCommand + ) + } + + } catch { + case _: SocketTimeoutException => // its ok + } + } + + } finally { + shutdown() + } + } + } + thread.start() + + def publishEvent(event: EventMessage): Unit = + { + val bytes = Serialization.serializeEvent(event) + publishBytes(bytes) + } + + def publishBytes(event: Array[Byte]): Unit = + { + out.write(event) + out.write(delimiter.toInt) + out.flush() + } + + def onCommand(command: CommandMessage): Unit = + command match { + case x: ExecCommand => append(Exec(x.commandLine, x.execId orElse Some(Exec.newExecId), Some(CommandSource(name)))) + } + + def shutdown(): Unit = { + println("Shutting down client connection") + running.set(false) + out.close() + } +} diff --git a/main-command/src/main/scala/sbt/internal/server/Serialization.scala b/main-command/src/main/scala/sbt/internal/server/Serialization.scala deleted file mode 100644 index a3d4736c1..000000000 --- a/main-command/src/main/scala/sbt/internal/server/Serialization.scala +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2016 Lightbend Inc. - */ -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 diff --git a/main-command/src/main/scala/sbt/internal/server/Server.scala b/main-command/src/main/scala/sbt/internal/server/Server.scala index f102cb736..f00ffcf53 100644 --- a/main-command/src/main/scala/sbt/internal/server/Server.scala +++ b/main-command/src/main/scala/sbt/internal/server/Server.scala @@ -5,22 +5,21 @@ package sbt package internal package server -import java.net.{ SocketTimeoutException, InetAddress, ServerSocket, SocketException } +import java.net.{ SocketTimeoutException, InetAddress, ServerSocket, Socket } 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 = + def start(host: String, port: Int, onIncomingSocket: Socket => Unit, + /*onIncommingCommand: CommandMessage => Unit,*/ log: Logger): ServerInstance = new ServerInstance { - val lock = new AnyRef {} - val clients: mutable.ListBuffer[ClientConnection] = mutable.ListBuffer.empty + // val lock = new AnyRef {} + // val clients: mutable.ListBuffer[ClientConnection] = mutable.ListBuffer.empty val running = new AtomicBoolean(true) val serverThread = new Thread("sbt-socket-server") { @@ -33,18 +32,7 @@ private[sbt] object Server { 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 - } - + onIncomingSocket(socket) } catch { case _: SocketTimeoutException => // its ok } @@ -54,25 +42,6 @@ private[sbt] object Server { } 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) diff --git a/main-command/src/main/scala/sbt/internal/server/protocol.scala b/main-command/src/main/scala/sbt/internal/server/protocol.scala deleted file mode 100644 index 7c16b0fc3..000000000 --- a/main-command/src/main/scala/sbt/internal/server/protocol.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2016 Lightbend Inc. - */ -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 diff --git a/main/src/main/scala/sbt/Cross.scala b/main/src/main/scala/sbt/Cross.scala index 53ac58ba4..4bd53ddab 100644 --- a/main/src/main/scala/sbt/Cross.scala +++ b/main/src/main/scala/sbt/Cross.scala @@ -137,7 +137,7 @@ object Cross { Seq(s"$SwitchCommand $verbose $version", projects.map(_ + "/" + aggCommand).mkString("all ", " ", "")) } - allCommands ::: CrossRestoreSessionCommand :: captureCurrentSession(state, x) + allCommands.toList ::: CrossRestoreSessionCommand :: captureCurrentSession(state, x) } } @@ -169,7 +169,7 @@ object Cross { private def switchCommandImpl(state: State, args: Switch): State = { val switchedState = switchScalaVersion(args, state) - args.command.toSeq ::: switchedState + args.command.toList ::: switchedState } private def switchScalaVersion(switch: Switch, state: State): State = { diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 87bd3fd4e..868811a09 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -94,9 +94,9 @@ object StandardMain { import BasicCommandStrings.isEarlyCommand val userCommands = configuration.arguments.map(_.trim) val (earlyCommands, normalCommands) = (preCommands ++ userCommands).partition(isEarlyCommand) - val commands = earlyCommands ++ normalCommands + val commands = (earlyCommands ++ normalCommands).toList map { x => Exec(x, None) } val initAttrs = BuiltinCommands.initialAttributes - val s = State(configuration, initialDefinitions, Set.empty, None, commands, State.newHistory, initAttrs, initialGlobalLogging, State.Continue) + val s = State(configuration, initialDefinitions, Set.empty, None, commands, State.newHistory, initAttrs, initialGlobalLogging, None, State.Continue) s.initializeClassLoaderCache } } @@ -115,7 +115,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, BasicCommands.server, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++ + ifLast, multi, shell, BasicCommands.server, BasicCommands.client, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++ compatCommands def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil @@ -129,7 +129,7 @@ object BuiltinCommands { // This parser schedules the default boot commands unless overridden by an alias def bootParser(s: State) = { - val orElse = () => DefaultBootCommands ::: s + val orElse = () => DefaultBootCommands.toList ::: s delegateToAlias(BootCommand, success(orElse))(s) } @@ -242,7 +242,7 @@ object BuiltinCommands { } def initialize = Command.command(InitCommand) { s => - /*"load-commands -base ~/.sbt/commands" :: */ readLines(readable(sbtRCs(s))) ::: s + /*"load-commands -base ~/.sbt/commands" :: */ readLines(readable(sbtRCs(s))).toList ::: s } def eval = Command.single(EvalCommand, Help.more(EvalCommand, evalDetailed)) { (s, arg) => diff --git a/main/src/main/scala/sbt/internal/Aggregation.scala b/main/src/main/scala/sbt/internal/Aggregation.scala index 012575c3f..6c295562a 100644 --- a/main/src/main/scala/sbt/internal/Aggregation.scala +++ b/main/src/main/scala/sbt/internal/Aggregation.scala @@ -35,21 +35,15 @@ object Aggregation { runTasks(s, structure, ts, DummyTaskMap(Nil), show) } - @deprecated("Use `timedRun` and `showRun` directly or use `runTasks`.", "0.13.0") - def runTasksWithResult[T](s: State, structure: BuildStructure, ts: Values[Task[T]], extra: DummyTaskMap, show: ShowConfig)(implicit display: Show[ScopedKey[_]]): (State, Result[Seq[KeyValue[T]]]) = + private def showRun[T](complete: Complete[T], show: ShowConfig)(implicit display: Show[ScopedKey[_]]): Unit = { - val complete = timedRun[T](s, ts, extra) - showRun(complete, show) - (complete.state, complete.results) + import complete._ + val log = state.log + val extracted = Project.extract(state) + val success = results match { case Value(_) => true; case Inc(_) => false } + results.toEither.right.foreach { r => if (show.taskValues) printSettings(r, show.print) } + if (show.success) printSuccess(start, stop, extracted, success, log) } - def showRun[T](complete: Complete[T], show: ShowConfig)(implicit display: Show[ScopedKey[_]]): Unit = { - import complete._ - val log = state.log - val extracted = Project.extract(state) - val success = results match { case Value(_) => true; case Inc(_) => false } - results.toEither.right.foreach { r => if (show.taskValues) printSettings(r, show.print) } - if (show.success) printSuccess(start, stop, extracted, success, log) - } def timedRun[T](s: State, ts: Values[Task[T]], extra: DummyTaskMap): Complete[T] = { import EvaluateTask._ diff --git a/main/src/main/scala/sbt/internal/IvyConsole.scala b/main/src/main/scala/sbt/internal/IvyConsole.scala index e7f300300..4ae73db61 100644 --- a/main/src/main/scala/sbt/internal/IvyConsole.scala +++ b/main/src/main/scala/sbt/internal/IvyConsole.scala @@ -20,7 +20,7 @@ object IvyConsole { final val Name = "ivy-console" lazy val command = Command.command(Name) { state => - val Dependencies(managed, repos, unmanaged) = parseDependencies(state.remainingCommands, state.log) + val Dependencies(managed, repos, unmanaged) = parseDependencies(state.remainingCommands map { _.commandLine }, state.log) val base = new File(CommandUtil.bootDirectory(state), Name) IO.createDirectory(base) @@ -39,7 +39,7 @@ object IvyConsole { val append = Load.transformSettings(Load.projectScope(currentRef), currentRef.build, rootProject, depSettings) val newStructure = Load.reapply(session.original ++ append, structure) - val newState = state.copy(remainingCommands = "console-quick" :: Nil) + val newState = state.copy(remainingCommands = Exec("console-quick", None) :: Nil) Project.setProject(session, newStructure, newState) } diff --git a/main/src/main/scala/sbt/internal/Script.scala b/main/src/main/scala/sbt/internal/Script.scala index 9ead77992..0a6ec339e 100644 --- a/main/src/main/scala/sbt/internal/Script.scala +++ b/main/src/main/scala/sbt/internal/Script.scala @@ -19,7 +19,7 @@ object Script { final val Name = "script" lazy val command = Command.command(Name) { state => - val scriptArg = state.remainingCommands.headOption getOrElse sys.error("No script file specified") + val scriptArg = state.remainingCommands.headOption map { _.commandLine } getOrElse sys.error("No script file specified") val scriptFile = new File(scriptArg).getAbsoluteFile val hash = Hash.halve(Hash.toHex(Hash(scriptFile.getAbsolutePath))) val base = new File(CommandUtil.bootDirectory(state), hash) diff --git a/main/src/test/scala/PluginCommandTest.scala b/main/src/test/scala/PluginCommandTest.scala index 2ce29a6dd..dd5348d9b 100644 --- a/main/src/test/scala/PluginCommandTest.scala +++ b/main/src/test/scala/PluginCommandTest.scala @@ -52,7 +52,7 @@ object FakeState { try { System.setOut(new PrintStream(outBuffer, true)) val state = FakeState(enabledPlugins: _*) - Command.process(input, state) + Command.process(Exec(input, None), state) new String(outBuffer.toByteArray) } finally { System.setOut(previousOut) @@ -106,10 +106,11 @@ object FakeState { Seq(BuiltinCommands.plugin), Set.empty, None, - Seq.empty, + List(), State.newHistory, attributes, GlobalLogging.initial(MainLogging.globalDefault(ConsoleOut.systemOut), File.createTempFile("sbt", ".log"), ConsoleOut.systemOut), + None, State.Continue ) diff --git a/protocol/src/main/contraband-scala/sbt/protocol/ChannelAcceptedEvent.scala b/protocol/src/main/contraband-scala/sbt/protocol/ChannelAcceptedEvent.scala new file mode 100644 index 000000000..f0dd2233d --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/ChannelAcceptedEvent.scala @@ -0,0 +1,32 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class ChannelAcceptedEvent private ( + val channelName: String) extends sbt.protocol.EventMessage() with Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: ChannelAcceptedEvent => (this.channelName == x.channelName) + case _ => false + } + override def hashCode: Int = { + 37 * (17 + channelName.##) + } + override def toString: String = { + "ChannelAcceptedEvent(" + channelName + ")" + } + protected[this] def copy(channelName: String = channelName): ChannelAcceptedEvent = { + new ChannelAcceptedEvent(channelName) + } + def withChannelName(channelName: String): ChannelAcceptedEvent = { + copy(channelName = channelName) + } +} +object ChannelAcceptedEvent { + + def apply(channelName: String): ChannelAcceptedEvent = new ChannelAcceptedEvent(channelName) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/CommandMessage.scala b/protocol/src/main/contraband-scala/sbt/protocol/CommandMessage.scala new file mode 100644 index 000000000..26c102a47 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/CommandMessage.scala @@ -0,0 +1,26 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +/** Message to invoke command. */ +abstract class CommandMessage() extends Serializable { + + + + +override def equals(o: Any): Boolean = o match { + case x: CommandMessage => true + case _ => false +} +override def hashCode: Int = { + 17 +} +override def toString: String = { + "CommandMessage()" +} +} +object CommandMessage { + +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/EventMessage.scala b/protocol/src/main/contraband-scala/sbt/protocol/EventMessage.scala new file mode 100644 index 000000000..c564c7fa0 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/EventMessage.scala @@ -0,0 +1,26 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +/** Message for events. */ +abstract class EventMessage() extends Serializable { + + + + +override def equals(o: Any): Boolean = o match { + case x: EventMessage => true + case _ => false +} +override def hashCode: Int = { + 17 +} +override def toString: String = { + "EventMessage()" +} +} +object EventMessage { + +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/ExecCommand.scala b/protocol/src/main/contraband-scala/sbt/protocol/ExecCommand.scala new file mode 100644 index 000000000..c484cc2a9 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/ExecCommand.scala @@ -0,0 +1,42 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +/** Command to execute sbt command. */ +final class ExecCommand private ( + val commandLine: String, + val execId: Option[String]) extends sbt.protocol.CommandMessage() with Serializable { + + private def this(commandLine: String) = this(commandLine, None) + + override def equals(o: Any): Boolean = o match { + case x: ExecCommand => (this.commandLine == x.commandLine) && (this.execId == x.execId) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + commandLine.##) + execId.##) + } + override def toString: String = { + "ExecCommand(" + commandLine + ", " + execId + ")" + } + protected[this] def copy(commandLine: String = commandLine, execId: Option[String] = execId): ExecCommand = { + new ExecCommand(commandLine, execId) + } + def withCommandLine(commandLine: String): ExecCommand = { + copy(commandLine = commandLine) + } + def withExecId(execId: Option[String]): ExecCommand = { + copy(execId = execId) + } + def withExecId(execId: String): ExecCommand = { + copy(execId = Option(execId)) + } +} +object ExecCommand { + + def apply(commandLine: String): ExecCommand = new ExecCommand(commandLine, None) + def apply(commandLine: String, execId: Option[String]): ExecCommand = new ExecCommand(commandLine, execId) + def apply(commandLine: String, execId: String): ExecCommand = new ExecCommand(commandLine, Option(execId)) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/ExecStatusEvent.scala b/protocol/src/main/contraband-scala/sbt/protocol/ExecStatusEvent.scala new file mode 100644 index 000000000..f92b4bae9 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/ExecStatusEvent.scala @@ -0,0 +1,52 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +/** Status event. */ +final class ExecStatusEvent private ( + val status: String, + val channelName: Option[String], + val execId: Option[String], + val commandQueue: Vector[String]) extends sbt.protocol.EventMessage() with Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: ExecStatusEvent => (this.status == x.status) && (this.channelName == x.channelName) && (this.execId == x.execId) && (this.commandQueue == x.commandQueue) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (17 + status.##) + channelName.##) + execId.##) + commandQueue.##) + } + override def toString: String = { + "ExecStatusEvent(" + status + ", " + channelName + ", " + execId + ", " + commandQueue + ")" + } + protected[this] def copy(status: String = status, channelName: Option[String] = channelName, execId: Option[String] = execId, commandQueue: Vector[String] = commandQueue): ExecStatusEvent = { + new ExecStatusEvent(status, channelName, execId, commandQueue) + } + def withStatus(status: String): ExecStatusEvent = { + copy(status = status) + } + def withChannelName(channelName: Option[String]): ExecStatusEvent = { + copy(channelName = channelName) + } + def withChannelName(channelName: String): ExecStatusEvent = { + copy(channelName = Option(channelName)) + } + def withExecId(execId: Option[String]): ExecStatusEvent = { + copy(execId = execId) + } + def withExecId(execId: String): ExecStatusEvent = { + copy(execId = Option(execId)) + } + def withCommandQueue(commandQueue: Vector[String]): ExecStatusEvent = { + copy(commandQueue = commandQueue) + } +} +object ExecStatusEvent { + + def apply(status: String, channelName: Option[String], execId: Option[String], commandQueue: Vector[String]): ExecStatusEvent = new ExecStatusEvent(status, channelName, execId, commandQueue) + def apply(status: String, channelName: String, execId: String, commandQueue: Vector[String]): ExecStatusEvent = new ExecStatusEvent(status, Option(channelName), Option(execId), commandQueue) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/ExecutionEvent.scala b/protocol/src/main/contraband-scala/sbt/protocol/ExecutionEvent.scala new file mode 100644 index 000000000..611b1845f --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/ExecutionEvent.scala @@ -0,0 +1,37 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +/** Execution event. */ +final class ExecutionEvent private ( + val success: String, + val commandLine: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: ExecutionEvent => (this.success == x.success) && (this.commandLine == x.commandLine) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + success.##) + commandLine.##) + } + override def toString: String = { + "ExecutionEvent(" + success + ", " + commandLine + ")" + } + protected[this] def copy(success: String = success, commandLine: String = commandLine): ExecutionEvent = { + new ExecutionEvent(success, commandLine) + } + def withSuccess(success: String): ExecutionEvent = { + copy(success = success) + } + def withCommandLine(commandLine: String): ExecutionEvent = { + copy(commandLine = commandLine) + } +} +object ExecutionEvent { + + def apply(success: String, commandLine: String): ExecutionEvent = new ExecutionEvent(success, commandLine) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/LogEvent.scala b/protocol/src/main/contraband-scala/sbt/protocol/LogEvent.scala new file mode 100644 index 000000000..79e239e6a --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/LogEvent.scala @@ -0,0 +1,37 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +/** Log event. */ +final class LogEvent private ( + val level: String, + val message: String) extends sbt.protocol.EventMessage() with Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: LogEvent => (this.level == x.level) && (this.message == x.message) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (17 + level.##) + message.##) + } + override def toString: String = { + "LogEvent(" + level + ", " + message + ")" + } + protected[this] def copy(level: String = level, message: String = message): LogEvent = { + new LogEvent(level, message) + } + def withLevel(level: String): LogEvent = { + copy(level = level) + } + def withMessage(message: String): LogEvent = { + copy(message = message) + } +} +object LogEvent { + + def apply(level: String, message: String): LogEvent = new LogEvent(level, message) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/ChannelAcceptedEventFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/ChannelAcceptedEventFormats.scala new file mode 100644 index 000000000..1bcb80ce7 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/ChannelAcceptedEventFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait ChannelAcceptedEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ChannelAcceptedEventFormat: JsonFormat[sbt.protocol.ChannelAcceptedEvent] = new JsonFormat[sbt.protocol.ChannelAcceptedEvent] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.ChannelAcceptedEvent = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val channelName = unbuilder.readField[String]("channelName") + unbuilder.endObject() + sbt.protocol.ChannelAcceptedEvent(channelName) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.ChannelAcceptedEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("channelName", obj.channelName) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala new file mode 100644 index 000000000..3a747c4e3 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala @@ -0,0 +1,10 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ExecCommandFormats => +implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat1[sbt.protocol.CommandMessage, sbt.protocol.ExecCommand]("type") +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/ConsolePromptEventFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/ConsolePromptEventFormats.scala new file mode 100644 index 000000000..ed5f52417 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/ConsolePromptEventFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait ConsolePromptEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ConsolePromptEventFormat: JsonFormat[sbt.protocol.ConsolePromptEvent] = new JsonFormat[sbt.protocol.ConsolePromptEvent] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.ConsolePromptEvent = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + + unbuilder.endObject() + sbt.protocol.ConsolePromptEvent() + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.ConsolePromptEvent, builder: Builder[J]): Unit = { + builder.beginObject() + + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala new file mode 100644 index 000000000..547cd7031 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala @@ -0,0 +1,10 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats => +implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat3[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent]("type") +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecCommandFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecCommandFormats.scala new file mode 100644 index 000000000..c58bdf800 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecCommandFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait ExecCommandFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ExecCommandFormat: JsonFormat[sbt.protocol.ExecCommand] = new JsonFormat[sbt.protocol.ExecCommand] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.ExecCommand = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val commandLine = unbuilder.readField[String]("commandLine") + val execId = unbuilder.readField[Option[String]]("execId") + unbuilder.endObject() + sbt.protocol.ExecCommand(commandLine, execId) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.ExecCommand, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("commandLine", obj.commandLine) + builder.addField("execId", obj.execId) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecStatusEventFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecStatusEventFormats.scala new file mode 100644 index 000000000..d95508389 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecStatusEventFormats.scala @@ -0,0 +1,33 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait ExecStatusEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ExecStatusEventFormat: JsonFormat[sbt.protocol.ExecStatusEvent] = new JsonFormat[sbt.protocol.ExecStatusEvent] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.ExecStatusEvent = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val status = unbuilder.readField[String]("status") + val channelName = unbuilder.readField[Option[String]]("channelName") + val execId = unbuilder.readField[Option[String]]("execId") + val commandQueue = unbuilder.readField[Vector[String]]("commandQueue") + unbuilder.endObject() + sbt.protocol.ExecStatusEvent(status, channelName, execId, commandQueue) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.ExecStatusEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("status", obj.status) + builder.addField("channelName", obj.channelName) + builder.addField("execId", obj.execId) + builder.addField("commandQueue", obj.commandQueue) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/ExectionEventFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExectionEventFormats.scala new file mode 100644 index 000000000..5ba94d94e --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExectionEventFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait ExectionEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ExectionEventFormat: JsonFormat[sbt.protocol.ExectionEvent] = new JsonFormat[sbt.protocol.ExectionEvent] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.ExectionEvent = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val success = unbuilder.readField[String]("success") + val commandLine = unbuilder.readField[String]("commandLine") + unbuilder.endObject() + sbt.protocol.ExectionEvent(success, commandLine) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.ExectionEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("success", obj.success) + builder.addField("commandLine", obj.commandLine) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecutionEventFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecutionEventFormats.scala new file mode 100644 index 000000000..285fcb6c0 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/ExecutionEventFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait ExecutionEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val ExecutionEventFormat: JsonFormat[sbt.protocol.ExecutionEvent] = new JsonFormat[sbt.protocol.ExecutionEvent] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.ExecutionEvent = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val success = unbuilder.readField[String]("success") + val commandLine = unbuilder.readField[String]("commandLine") + unbuilder.endObject() + sbt.protocol.ExecutionEvent(success, commandLine) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.ExecutionEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("success", obj.success) + builder.addField("commandLine", obj.commandLine) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala new file mode 100644 index 000000000..fa9fb28ac --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala @@ -0,0 +1,15 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +trait JsonProtocol extends sjsonnew.BasicJsonProtocol + with sbt.protocol.codec.ExecCommandFormats + with sbt.protocol.codec.CommandMessageFormats + with sbt.protocol.codec.ChannelAcceptedEventFormats + with sbt.protocol.codec.LogEventFormats + with sbt.protocol.codec.ExecStatusEventFormats + with sbt.protocol.codec.EventMessageFormats + with sbt.protocol.codec.ExecutionEventFormats +object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/LogEventFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/LogEventFormats.scala new file mode 100644 index 000000000..861776178 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/LogEventFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait LogEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val LogEventFormat: JsonFormat[sbt.protocol.LogEvent] = new JsonFormat[sbt.protocol.LogEvent] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.LogEvent = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val level = unbuilder.readField[String]("level") + val message = unbuilder.readField[String]("message") + unbuilder.endObject() + sbt.protocol.LogEvent(level, message) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.LogEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("level", obj.level) + builder.addField("message", obj.message) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/StatusEventFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/StatusEventFormats.scala new file mode 100644 index 000000000..0819d1a22 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/StatusEventFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using sbt-datatype. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder } +trait StatusEventFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val StatusEventFormat: JsonFormat[sbt.protocol.StatusEvent] = new JsonFormat[sbt.protocol.StatusEvent] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.StatusEvent = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val status = unbuilder.readField[String]("status") + val commandQueue = unbuilder.readField[Vector[String]]("commandQueue") + unbuilder.endObject() + sbt.protocol.StatusEvent(status, commandQueue) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.StatusEvent, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("status", obj.status) + builder.addField("commandQueue", obj.commandQueue) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband/server.contra b/protocol/src/main/contraband/server.contra new file mode 100644 index 000000000..717b74d61 --- /dev/null +++ b/protocol/src/main/contraband/server.contra @@ -0,0 +1,47 @@ +package sbt.protocol +@target(Scala) +@codecPackage("sbt.protocol.codec") +@fullCodec("JsonProtocol") + +## Message to invoke command. +interface CommandMessage { +} + +## Command to execute sbt command. +type ExecCommand implements CommandMessage { + commandLine: String! + execId: String @since("0.0.1") +} + +## Message for events. +interface EventMessage { +} + +type ChannelAcceptedEvent implements EventMessage { + channelName: String! +} + +## Log event. +type LogEvent implements EventMessage { + level: String! + message: String! +} + +## Status event. +type ExecStatusEvent implements EventMessage { + status: String! + channelName: String + execId: String + commandQueue: [String] +} + +# enum Status { +# Ready +# Processing +# } + +## Execution event. +type ExecutionEvent { + success: String! + commandLine: String! +} diff --git a/protocol/src/main/scala/sbt/protocol/Serialization.scala b/protocol/src/main/scala/sbt/protocol/Serialization.scala new file mode 100644 index 000000000..0ca01cb56 --- /dev/null +++ b/protocol/src/main/scala/sbt/protocol/Serialization.scala @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 Lightbend Inc. + */ +package sbt +package protocol + +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 serializeCommand(command: CommandMessage): Array[Byte] = + { + import codec.JsonProtocol._ + val json: JValue = Converter.toJson[CommandMessage](command).get + CompactPrinter(json).getBytes("UTF-8") + } + + def serializeEvent(event: EventMessage): Array[Byte] = + { + import codec.JsonProtocol._ + val json: JValue = Converter.toJson[EventMessage](event).get + CompactPrinter(json).getBytes("UTF-8") + } + + /** + * @return A command or an invalid input description + */ + def deserializeCommand(bytes: Seq[Byte]): Either[String, CommandMessage] = + { + val buffer = ByteBuffer.wrap(bytes.toArray) + Parser.parseFromByteBuffer(buffer) match { + case Success(json) => + import codec.JsonProtocol._ + Converter.fromJson[CommandMessage](json) match { + case Success(command) => Right(command) + case Failure(e) => Left(e.getMessage) + } + case Failure(e) => + Left(s"Parse error: ${e.getMessage}") + } + } + + /** + * @return A command or an invalid input description + */ + def deserializeEvent(bytes: Seq[Byte]): Either[String, EventMessage] = + { + val buffer = ByteBuffer.wrap(bytes.toArray) + Parser.parseFromByteBuffer(buffer) match { + case Success(json) => + import codec.JsonProtocol._ + Converter.fromJson[EventMessage](json) match { + case Success(event) => Right(event) + case Failure(e) => Left(e.getMessage) + } + case Failure(e) => + Left(s"Parse error: ${e.getMessage}") + } + } +} diff --git a/server.md b/server.md new file mode 100644 index 000000000..ddaa9b034 --- /dev/null +++ b/server.md @@ -0,0 +1,8 @@ + + +### ExecCommand + +```json +{ "type": "ExecCommand", "commandLine": "compile" } +``` +