From 9557107c97f8d5175d926d29d942173385453d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andre=CC=81n?= Date: Tue, 1 Mar 2016 17:29:02 +0100 Subject: [PATCH 01/18] First sloppy stab at an embedded server in sbt --- build.sbt | 10 ++- .../scala/sbt/server/ClientConnection.scala | 66 ++++++++++++++++ .../main/scala/sbt/server/Serialization.scala | 54 +++++++++++++ .../src/main/scala/sbt/server/Server.scala | 78 +++++++++++++++++++ .../src/main/scala/sbt/server/protocol.scala | 14 ++++ main/src/main/scala/sbt/Main.scala | 3 + 6 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 main/server/src/main/scala/sbt/server/ClientConnection.scala create mode 100644 main/server/src/main/scala/sbt/server/Serialization.scala create mode 100644 main/server/src/main/scala/sbt/server/Server.scala create mode 100644 main/server/src/main/scala/sbt/server/protocol.scala diff --git a/build.sbt b/build.sbt index 648d72b52..a25c4adc0 100644 --- a/build.sbt +++ b/build.sbt @@ -193,9 +193,15 @@ lazy val mainSettingsProj = (project in file("main-settings")). utilLogging, sbtIO, utilCompletion, compilerClasspath, libraryManagement) ) +lazy val serverProj = (project in mainPath / "server"). + settings( + baseSettings, + libraryDependencies ++= Seq(json4s, json4sNative) // to transitively get json4s + ) + // The main integration project for sbt. It brings all of the Projsystems together, configures them, and provides for overriding conventions. lazy val mainProj = (project in file("main")). - dependsOn(actionsProj, mainSettingsProj, runProj, commandProj). + dependsOn(actionsProj, mainSettingsProj, runProj, commandProj, serverProj). settings( testedBaseSettings, name := "Main", @@ -244,7 +250,7 @@ lazy val myProvided = config("provided") intransitive def allProjects = Seq( testingProj, testAgentProj, taskProj, stdTaskProj, runProj, scriptedSbtProj, scriptedPluginProj, - actionsProj, commandProj, mainSettingsProj, mainProj, sbtProj, bundledLauncherProj) + actionsProj, commandProj, mainSettingsProj, serverProj, mainProj, sbtProj, bundledLauncherProj) def projectsWithMyProvided = allProjects.map(p => p.copy(configurations = (p.configurations.filter(_ != Provided)) :+ myProvided)) lazy val nonRoots = projectsWithMyProvided.map(p => LocalProject(p.id)) diff --git a/main/server/src/main/scala/sbt/server/ClientConnection.scala b/main/server/src/main/scala/sbt/server/ClientConnection.scala new file mode 100644 index 000000000..493c223a0 --- /dev/null +++ b/main/server/src/main/scala/sbt/server/ClientConnection.scala @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 Lightbend Inc. + */ +package sbt.server + +import java.net.{ SocketTimeoutException, Socket } +import java.util.concurrent.atomic.AtomicBoolean + +abstract class ClientConnection(connection: Socket) { + + // TODO handle client disconnect + private val running = new AtomicBoolean(true) + private val delimiter = '\0'.toByte + + private val out = connection.getOutputStream + + val thread = new Thread(s"sbt-client-${connection.getPort}") { + override def run(): Unit = { + 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) + + Serialization.deserialize(chunk).fold( + errorDesc => println("Got invalid chunk from client: " + errorDesc), + onCommand + ) + } + + } catch { + case _: SocketTimeoutException => // its ok + } + } + + 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() + } + +} diff --git a/main/server/src/main/scala/sbt/server/Serialization.scala b/main/server/src/main/scala/sbt/server/Serialization.scala new file mode 100644 index 000000000..fbb98fbc2 --- /dev/null +++ b/main/server/src/main/scala/sbt/server/Serialization.scala @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 Lightbend Inc. + */ +package sbt.server + +import org.json4s.JsonAST.{ JArray, JString } +import org.json4s._ +import org.json4s.JsonDSL._ +import org.json4s.native.JsonMethods._ + +object Serialization { + + def serialize(event: Event): Array[Byte] = { + compact(render(toJson(event))).getBytes("UTF-8") + } + + def toJson(event: Event): JObject = event match { + case LogEvent() => + JObject( + "type" -> JString("log_event"), + "level" -> JString("INFO"), + "message" -> JString("todo") + ) + + case StatusEvent() => + JObject( + "type" -> JString("status_event"), + "status" -> JString("ready"), + "command_queue" -> JArray(List.empty) + ) + + case ExecutionEvent() => + JObject( + "type" -> JString("execution_event"), + "command" -> JString("project todo"), + "success" -> JArray(List.empty) + ) + } + + /** + * @return A command or an invalid input description + */ + def deserialize(bytes: Seq[Byte]): Either[String, Command] = { + val json = parse(new String(bytes.toArray, "UTF-8")) + + implicit val formats = DefaultFormats + + (json \ "type").extract[String] match { + case "command" => Right(Execution((json \ "command_line").extract[String])) + case cmd => Left(s"Unknown command type $cmd") + } + } + +} diff --git a/main/server/src/main/scala/sbt/server/Server.scala b/main/server/src/main/scala/sbt/server/Server.scala new file mode 100644 index 000000000..bb2610db2 --- /dev/null +++ b/main/server/src/main/scala/sbt/server/Server.scala @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 Lightbend Inc. + */ +package sbt.server + +import java.net.{ SocketTimeoutException, InetAddress, ServerSocket } +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean + +object Server { + + trait ServerInstance { + def shutdown(): Unit + } + + def start(host: String, port: Int): ServerInstance = + new ServerInstance { + + val commandQueue = new ConcurrentLinkedQueue[Command]() + + val lock = new AnyRef {} + var clients = Vector[ClientConnection]() + val running = new AtomicBoolean(true) + + val serverSocket = new ServerSocket(port, 50, InetAddress.getByName(host)) + serverSocket.setSoTimeout(5000) + + val serverThread = new Thread("sbt-socket-server") { + + override def run(): Unit = { + println(s"SBT socket server started at $host:$port") + while (running.get()) { + try { + val socket = serverSocket.accept() + println(s"New client connected from: ${socket.getPort}") + + val connection = new ClientConnection(socket) { + override def onCommand(command: Command): Unit = { + commandQueue.add(command) + } + } + + lock.synchronized { + clients = 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 { + clients.foreach(_.publish(bytes)) + } + } + + /** + * @return The next queued command if there is one. It will have to be consumed because it is taken off the queue. + */ + def nextCommand(): Option[Command] = { + Option(commandQueue.poll()) + } + + override def shutdown(): Unit = { + println("Shutting down server") + running.set(false) + } + } + +} \ No newline at end of file diff --git a/main/server/src/main/scala/sbt/server/protocol.scala b/main/server/src/main/scala/sbt/server/protocol.scala new file mode 100644 index 000000000..714a640ec --- /dev/null +++ b/main/server/src/main/scala/sbt/server/protocol.scala @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2016 Lightbend Inc. + */ +package sbt.server + +trait Event + +case class LogEvent() extends Event +case class StatusEvent() extends Event +case class ExecutionEvent() extends Event + +trait Command + +case class Execution(cmd: String) extends Command \ No newline at end of file diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 279da153a..5945e222b 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -84,6 +84,9 @@ object StandardMain { /** The common interface to standard output, used for all built-in ConsoleLoggers. */ val console = ConsoleOut.systemOutOverwrite(ConsoleOut.overwriteContaining("Resolving ")) + // TODO hook it in, start in the right place, shutdown on termination + val server = sbt.server.Server.start("127.0.0.1", 12700) + def initialGlobalLogging: GlobalLogging = GlobalLogging.initial(MainLogging.globalDefault(console), File.createTempFile("sbt", ".log"), console) def initialState(configuration: xsbti.AppConfiguration, initialDefinitions: Seq[Command], preCommands: Seq[String]): State = From ec0fe7bb213c0d8ee45dd8cd52bbf197155e8598 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 1 Mar 2016 20:58:48 +0100 Subject: [PATCH 02/18] Split using newline. Also more error handling. --- .../scala/sbt/server/ClientConnection.scala | 2 +- .../main/scala/sbt/server/Serialization.scala | 22 +++++++++++-------- .../src/main/scala/sbt/server/Server.scala | 1 + 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/main/server/src/main/scala/sbt/server/ClientConnection.scala b/main/server/src/main/scala/sbt/server/ClientConnection.scala index 493c223a0..2e6426b51 100644 --- a/main/server/src/main/scala/sbt/server/ClientConnection.scala +++ b/main/server/src/main/scala/sbt/server/ClientConnection.scala @@ -10,7 +10,7 @@ abstract class ClientConnection(connection: Socket) { // TODO handle client disconnect private val running = new AtomicBoolean(true) - private val delimiter = '\0'.toByte + private val delimiter: Byte = '\n'.toByte private val out = connection.getOutputStream diff --git a/main/server/src/main/scala/sbt/server/Serialization.scala b/main/server/src/main/scala/sbt/server/Serialization.scala index fbb98fbc2..50730798b 100644 --- a/main/server/src/main/scala/sbt/server/Serialization.scala +++ b/main/server/src/main/scala/sbt/server/Serialization.scala @@ -7,6 +7,7 @@ import org.json4s.JsonAST.{ JArray, JString } import org.json4s._ import org.json4s.JsonDSL._ import org.json4s.native.JsonMethods._ +import org.json4s.ParserUtil.ParseException object Serialization { @@ -40,15 +41,18 @@ object Serialization { /** * @return A command or an invalid input description */ - def deserialize(bytes: Seq[Byte]): Either[String, Command] = { - val json = parse(new String(bytes.toArray, "UTF-8")) + def deserialize(bytes: Seq[Byte]): Either[String, Command] = + try { + val json = parse(new String(bytes.toArray, "UTF-8")) + implicit val formats = DefaultFormats - implicit val formats = DefaultFormats - - (json \ "type").extract[String] match { - case "command" => Right(Execution((json \ "command_line").extract[String])) - case cmd => Left(s"Unknown command type $cmd") + // TODO: is using extract safe? + (json \ "type").extract[String] match { + case "execution" => Right(Execution((json \ "command_line").extract[String])) + case cmd => Left(s"Unknown command type $cmd") + } + } catch { + case e: ParseException => Left(s"Parse error: ${e.getMessage}") + case e: MappingException => Left(s"Missing type field") } - } - } diff --git a/main/server/src/main/scala/sbt/server/Server.scala b/main/server/src/main/scala/sbt/server/Server.scala index bb2610db2..15a99f3a1 100644 --- a/main/server/src/main/scala/sbt/server/Server.scala +++ b/main/server/src/main/scala/sbt/server/Server.scala @@ -36,6 +36,7 @@ object Server { val connection = new ClientConnection(socket) { override def onCommand(command: Command): Unit = { + println(s"onCommand $command") commandQueue.add(command) } } From 649dc0ce3c39f45134389f7fb0f3dc0c583e9ac9 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 2 Mar 2016 04:02:03 +0100 Subject: [PATCH 03/18] This is where we can multiplex commands --- build.sbt | 1 + main-command/src/main/scala/sbt/BasicCommands.scala | 4 ++++ main/server/src/main/scala/sbt/server/Server.scala | 10 +++++----- main/src/main/scala/sbt/Main.scala | 3 --- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index a25c4adc0..9d8b039c7 100644 --- a/build.sbt +++ b/build.sbt @@ -176,6 +176,7 @@ 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")). + dependsOn(serverProj). settings( testedBaseSettings, name := "Command", diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 68503fda4..937de57bb 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -13,6 +13,7 @@ import BasicCommandStrings._ import CommandUtil._ import BasicKeys._ +import sbt.server.Server import java.io.File import sbt.io.IO @@ -179,6 +180,9 @@ object BasicCommands { } def shell = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s => + // TODO hook it in, start in the right place, shutdown on termination + val server = Server.start("127.0.0.1", 12700) + 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) diff --git a/main/server/src/main/scala/sbt/server/Server.scala b/main/server/src/main/scala/sbt/server/Server.scala index 15a99f3a1..0d8b46941 100644 --- a/main/server/src/main/scala/sbt/server/Server.scala +++ b/main/server/src/main/scala/sbt/server/Server.scala @@ -7,12 +7,12 @@ import java.net.{ SocketTimeoutException, InetAddress, ServerSocket } import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean +sealed trait ServerInstance { + def shutdown(): Unit + def nextCommand(): Option[Command] +} + object Server { - - trait ServerInstance { - def shutdown(): Unit - } - def start(host: String, port: Int): ServerInstance = new ServerInstance { diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 5945e222b..279da153a 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -84,9 +84,6 @@ object StandardMain { /** The common interface to standard output, used for all built-in ConsoleLoggers. */ val console = ConsoleOut.systemOutOverwrite(ConsoleOut.overwriteContaining("Resolving ")) - // TODO hook it in, start in the right place, shutdown on termination - val server = sbt.server.Server.start("127.0.0.1", 12700) - def initialGlobalLogging: GlobalLogging = GlobalLogging.initial(MainLogging.globalDefault(console), File.createTempFile("sbt", ".log"), console) def initialState(configuration: xsbti.AppConfiguration, initialDefinitions: Seq[Command], preCommands: Seq[String]): State = From f9dd8b73b7a035fe16f933560e97fa4b79230f07 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 2 Mar 2016 11:49:17 +0100 Subject: [PATCH 04/18] Refactor to abstract listening for command line --- build.sbt | 13 ++----- .../main/scala/sbt/BasicCommandStrings.scala | 3 ++ .../src/main/scala/sbt/BasicCommands.scala | 35 ++++++++++++++++--- .../src/main/scala/sbt/CommandListener.scala | 12 +++++++ .../src/main/scala/sbt/CommandStatus.scala | 3 ++ .../src/main/scala/sbt/ConsoleListener.scala | 31 ++++++++++++++++ .../src/main/scala/sbt/NetworkListener.scala | 22 ++++++++++++ .../scala/sbt/server/ClientConnection.scala | 3 +- .../main/scala/sbt/server/Serialization.scala | 3 +- .../src/main/scala/sbt/server/Server.scala | 3 +- .../src/main/scala/sbt/server/protocol.scala | 3 +- 11 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 main/command/src/main/scala/sbt/CommandListener.scala create mode 100644 main/command/src/main/scala/sbt/CommandStatus.scala create mode 100644 main/command/src/main/scala/sbt/ConsoleListener.scala create mode 100644 main/command/src/main/scala/sbt/NetworkListener.scala rename main/{server => command}/src/main/scala/sbt/server/ClientConnection.scala (98%) rename main/{server => command}/src/main/scala/sbt/server/Serialization.scala (98%) rename main/{server => command}/src/main/scala/sbt/server/Server.scala (98%) rename main/{server => command}/src/main/scala/sbt/server/protocol.scala (74%) diff --git a/build.sbt b/build.sbt index 9d8b039c7..20f0b16fa 100644 --- a/build.sbt +++ b/build.sbt @@ -176,12 +176,11 @@ 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")). - dependsOn(serverProj). settings( testedBaseSettings, name := "Command", libraryDependencies ++= Seq(launcherInterface, compilerInterface, - sbtIO, utilLogging, utilCompletion, compilerClasspath) + sbtIO, utilLogging, utilCompletion, compilerClasspath, json4s, json4sNative) // to transitively get json4s) ) // Fixes scope=Scope for Setting (core defined in collectionProj) to define the settings system used in build definitions @@ -194,15 +193,9 @@ lazy val mainSettingsProj = (project in file("main-settings")). utilLogging, sbtIO, utilCompletion, compilerClasspath, libraryManagement) ) -lazy val serverProj = (project in mainPath / "server"). - settings( - baseSettings, - libraryDependencies ++= Seq(json4s, json4sNative) // to transitively get json4s - ) - // The main integration project for sbt. It brings all of the Projsystems together, configures them, and provides for overriding conventions. lazy val mainProj = (project in file("main")). - dependsOn(actionsProj, mainSettingsProj, runProj, commandProj, serverProj). + dependsOn(actionsProj, mainSettingsProj, runProj, commandProj). settings( testedBaseSettings, name := "Main", @@ -251,7 +244,7 @@ lazy val myProvided = config("provided") intransitive def allProjects = Seq( testingProj, testAgentProj, taskProj, stdTaskProj, runProj, scriptedSbtProj, scriptedPluginProj, - actionsProj, commandProj, mainSettingsProj, serverProj, mainProj, sbtProj, bundledLauncherProj) + actionsProj, commandProj, mainSettingsProj, mainProj, sbtProj, bundledLauncherProj) def projectsWithMyProvided = allProjects.map(p => p.copy(configurations = (p.configurations.filter(_ != Provided)) :+ myProvided)) lazy val nonRoots = projectsWithMyProvided.map(p => LocalProject(p.id)) diff --git a/main-command/src/main/scala/sbt/BasicCommandStrings.scala b/main-command/src/main/scala/sbt/BasicCommandStrings.scala index 032b51eda..bfd424a65 100644 --- a/main-command/src/main/scala/sbt/BasicCommandStrings.scala +++ b/main-command/src/main/scala/sbt/BasicCommandStrings.scala @@ -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" diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 937de57bb..54f75a75d 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -13,14 +13,16 @@ import BasicCommandStrings._ import CommandUtil._ import BasicKeys._ -import sbt.server.Server import java.io.File import sbt.io.IO +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean import scala.util.control.NonFatal +import scala.annotation.tailrec 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) @@ -180,9 +182,6 @@ object BasicCommands { } def shell = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s => - // TODO hook it in, start in the right place, shutdown on termination - val server = Server.start("127.0.0.1", 12700) - 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) @@ -195,6 +194,32 @@ object BasicCommands { } } + var askingAlready = false + val commandListers = Seq(new ConsoleListener(), new NetworkListener()) + val commandQueue: ConcurrentLinkedQueue[Option[String]] = new ConcurrentLinkedQueue() + + @tailrec def blockUntilNextCommand: Option[String] = + Option(commandQueue.poll) match { + case Some(x) => x + case None => + Thread.sleep(50) + blockUntilNextCommand + } + def server = Command.command(Server, Help.more(Server, ServerDetailed)) { s => + if (!askingAlready) { + commandListers foreach { x => + x.run(commandQueue, CommandStatus(s, true)) + } + } + blockUntilNextCommand match { + case Some(line) => + // tell listern to be inactive . + val newState = s.copy(onFailure = Some(Server), remainingCommands = line +: Server +: s.remainingCommands).setInteractive(true) + if (line.trim.isEmpty) newState else newState.clearGlobalLog + case None => s.setInteractive(false) + } + } + def read = Command.make(ReadCommand, Help.more(ReadCommand, ReadDetailed))(s => applyEffect(readParser(s))(doRead(s))) def readParser(s: State) = { diff --git a/main/command/src/main/scala/sbt/CommandListener.scala b/main/command/src/main/scala/sbt/CommandListener.scala new file mode 100644 index 000000000..fcc71cb85 --- /dev/null +++ b/main/command/src/main/scala/sbt/CommandListener.scala @@ -0,0 +1,12 @@ +package sbt + +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean + +trait CommandListener { + // represents a loop that keeps asking an IO device for String input + def run(queue: ConcurrentLinkedQueue[Option[String]], + status: CommandStatus): Unit + def shutdown(): Unit + def setStatus(status: CommandStatus): Unit +} diff --git a/main/command/src/main/scala/sbt/CommandStatus.scala b/main/command/src/main/scala/sbt/CommandStatus.scala new file mode 100644 index 000000000..60d0eb76c --- /dev/null +++ b/main/command/src/main/scala/sbt/CommandStatus.scala @@ -0,0 +1,3 @@ +package sbt + +case class CommandStatus(state: State, canEnter: Boolean) diff --git a/main/command/src/main/scala/sbt/ConsoleListener.scala b/main/command/src/main/scala/sbt/ConsoleListener.scala new file mode 100644 index 000000000..1a2f7ae49 --- /dev/null +++ b/main/command/src/main/scala/sbt/ConsoleListener.scala @@ -0,0 +1,31 @@ +package sbt + +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } + +class ConsoleListener extends CommandListener { + + // 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) + // val line = reader.readLine(prompt) + // line match { + // case Some(line) => + // val newState = s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands).setInteractive(true) + // if (line.trim.isEmpty) newState else newState.clearGlobalLog + // case None => s.setInteractive(false) + // } + + def run(queue: ConcurrentLinkedQueue[Option[String]], + status: CommandStatus): Unit = + { + // spawn thread and loop + } + + def shutdown(): Unit = + { + // interrupt and kill the thread + } + + def setStatus(status: CommandStatus): Unit = ??? +} diff --git a/main/command/src/main/scala/sbt/NetworkListener.scala b/main/command/src/main/scala/sbt/NetworkListener.scala new file mode 100644 index 000000000..6853b256c --- /dev/null +++ b/main/command/src/main/scala/sbt/NetworkListener.scala @@ -0,0 +1,22 @@ +package sbt + +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean + +import server.Server + +class NetworkListener extends CommandListener { + def run(queue: ConcurrentLinkedQueue[Option[String]], + status: CommandStatus): Unit = + { + val server = Server.start("127.0.0.1", 12700) + // spawn thread and loop + } + + def shutdown(): Unit = + { + // interrupt and kill the thread + } + + def setStatus(status: CommandStatus): Unit = ??? +} diff --git a/main/server/src/main/scala/sbt/server/ClientConnection.scala b/main/command/src/main/scala/sbt/server/ClientConnection.scala similarity index 98% rename from main/server/src/main/scala/sbt/server/ClientConnection.scala rename to main/command/src/main/scala/sbt/server/ClientConnection.scala index 2e6426b51..34bbb9865 100644 --- a/main/server/src/main/scala/sbt/server/ClientConnection.scala +++ b/main/command/src/main/scala/sbt/server/ClientConnection.scala @@ -1,7 +1,8 @@ /* * Copyright (C) 2016 Lightbend Inc. */ -package sbt.server +package sbt +package server import java.net.{ SocketTimeoutException, Socket } import java.util.concurrent.atomic.AtomicBoolean diff --git a/main/server/src/main/scala/sbt/server/Serialization.scala b/main/command/src/main/scala/sbt/server/Serialization.scala similarity index 98% rename from main/server/src/main/scala/sbt/server/Serialization.scala rename to main/command/src/main/scala/sbt/server/Serialization.scala index 50730798b..37a4742c9 100644 --- a/main/server/src/main/scala/sbt/server/Serialization.scala +++ b/main/command/src/main/scala/sbt/server/Serialization.scala @@ -1,7 +1,8 @@ /* * Copyright (C) 2016 Lightbend Inc. */ -package sbt.server +package sbt +package server import org.json4s.JsonAST.{ JArray, JString } import org.json4s._ diff --git a/main/server/src/main/scala/sbt/server/Server.scala b/main/command/src/main/scala/sbt/server/Server.scala similarity index 98% rename from main/server/src/main/scala/sbt/server/Server.scala rename to main/command/src/main/scala/sbt/server/Server.scala index 0d8b46941..9a4302ff1 100644 --- a/main/server/src/main/scala/sbt/server/Server.scala +++ b/main/command/src/main/scala/sbt/server/Server.scala @@ -1,7 +1,8 @@ /* * Copyright (C) 2016 Lightbend Inc. */ -package sbt.server +package sbt +package server import java.net.{ SocketTimeoutException, InetAddress, ServerSocket } import java.util.concurrent.ConcurrentLinkedQueue diff --git a/main/server/src/main/scala/sbt/server/protocol.scala b/main/command/src/main/scala/sbt/server/protocol.scala similarity index 74% rename from main/server/src/main/scala/sbt/server/protocol.scala rename to main/command/src/main/scala/sbt/server/protocol.scala index 714a640ec..e96392edb 100644 --- a/main/server/src/main/scala/sbt/server/protocol.scala +++ b/main/command/src/main/scala/sbt/server/protocol.scala @@ -1,7 +1,8 @@ /* * Copyright (C) 2016 Lightbend Inc. */ -package sbt.server +package sbt +package server trait Event From ff211d08f9d930d98847344e1c5464a5f9b90dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andre=CC=81n?= Date: Wed, 2 Mar 2016 13:45:25 +0100 Subject: [PATCH 05/18] Server hooked in as a CommandListener --- .../src/main/scala/sbt/BasicCommands.scala | 1 + .../src/main/scala/sbt/NetworkListener.scala | 27 +++++++--- .../scala/sbt/server/ClientConnection.scala | 52 ++++++++++--------- .../main/scala/sbt/server/Serialization.scala | 36 ++++++++----- .../src/main/scala/sbt/server/Server.scala | 26 +++------- .../src/main/scala/sbt/server/protocol.scala | 22 +++++--- 6 files changed, 96 insertions(+), 68 deletions(-) diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 54f75a75d..e41ccd378 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -210,6 +210,7 @@ object BasicCommands { commandListers foreach { x => x.run(commandQueue, CommandStatus(s, true)) } + askingAlready = true } blockUntilNextCommand match { case Some(line) => diff --git a/main/command/src/main/scala/sbt/NetworkListener.scala b/main/command/src/main/scala/sbt/NetworkListener.scala index 6853b256c..9f1c31b60 100644 --- a/main/command/src/main/scala/sbt/NetworkListener.scala +++ b/main/command/src/main/scala/sbt/NetworkListener.scala @@ -1,22 +1,37 @@ package sbt import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicBoolean -import server.Server +import sbt.server._ + +private[sbt] final class NetworkListener extends CommandListener { + + private var server: Option[ServerInstance] = None -class NetworkListener extends CommandListener { def run(queue: ConcurrentLinkedQueue[Option[String]], status: CommandStatus): Unit = { - val server = Server.start("127.0.0.1", 12700) - // spawn thread and loop + def onCommand(command: sbt.server.Command): Unit = { + command match { + case Execution(cmd) => queue.add(Some(cmd)) + } + } + + server = Some(Server.start("127.0.0.1", 12700, onCommand)) } def shutdown(): Unit = { // interrupt and kill the thread + server.foreach(_.shutdown()) } - def setStatus(status: CommandStatus): Unit = ??? + def setStatus(cmdStatus: CommandStatus): 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/server/ClientConnection.scala b/main/command/src/main/scala/sbt/server/ClientConnection.scala index 34bbb9865..42f914693 100644 --- a/main/command/src/main/scala/sbt/server/ClientConnection.scala +++ b/main/command/src/main/scala/sbt/server/ClientConnection.scala @@ -9,7 +9,6 @@ import java.util.concurrent.atomic.AtomicBoolean abstract class ClientConnection(connection: Socket) { - // TODO handle client disconnect private val running = new AtomicBoolean(true) private val delimiter: Byte = '\n'.toByte @@ -17,35 +16,38 @@ abstract class ClientConnection(connection: Socket) { val thread = new Thread(s"sbt-client-${connection.getPort}") { override def run(): Unit = { - 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 + 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) + // 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 + ) + } - Serialization.deserialize(chunk).fold( - errorDesc => println("Got invalid chunk from client: " + errorDesc), - onCommand - ) + } catch { + case _: SocketTimeoutException => // its ok } - - } catch { - case _: SocketTimeoutException => // its ok } - } - shutdown() + } finally { + shutdown() + } } } thread.start() diff --git a/main/command/src/main/scala/sbt/server/Serialization.scala b/main/command/src/main/scala/sbt/server/Serialization.scala index 37a4742c9..0b8132a5c 100644 --- a/main/command/src/main/scala/sbt/server/Serialization.scala +++ b/main/command/src/main/scala/sbt/server/Serialization.scala @@ -17,25 +17,32 @@ object Serialization { } def toJson(event: Event): JObject = event match { - case LogEvent() => + case LogEvent(level, message) => JObject( "type" -> JString("log_event"), - "level" -> JString("INFO"), - "message" -> JString("todo") + "level" -> JString(level), + "message" -> JString(message) ) - case StatusEvent() => + case StatusEvent(Ready) => JObject( "type" -> JString("status_event"), "status" -> JString("ready"), "command_queue" -> JArray(List.empty) ) - case ExecutionEvent() => + case StatusEvent(Processing(command, commandQueue)) => + JObject( + "type" -> JString("status_event"), + "status" -> JString("processing"), + "command_queue" -> JArray(commandQueue.map(JString).toList) + ) + + case ExecutionEvent(command, status) => JObject( "type" -> JString("execution_event"), - "command" -> JString("project todo"), - "success" -> JArray(List.empty) + "command" -> JString(command), + "success" -> JBool(status) ) } @@ -47,13 +54,16 @@ object Serialization { val json = parse(new String(bytes.toArray, "UTF-8")) implicit val formats = DefaultFormats - // TODO: is using extract safe? - (json \ "type").extract[String] match { - case "execution" => Right(Execution((json \ "command_line").extract[String])) - case cmd => Left(s"Unknown command type $cmd") + (json \ "type").toOption match { + case Some(JString("execution")) => + (json \ "command_line").toOption match { + case Some(JString(cmd)) => Right(Execution(cmd)) + case _ => Left("Missing or invalid command_line field") + } + case Some(cmd) => Left(s"Unknown command type $cmd") + case None => Left("Invalid command, missing type field") } } catch { - case e: ParseException => Left(s"Parse error: ${e.getMessage}") - case e: MappingException => Left(s"Missing type field") + case e: ParseException => Left(s"Parse error: ${e.getMessage}") } } diff --git a/main/command/src/main/scala/sbt/server/Server.scala b/main/command/src/main/scala/sbt/server/Server.scala index 9a4302ff1..672b9ab89 100644 --- a/main/command/src/main/scala/sbt/server/Server.scala +++ b/main/command/src/main/scala/sbt/server/Server.scala @@ -8,27 +8,25 @@ import java.net.{ SocketTimeoutException, InetAddress, ServerSocket } import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean -sealed trait ServerInstance { +private[sbt] sealed trait ServerInstance { def shutdown(): Unit - def nextCommand(): Option[Command] + def publish(event: Event): Unit } -object Server { - def start(host: String, port: Int): ServerInstance = +private[sbt] object Server { + def start(host: String, port: Int, onIncommingCommand: Command => Unit): ServerInstance = new ServerInstance { - val commandQueue = new ConcurrentLinkedQueue[Command]() - val lock = new AnyRef {} var clients = Vector[ClientConnection]() val running = new AtomicBoolean(true) - val serverSocket = new ServerSocket(port, 50, InetAddress.getByName(host)) - serverSocket.setSoTimeout(5000) - val serverThread = new Thread("sbt-socket-server") { override def run(): Unit = { + val serverSocket = new ServerSocket(port, 50, InetAddress.getByName(host)) + serverSocket.setSoTimeout(5000) + println(s"SBT socket server started at $host:$port") while (running.get()) { try { @@ -37,8 +35,7 @@ object Server { val connection = new ClientConnection(socket) { override def onCommand(command: Command): Unit = { - println(s"onCommand $command") - commandQueue.add(command) + onIncommingCommand(command) } } @@ -64,13 +61,6 @@ object Server { } } - /** - * @return The next queued command if there is one. It will have to be consumed because it is taken off the queue. - */ - def nextCommand(): Option[Command] = { - Option(commandQueue.poll()) - } - override def shutdown(): Unit = { println("Shutting down server") running.set(false) diff --git a/main/command/src/main/scala/sbt/server/protocol.scala b/main/command/src/main/scala/sbt/server/protocol.scala index e96392edb..dab061021 100644 --- a/main/command/src/main/scala/sbt/server/protocol.scala +++ b/main/command/src/main/scala/sbt/server/protocol.scala @@ -4,12 +4,22 @@ package sbt package server -trait Event +/* + * 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 -case class LogEvent() extends Event -case class StatusEvent() extends Event -case class ExecutionEvent() extends Event +private[sbt] final case class LogEvent(level: String, message: String) extends Event -trait Command +sealed trait Status +private[sbt] final case object Ready extends Status +private[sbt] final case class Processing(command: String, commandQueue: Seq[String]) extends Status -case class Execution(cmd: String) extends Command \ No newline at end of file +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 \ No newline at end of file From 3381f59ae86e0cb56c7b98d67e2ff0d84ff290b1 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 18 Mar 2016 02:15:56 -0400 Subject: [PATCH 06/18] Implement ConsoleListener --- .../src/main/scala/sbt/BasicCommands.scala | 30 +++++--- .../src/main/scala/sbt/CommandListener.scala | 7 +- .../src/main/scala/sbt/ConsoleListener.scala | 73 ++++++++++++++----- .../src/main/scala/sbt/NetworkListener.scala | 13 +++- .../scala/sbt/server/ClientConnection.scala | 2 +- main/src/main/scala/sbt/Main.scala | 2 +- 6 files changed, 88 insertions(+), 39 deletions(-) diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index e41ccd378..030458e21 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -194,30 +194,38 @@ object BasicCommands { } } - var askingAlready = false - val commandListers = Seq(new ConsoleListener(), new NetworkListener()) - val commandQueue: ConcurrentLinkedQueue[Option[String]] = new ConcurrentLinkedQueue() - - @tailrec def blockUntilNextCommand: Option[String] = + private[sbt] var askingAlready = false + private[sbt] val commandQueue: ConcurrentLinkedQueue[(String, Option[String])] = new ConcurrentLinkedQueue() + private[sbt] val commandListers = Seq(new ConsoleListener(commandQueue), new NetworkListener(commandQueue)) + @tailrec private[sbt] def blockUntilNextCommand: (String, Option[String]) = Option(commandQueue.poll) match { case Some(x) => x - case None => + case _ => Thread.sleep(50) blockUntilNextCommand } def server = Command.command(Server, Help.more(Server, ServerDetailed)) { s => - if (!askingAlready) { + if (askingAlready) { commandListers foreach { x => - x.run(commandQueue, CommandStatus(s, true)) + x.resume(CommandStatus(s, true)) + } + } else { + commandListers foreach { x => + x.run(CommandStatus(s, true)) } askingAlready = true } blockUntilNextCommand match { - case Some(line) => - // tell listern to be inactive . + case (source, Some(line)) => + if (source != "human") { + println(line) + } + commandListers foreach { x => + x.pause() + } val newState = s.copy(onFailure = Some(Server), remainingCommands = line +: Server +: s.remainingCommands).setInteractive(true) if (line.trim.isEmpty) newState else newState.clearGlobalLog - case None => s.setInteractive(false) + case _ => s.setInteractive(false) } } diff --git a/main/command/src/main/scala/sbt/CommandListener.scala b/main/command/src/main/scala/sbt/CommandListener.scala index fcc71cb85..320daf031 100644 --- a/main/command/src/main/scala/sbt/CommandListener.scala +++ b/main/command/src/main/scala/sbt/CommandListener.scala @@ -3,10 +3,11 @@ package sbt import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean -trait CommandListener { +abstract class CommandListener(queue: ConcurrentLinkedQueue[(String, Option[String])]) { // represents a loop that keeps asking an IO device for String input - def run(queue: ConcurrentLinkedQueue[Option[String]], - status: CommandStatus): Unit + def run(status: CommandStatus): Unit def shutdown(): Unit def setStatus(status: CommandStatus): Unit + def pause(): Unit + def resume(status: CommandStatus): Unit } diff --git a/main/command/src/main/scala/sbt/ConsoleListener.scala b/main/command/src/main/scala/sbt/ConsoleListener.scala index 1a2f7ae49..cff2577fd 100644 --- a/main/command/src/main/scala/sbt/ConsoleListener.scala +++ b/main/command/src/main/scala/sbt/ConsoleListener.scala @@ -1,31 +1,66 @@ package sbt +import sbt.internal.util._ import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } +import BasicKeys._ +import java.io.File -class ConsoleListener extends CommandListener { +private[sbt] final class ConsoleListener(queue: ConcurrentLinkedQueue[(String, Option[String])]) extends CommandListener(queue) { + 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) + // val line = reader.readLine(prompt) + // line match { + // case Some(line) => + // val newState = s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands).setInteractive(true) + // if (line.trim.isEmpty) newState else newState.clearGlobalLog + // case None => s.setInteractive(false) + // } + val reader = JLine.simple(None, false) + override def run(): Unit = { + try { + val line = reader.readLine(prompt) + line map { x => queue.add(("human", Some(x))) } + } catch { + case e: InterruptedException => + } + } + } - // 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) - // val line = reader.readLine(prompt) - // line match { - // case Some(line) => - // val newState = s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands).setInteractive(true) - // if (line.trim.isEmpty) newState else newState.clearGlobalLog - // case None => s.setInteractive(false) - // } - - def run(queue: ConcurrentLinkedQueue[Option[String]], - status: CommandStatus): Unit = - { - // spawn thread and loop + def run(status: CommandStatus): Unit = + askUserThread match { + case Some(x) if x.isAlive => // + case _ => + val x = makeAskUserThread(status) + x.start + askUserThread = Some(x) } def shutdown(): Unit = - { - // interrupt and kill the thread + askUserThread match { + case Some(x) if x.isAlive => + x.interrupt + askUserThread = None + println("shutdown ask user thread") + case _ => () } - def setStatus(status: CommandStatus): Unit = ??? + def pause(): Unit = shutdown() + + def resume(status: CommandStatus): Unit = + askUserThread match { + case Some(x) if x.isAlive => // + println("resume??") + case _ => + val x = makeAskUserThread(status) + println("resume") + x.start + askUserThread = Some(x) + } + + def setStatus(status: CommandStatus): Unit = () } diff --git a/main/command/src/main/scala/sbt/NetworkListener.scala b/main/command/src/main/scala/sbt/NetworkListener.scala index 9f1c31b60..4c155ac00 100644 --- a/main/command/src/main/scala/sbt/NetworkListener.scala +++ b/main/command/src/main/scala/sbt/NetworkListener.scala @@ -4,16 +4,15 @@ import java.util.concurrent.ConcurrentLinkedQueue import sbt.server._ -private[sbt] final class NetworkListener extends CommandListener { +private[sbt] final class NetworkListener(queue: ConcurrentLinkedQueue[(String, Option[String])]) extends CommandListener(queue) { private var server: Option[ServerInstance] = None - def run(queue: ConcurrentLinkedQueue[Option[String]], - status: CommandStatus): Unit = + def run(status: CommandStatus): Unit = { def onCommand(command: sbt.server.Command): Unit = { command match { - case Execution(cmd) => queue.add(Some(cmd)) + case Execution(cmd) => queue.add(("network", Some(cmd))) } } @@ -26,6 +25,12 @@ private[sbt] final class NetworkListener extends CommandListener { server.foreach(_.shutdown()) } + // network doesn't pause or resume + def pause(): Unit = () + + // network doesn't pause or resume + def resume(status: CommandStatus): Unit = () + def setStatus(cmdStatus: CommandStatus): Unit = { server.foreach(server => server.publish( diff --git a/main/command/src/main/scala/sbt/server/ClientConnection.scala b/main/command/src/main/scala/sbt/server/ClientConnection.scala index 42f914693..d3de48152 100644 --- a/main/command/src/main/scala/sbt/server/ClientConnection.scala +++ b/main/command/src/main/scala/sbt/server/ClientConnection.scala @@ -33,7 +33,7 @@ abstract class ClientConnection(connection: Socket) { 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 diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 279da153a..58c2ccc86 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -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 From 1b1f2abfbe4bfbe5174a428f0ba20619564dd089 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 18 Mar 2016 23:29:36 -0400 Subject: [PATCH 07/18] Use util 0.1.0-M9 that implements thread-friendly readLine --- .../src/main/scala/sbt/ConsoleListener.scala | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/main/command/src/main/scala/sbt/ConsoleListener.scala b/main/command/src/main/scala/sbt/ConsoleListener.scala index cff2577fd..6dda42aa1 100644 --- a/main/command/src/main/scala/sbt/ConsoleListener.scala +++ b/main/command/src/main/scala/sbt/ConsoleListener.scala @@ -12,15 +12,7 @@ private[sbt] final class ConsoleListener(queue: ConcurrentLinkedQueue[(String, O 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) - // val line = reader.readLine(prompt) - // line match { - // case Some(line) => - // val newState = s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands).setInteractive(true) - // if (line.trim.isEmpty) newState else newState.clearGlobalLog - // case None => s.setInteractive(false) - // } - val reader = JLine.simple(None, false) + val reader = new FullReader(history, s.combinedParser) override def run(): Unit = { try { val line = reader.readLine(prompt) @@ -45,7 +37,6 @@ private[sbt] final class ConsoleListener(queue: ConcurrentLinkedQueue[(String, O case Some(x) if x.isAlive => x.interrupt askUserThread = None - println("shutdown ask user thread") case _ => () } @@ -54,10 +45,8 @@ private[sbt] final class ConsoleListener(queue: ConcurrentLinkedQueue[(String, O def resume(status: CommandStatus): Unit = askUserThread match { case Some(x) if x.isAlive => // - println("resume??") case _ => val x = makeAskUserThread(status) - println("resume") x.start askUserThread = Some(x) } From 19b079caf261686da8b4b33f5204fd1556b62481 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 18 Mar 2016 23:41:39 -0400 Subject: [PATCH 08/18] Change type to command_exec --- main/command/src/main/scala/sbt/server/Serialization.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/command/src/main/scala/sbt/server/Serialization.scala b/main/command/src/main/scala/sbt/server/Serialization.scala index 0b8132a5c..648a85807 100644 --- a/main/command/src/main/scala/sbt/server/Serialization.scala +++ b/main/command/src/main/scala/sbt/server/Serialization.scala @@ -55,7 +55,7 @@ object Serialization { implicit val formats = DefaultFormats (json \ "type").toOption match { - case Some(JString("execution")) => + case Some(JString("command_exec")) => (json \ "command_line").toOption match { case Some(JString(cmd)) => Right(Execution(cmd)) case _ => Left("Missing or invalid command_line field") From 48d3b01e6b1683bbfbf6428d2ae1da84a80f2d05 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 19 Mar 2016 00:05:16 -0400 Subject: [PATCH 09/18] Move files around --- main-command/src/main/scala/sbt/BasicCommands.scala | 1 + .../src/main/scala/sbt/{ => internal}/CommandListener.scala | 1 + .../src/main/scala/sbt/{ => internal}/ConsoleListener.scala | 1 + .../src/main/scala/sbt/{ => internal}/NetworkListener.scala | 5 +++-- .../scala/sbt/{ => internal}/server/ClientConnection.scala | 1 + .../main/scala/sbt/{ => internal}/server/Serialization.scala | 1 + .../src/main/scala/sbt/{ => internal}/server/Server.scala | 3 ++- .../src/main/scala/sbt/{ => internal}/server/protocol.scala | 3 ++- 8 files changed, 12 insertions(+), 4 deletions(-) rename main/command/src/main/scala/sbt/{ => internal}/CommandListener.scala (96%) rename main/command/src/main/scala/sbt/{ => internal}/ConsoleListener.scala (98%) rename main/command/src/main/scala/sbt/{ => internal}/NetworkListener.scala (90%) rename main/command/src/main/scala/sbt/{ => internal}/server/ClientConnection.scala (99%) rename main/command/src/main/scala/sbt/{ => internal}/server/Serialization.scala (99%) rename main/command/src/main/scala/sbt/{ => internal}/server/Server.scala (99%) rename main/command/src/main/scala/sbt/{ => internal}/server/protocol.scala (97%) diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 030458e21..4fbae934d 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -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.{ NetworkListener, ConsoleListener } import DefaultParsers._ import Function.tupled import Command.applyEffect diff --git a/main/command/src/main/scala/sbt/CommandListener.scala b/main/command/src/main/scala/sbt/internal/CommandListener.scala similarity index 96% rename from main/command/src/main/scala/sbt/CommandListener.scala rename to main/command/src/main/scala/sbt/internal/CommandListener.scala index 320daf031..2ea5fc7ee 100644 --- a/main/command/src/main/scala/sbt/CommandListener.scala +++ b/main/command/src/main/scala/sbt/internal/CommandListener.scala @@ -1,4 +1,5 @@ package sbt +package internal import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean diff --git a/main/command/src/main/scala/sbt/ConsoleListener.scala b/main/command/src/main/scala/sbt/internal/ConsoleListener.scala similarity index 98% rename from main/command/src/main/scala/sbt/ConsoleListener.scala rename to main/command/src/main/scala/sbt/internal/ConsoleListener.scala index 6dda42aa1..59bee14a7 100644 --- a/main/command/src/main/scala/sbt/ConsoleListener.scala +++ b/main/command/src/main/scala/sbt/internal/ConsoleListener.scala @@ -1,4 +1,5 @@ package sbt +package internal import sbt.internal.util._ import java.util.concurrent.ConcurrentLinkedQueue diff --git a/main/command/src/main/scala/sbt/NetworkListener.scala b/main/command/src/main/scala/sbt/internal/NetworkListener.scala similarity index 90% rename from main/command/src/main/scala/sbt/NetworkListener.scala rename to main/command/src/main/scala/sbt/internal/NetworkListener.scala index 4c155ac00..e3bc7fda5 100644 --- a/main/command/src/main/scala/sbt/NetworkListener.scala +++ b/main/command/src/main/scala/sbt/internal/NetworkListener.scala @@ -1,8 +1,9 @@ package sbt +package internal import java.util.concurrent.ConcurrentLinkedQueue -import sbt.server._ +import sbt.internal.server._ private[sbt] final class NetworkListener(queue: ConcurrentLinkedQueue[(String, Option[String])]) extends CommandListener(queue) { @@ -10,7 +11,7 @@ private[sbt] final class NetworkListener(queue: ConcurrentLinkedQueue[(String, O def run(status: CommandStatus): Unit = { - def onCommand(command: sbt.server.Command): Unit = { + def onCommand(command: internal.server.Command): Unit = { command match { case Execution(cmd) => queue.add(("network", Some(cmd))) } diff --git a/main/command/src/main/scala/sbt/server/ClientConnection.scala b/main/command/src/main/scala/sbt/internal/server/ClientConnection.scala similarity index 99% rename from main/command/src/main/scala/sbt/server/ClientConnection.scala rename to main/command/src/main/scala/sbt/internal/server/ClientConnection.scala index d3de48152..26b8188fb 100644 --- a/main/command/src/main/scala/sbt/server/ClientConnection.scala +++ b/main/command/src/main/scala/sbt/internal/server/ClientConnection.scala @@ -2,6 +2,7 @@ * Copyright (C) 2016 Lightbend Inc. */ package sbt +package internal package server import java.net.{ SocketTimeoutException, Socket } diff --git a/main/command/src/main/scala/sbt/server/Serialization.scala b/main/command/src/main/scala/sbt/internal/server/Serialization.scala similarity index 99% rename from main/command/src/main/scala/sbt/server/Serialization.scala rename to main/command/src/main/scala/sbt/internal/server/Serialization.scala index 648a85807..e399cd450 100644 --- a/main/command/src/main/scala/sbt/server/Serialization.scala +++ b/main/command/src/main/scala/sbt/internal/server/Serialization.scala @@ -2,6 +2,7 @@ * Copyright (C) 2016 Lightbend Inc. */ package sbt +package internal package server import org.json4s.JsonAST.{ JArray, JString } diff --git a/main/command/src/main/scala/sbt/server/Server.scala b/main/command/src/main/scala/sbt/internal/server/Server.scala similarity index 99% rename from main/command/src/main/scala/sbt/server/Server.scala rename to main/command/src/main/scala/sbt/internal/server/Server.scala index 672b9ab89..e8c846de7 100644 --- a/main/command/src/main/scala/sbt/server/Server.scala +++ b/main/command/src/main/scala/sbt/internal/server/Server.scala @@ -2,6 +2,7 @@ * Copyright (C) 2016 Lightbend Inc. */ package sbt +package internal package server import java.net.{ SocketTimeoutException, InetAddress, ServerSocket } @@ -67,4 +68,4 @@ private[sbt] object Server { } } -} \ No newline at end of file +} diff --git a/main/command/src/main/scala/sbt/server/protocol.scala b/main/command/src/main/scala/sbt/internal/server/protocol.scala similarity index 97% rename from main/command/src/main/scala/sbt/server/protocol.scala rename to main/command/src/main/scala/sbt/internal/server/protocol.scala index dab061021..7c16b0fc3 100644 --- a/main/command/src/main/scala/sbt/server/protocol.scala +++ b/main/command/src/main/scala/sbt/internal/server/protocol.scala @@ -2,6 +2,7 @@ * Copyright (C) 2016 Lightbend Inc. */ package sbt +package internal package server /* @@ -22,4 +23,4 @@ private[sbt] final case class ExecutionEvent(command: String, success: Boolean) private[sbt] sealed trait Command -private[sbt] final case class Execution(cmd: String) extends Command \ No newline at end of file +private[sbt] final case class Execution(cmd: String) extends Command From ebf4715dd108b014418e9a6642d45eecda0aa4e3 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 19 Mar 2016 02:36:16 -0400 Subject: [PATCH 10/18] Refactor to CommandExchange and CommandChannel --- .../src/main/scala/sbt/BasicCommands.scala | 44 +++++-------------- main-command/src/main/scala/sbt/State.scala | 3 ++ .../src/main/scala/sbt/CommandStatus.scala | 3 -- .../scala/sbt/internal/CommandChannel.scala | 29 ++++++++++++ .../scala/sbt/internal/CommandExchange.scala | 27 ++++++++++++ .../scala/sbt/internal/CommandListener.scala | 14 ------ ...oleListener.scala => ConsoleChannel.scala} | 43 ++++++++++-------- ...orkListener.scala => NetworkChannel.scala} | 19 ++++---- 8 files changed, 104 insertions(+), 78 deletions(-) delete mode 100644 main/command/src/main/scala/sbt/CommandStatus.scala create mode 100644 main/command/src/main/scala/sbt/internal/CommandChannel.scala create mode 100644 main/command/src/main/scala/sbt/internal/CommandExchange.scala delete mode 100644 main/command/src/main/scala/sbt/internal/CommandListener.scala rename main/command/src/main/scala/sbt/internal/{ConsoleListener.scala => ConsoleChannel.scala} (52%) rename main/command/src/main/scala/sbt/internal/{NetworkListener.scala => NetworkChannel.scala} (58%) diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 4fbae934d..506e28509 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -6,7 +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.{ NetworkListener, ConsoleListener } +import sbt.internal.{ CommandRequest, CommandSource, CommandStatus } import DefaultParsers._ import Function.tupled import Command.applyEffect @@ -16,11 +16,9 @@ import BasicKeys._ import java.io.File import sbt.io.IO -import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean import scala.util.control.NonFatal -import scala.annotation.tailrec 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 @@ -195,39 +193,19 @@ object BasicCommands { } } - private[sbt] var askingAlready = false - private[sbt] val commandQueue: ConcurrentLinkedQueue[(String, Option[String])] = new ConcurrentLinkedQueue() - private[sbt] val commandListers = Seq(new ConsoleListener(commandQueue), new NetworkListener(commandQueue)) - @tailrec private[sbt] def blockUntilNextCommand: (String, Option[String]) = - Option(commandQueue.poll) match { - case Some(x) => x - case _ => - Thread.sleep(50) - blockUntilNextCommand - } def server = Command.command(Server, Help.more(Server, ServerDetailed)) { s => - if (askingAlready) { - commandListers foreach { x => - x.resume(CommandStatus(s, true)) - } - } else { - commandListers foreach { x => - x.run(CommandStatus(s, true)) - } - askingAlready = true + val exchange = State.exchange + exchange.channels foreach { x => + x.runOrResume(CommandStatus(s, true)) + x.setStatus(CommandStatus(s, true), None) } - blockUntilNextCommand match { - case (source, Some(line)) => - if (source != "human") { - println(line) - } - commandListers foreach { x => - x.pause() - } - val newState = s.copy(onFailure = Some(Server), remainingCommands = line +: Server +: s.remainingCommands).setInteractive(true) - if (line.trim.isEmpty) newState else newState.clearGlobalLog - case _ => s.setInteractive(false) + val CommandRequest(source, line) = exchange.blockUntilNextCommand + val newState = s.copy(onFailure = Some(Server), remainingCommands = line +: Server +: s.remainingCommands).setInteractive(true) + exchange.channels foreach { x => + x.setStatus(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))) diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index f4b4041cf..61edef3e6 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -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 = diff --git a/main/command/src/main/scala/sbt/CommandStatus.scala b/main/command/src/main/scala/sbt/CommandStatus.scala deleted file mode 100644 index 60d0eb76c..000000000 --- a/main/command/src/main/scala/sbt/CommandStatus.scala +++ /dev/null @@ -1,3 +0,0 @@ -package sbt - -case class CommandStatus(state: State, canEnter: Boolean) diff --git a/main/command/src/main/scala/sbt/internal/CommandChannel.scala b/main/command/src/main/scala/sbt/internal/CommandChannel.scala new file mode 100644 index 000000000..c6bf79049 --- /dev/null +++ b/main/command/src/main/scala/sbt/internal/CommandChannel.scala @@ -0,0 +1,29 @@ +package sbt +package internal + +/** + * 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(exchange: CommandExchange) { + /** start listening for a command request. */ + def runOrResume(status: CommandStatus): Unit + def setStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit + def shutdown(): Unit +} + +case class CommandRequest(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) diff --git a/main/command/src/main/scala/sbt/internal/CommandExchange.scala b/main/command/src/main/scala/sbt/internal/CommandExchange.scala new file mode 100644 index 000000000..11ecbed25 --- /dev/null +++ b/main/command/src/main/scala/sbt/internal/CommandExchange.scala @@ -0,0 +1,27 @@ +package sbt +package internal + +import java.util.concurrent.ConcurrentLinkedQueue +import scala.annotation.tailrec + +/** + * 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[CommandRequest] = new ConcurrentLinkedQueue() + lazy val channels = Seq(new ConsoleChannel(this), new NetworkChannel(this)) + + def append(request: CommandRequest): Boolean = + commandQueue.add(request) + + @tailrec def blockUntilNextCommand: CommandRequest = + Option(commandQueue.poll) match { + case Some(x) => x + case _ => + Thread.sleep(50) + blockUntilNextCommand + } +} diff --git a/main/command/src/main/scala/sbt/internal/CommandListener.scala b/main/command/src/main/scala/sbt/internal/CommandListener.scala deleted file mode 100644 index 2ea5fc7ee..000000000 --- a/main/command/src/main/scala/sbt/internal/CommandListener.scala +++ /dev/null @@ -1,14 +0,0 @@ -package sbt -package internal - -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicBoolean - -abstract class CommandListener(queue: ConcurrentLinkedQueue[(String, Option[String])]) { - // represents a loop that keeps asking an IO device for String input - def run(status: CommandStatus): Unit - def shutdown(): Unit - def setStatus(status: CommandStatus): Unit - def pause(): Unit - def resume(status: CommandStatus): Unit -} diff --git a/main/command/src/main/scala/sbt/internal/ConsoleListener.scala b/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala similarity index 52% rename from main/command/src/main/scala/sbt/internal/ConsoleListener.scala rename to main/command/src/main/scala/sbt/internal/ConsoleChannel.scala index 59bee14a7..f845c02ef 100644 --- a/main/command/src/main/scala/sbt/internal/ConsoleListener.scala +++ b/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala @@ -2,29 +2,33 @@ package sbt package internal import sbt.internal.util._ -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import BasicKeys._ import java.io.File -private[sbt] final class ConsoleListener(queue: ConcurrentLinkedQueue[(String, Option[String])]) extends CommandListener(queue) { +private[sbt] final class ConsoleChannel(exchange: CommandExchange) extends CommandChannel(exchange) { 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 prompt = (s get shellPrompt) match { + case Some(pf) => pf(s) + case None => "> " + } val reader = new FullReader(history, s.combinedParser) override def run(): Unit = { try { val line = reader.readLine(prompt) - line map { x => queue.add(("human", Some(x))) } + line match { + case Some(cmd) => exchange.append(CommandRequest(CommandSource.Human, cmd)) + case None => exchange.append(CommandRequest(CommandSource.Human, "exit")) + } } catch { case e: InterruptedException => } } } - def run(status: CommandStatus): Unit = + def runOrResume(status: CommandStatus): Unit = askUserThread match { case Some(x) if x.isAlive => // case _ => @@ -33,6 +37,20 @@ private[sbt] final class ConsoleListener(queue: ConcurrentLinkedQueue[(String, O askUserThread = Some(x) } + def setStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit = + if (status.canEnter) () + else { + shutdown() + lastSource match { + case Some(src) if src != CommandSource.Human => + val s = status.state + s.remainingCommands.headOption map { + println(_) + } + case _ => // + } + } + def shutdown(): Unit = askUserThread match { case Some(x) if x.isAlive => @@ -40,17 +58,4 @@ private[sbt] final class ConsoleListener(queue: ConcurrentLinkedQueue[(String, O askUserThread = None case _ => () } - - def pause(): Unit = shutdown() - - def resume(status: CommandStatus): Unit = - askUserThread match { - case Some(x) if x.isAlive => // - case _ => - val x = makeAskUserThread(status) - x.start - askUserThread = Some(x) - } - - def setStatus(status: CommandStatus): Unit = () } diff --git a/main/command/src/main/scala/sbt/internal/NetworkListener.scala b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala similarity index 58% rename from main/command/src/main/scala/sbt/internal/NetworkListener.scala rename to main/command/src/main/scala/sbt/internal/NetworkChannel.scala index e3bc7fda5..3d44a7b6a 100644 --- a/main/command/src/main/scala/sbt/internal/NetworkListener.scala +++ b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala @@ -1,29 +1,30 @@ package sbt package internal -import java.util.concurrent.ConcurrentLinkedQueue - import sbt.internal.server._ -private[sbt] final class NetworkListener(queue: ConcurrentLinkedQueue[(String, Option[String])]) extends CommandListener(queue) { - +private[sbt] final class NetworkChannel(exchange: CommandExchange) extends CommandChannel(exchange) { private var server: Option[ServerInstance] = None - def run(status: CommandStatus): Unit = + def runOrResume(status: CommandStatus): Unit = { def onCommand(command: internal.server.Command): Unit = { command match { - case Execution(cmd) => queue.add(("network", Some(cmd))) + case Execution(cmd) => exchange.append(CommandRequest(CommandSource.Network, cmd)) } } - - server = Some(Server.start("127.0.0.1", 12700, onCommand)) + server match { + case Some(x) => // do nothing + case _ => + server = Some(Server.start("127.0.0.1", 12700, onCommand)) + } } def shutdown(): Unit = { // interrupt and kill the thread server.foreach(_.shutdown()) + server = None } // network doesn't pause or resume @@ -32,7 +33,7 @@ private[sbt] final class NetworkListener(queue: ConcurrentLinkedQueue[(String, O // network doesn't pause or resume def resume(status: CommandStatus): Unit = () - def setStatus(cmdStatus: CommandStatus): Unit = { + def setStatus(cmdStatus: CommandStatus, lastSource: Option[CommandSource]): Unit = { server.foreach(server => server.publish( if (cmdStatus.canEnter) StatusEvent(Ready) From 3359163636eb2ca4ffa71f707d995998009a196f Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 19 Mar 2016 05:15:32 -0400 Subject: [PATCH 11/18] Adds serverPort attribute key --- main-command/src/main/scala/sbt/BasicKeys.scala | 1 + .../src/main/scala/sbt/internal/NetworkChannel.scala | 8 +++++++- main/src/main/scala/sbt/Defaults.scala | 5 +++-- main/src/main/scala/sbt/Keys.scala | 1 + main/src/main/scala/sbt/Project.scala | 9 +++++---- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index ca77d1d73..d42a1d381 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -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) diff --git a/main/command/src/main/scala/sbt/internal/NetworkChannel.scala b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala index 3d44a7b6a..701a4ecdb 100644 --- a/main/command/src/main/scala/sbt/internal/NetworkChannel.scala +++ b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala @@ -2,12 +2,18 @@ package sbt package internal import sbt.internal.server._ +import BasicKeys._ private[sbt] final class NetworkChannel(exchange: CommandExchange) extends CommandChannel(exchange) { private var server: Option[ServerInstance] = None def runOrResume(status: CommandStatus): Unit = { + val s = status.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) => exchange.append(CommandRequest(CommandSource.Network, cmd)) @@ -16,7 +22,7 @@ private[sbt] final class NetworkChannel(exchange: CommandExchange) extends Comma server match { case Some(x) => // do nothing case _ => - server = Some(Server.start("127.0.0.1", 12700, onCommand)) + server = Some(Server.start("127.0.0.1", port, onCommand)) } } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index ed54a98be..049bdbcfa 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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), diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index ee21d78b8..33ac5f3f2 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index dce3233e5..7ddcc6c82 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -6,9 +6,8 @@ package sbt 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 Project.{ Initialize => _, Setting => _, _ } +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 = From a89fcb37cabbc6ee3e2ece5f965cd83b06495dac Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 21 Mar 2016 14:15:42 -0400 Subject: [PATCH 12/18] Exec and CommandExchange refactoring --- .../src/main/scala/sbt/BasicCommands.scala | 18 +++---- .../scala/sbt/internal/CommandChannel.scala | 17 +++++-- .../scala/sbt/internal/CommandExchange.scala | 47 ++++++++++++++----- .../scala/sbt/internal/ConsoleChannel.scala | 28 +++++------ .../scala/sbt/internal/NetworkChannel.scala | 16 ++----- 5 files changed, 74 insertions(+), 52 deletions(-) diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 506e28509..12e0ef49b 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -6,7 +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.{ CommandRequest, CommandSource, CommandStatus } +import sbt.internal.{ Exec, CommandSource, CommandStatus } import DefaultParsers._ import Function.tupled import Command.applyEffect @@ -193,17 +193,13 @@ object BasicCommands { } } - def server = Command.command(Server, Help.more(Server, ServerDetailed)) { s => + def server = Command.command(Server, Help.more(Server, ServerDetailed)) { s0 => val exchange = State.exchange - exchange.channels foreach { x => - x.runOrResume(CommandStatus(s, true)) - x.setStatus(CommandStatus(s, true), None) - } - val CommandRequest(source, line) = exchange.blockUntilNextCommand - val newState = s.copy(onFailure = Some(Server), remainingCommands = line +: Server +: s.remainingCommands).setInteractive(true) - exchange.channels foreach { x => - x.setStatus(CommandStatus(newState, false), Some(source)) - } + 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 } diff --git a/main/command/src/main/scala/sbt/internal/CommandChannel.scala b/main/command/src/main/scala/sbt/internal/CommandChannel.scala index c6bf79049..f01d9ffbd 100644 --- a/main/command/src/main/scala/sbt/internal/CommandChannel.scala +++ b/main/command/src/main/scala/sbt/internal/CommandChannel.scala @@ -1,19 +1,26 @@ 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(exchange: CommandExchange) { - /** start listening for a command request. */ - def runOrResume(status: CommandStatus): Unit - def setStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit +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 CommandRequest(source: CommandSource, commandLine: String) +case class Exec(source: CommandSource, commandLine: String) sealed trait CommandSource object CommandSource { diff --git a/main/command/src/main/scala/sbt/internal/CommandExchange.scala b/main/command/src/main/scala/sbt/internal/CommandExchange.scala index 11ecbed25..6bce31b0f 100644 --- a/main/command/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/command/src/main/scala/sbt/internal/CommandExchange.scala @@ -1,8 +1,9 @@ package sbt package internal -import java.util.concurrent.ConcurrentLinkedQueue 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), @@ -11,17 +12,41 @@ import scala.annotation.tailrec * this exchange, which could serve command request from either of the channel. */ private[sbt] final class CommandExchange { - private val commandQueue: ConcurrentLinkedQueue[CommandRequest] = new ConcurrentLinkedQueue() - lazy val channels = Seq(new ConsoleChannel(this), new NetworkChannel(this)) + 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) - def append(request: CommandRequest): Boolean = - commandQueue.add(request) + subscribe(new ConsoleChannel()) + subscribe(new NetworkChannel()) - @tailrec def blockUntilNextCommand: CommandRequest = - Option(commandQueue.poll) match { - case Some(x) => x - case _ => - Thread.sleep(50) - blockUntilNextCommand + // 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) } } diff --git a/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala b/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala index f845c02ef..8827c866c 100644 --- a/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala +++ b/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala @@ -5,7 +5,7 @@ import sbt.internal.util._ import BasicKeys._ import java.io.File -private[sbt] final class ConsoleChannel(exchange: CommandExchange) extends CommandChannel(exchange) { +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 @@ -19,8 +19,8 @@ private[sbt] final class ConsoleChannel(exchange: CommandExchange) extends Comma try { val line = reader.readLine(prompt) line match { - case Some(cmd) => exchange.append(CommandRequest(CommandSource.Human, cmd)) - case None => exchange.append(CommandRequest(CommandSource.Human, "exit")) + case Some(cmd) => append(Exec(CommandSource.Human, cmd)) + case None => append(Exec(CommandSource.Human, "exit")) } } catch { case e: InterruptedException => @@ -28,18 +28,18 @@ private[sbt] final class ConsoleChannel(exchange: CommandExchange) extends Comma } } - def runOrResume(status: CommandStatus): Unit = - askUserThread match { - case Some(x) if x.isAlive => // - case _ => - val x = makeAskUserThread(status) - x.start - askUserThread = Some(x) - } + def run(s: State): State = s - def setStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit = - if (status.canEnter) () - else { + def publishStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit = + if (status.canEnter) { + askUserThread match { + case Some(x) if x.isAlive => // + case _ => + val x = makeAskUserThread(status) + x.start + askUserThread = Some(x) + } + } else { shutdown() lastSource match { case Some(src) if src != CommandSource.Human => diff --git a/main/command/src/main/scala/sbt/internal/NetworkChannel.scala b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala index 701a4ecdb..f4a2abc02 100644 --- a/main/command/src/main/scala/sbt/internal/NetworkChannel.scala +++ b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala @@ -4,19 +4,18 @@ package internal import sbt.internal.server._ import BasicKeys._ -private[sbt] final class NetworkChannel(exchange: CommandExchange) extends CommandChannel(exchange) { +private[sbt] final class NetworkChannel extends CommandChannel { private var server: Option[ServerInstance] = None - def runOrResume(status: CommandStatus): Unit = + def run(s: State): State = { - val s = status.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) => exchange.append(CommandRequest(CommandSource.Network, cmd)) + case Execution(cmd) => append(Exec(CommandSource.Network, cmd)) } } server match { @@ -24,6 +23,7 @@ private[sbt] final class NetworkChannel(exchange: CommandExchange) extends Comma case _ => server = Some(Server.start("127.0.0.1", port, onCommand)) } + s } def shutdown(): Unit = @@ -33,13 +33,7 @@ private[sbt] final class NetworkChannel(exchange: CommandExchange) extends Comma server = None } - // network doesn't pause or resume - def pause(): Unit = () - - // network doesn't pause or resume - def resume(status: CommandStatus): Unit = () - - def setStatus(cmdStatus: CommandStatus, lastSource: Option[CommandSource]): Unit = { + def publishStatus(cmdStatus: CommandStatus, lastSource: Option[CommandSource]): Unit = { server.foreach(server => server.publish( if (cmdStatus.canEnter) StatusEvent(Ready) From 5e0b087daa4d6f9e9f68e2bdf9adab438936e37a Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 21 Mar 2016 14:31:07 -0400 Subject: [PATCH 13/18] Use logger --- .../src/main/scala/sbt/internal/ConsoleChannel.scala | 2 +- .../src/main/scala/sbt/internal/NetworkChannel.scala | 2 +- .../src/main/scala/sbt/internal/server/Server.scala | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala b/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala index 8827c866c..8bf214365 100644 --- a/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala +++ b/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala @@ -45,7 +45,7 @@ private[sbt] final class ConsoleChannel extends CommandChannel { case Some(src) if src != CommandSource.Human => val s = status.state s.remainingCommands.headOption map { - println(_) + System.out.println(_) } case _ => // } diff --git a/main/command/src/main/scala/sbt/internal/NetworkChannel.scala b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala index f4a2abc02..597f6e602 100644 --- a/main/command/src/main/scala/sbt/internal/NetworkChannel.scala +++ b/main/command/src/main/scala/sbt/internal/NetworkChannel.scala @@ -21,7 +21,7 @@ private[sbt] final class NetworkChannel extends CommandChannel { server match { case Some(x) => // do nothing case _ => - server = Some(Server.start("127.0.0.1", port, onCommand)) + server = Some(Server.start("127.0.0.1", port, onCommand, s.log)) } s } 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 e8c846de7..72f14d2cc 100644 --- a/main/command/src/main/scala/sbt/internal/server/Server.scala +++ b/main/command/src/main/scala/sbt/internal/server/Server.scala @@ -8,6 +8,7 @@ package server import java.net.{ SocketTimeoutException, InetAddress, ServerSocket } import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean +import sbt.util.Logger private[sbt] sealed trait ServerInstance { def shutdown(): Unit @@ -15,7 +16,7 @@ private[sbt] sealed trait ServerInstance { } private[sbt] object Server { - def start(host: String, port: Int, onIncommingCommand: Command => Unit): ServerInstance = + def start(host: String, port: Int, onIncommingCommand: Command => Unit, log: Logger): ServerInstance = new ServerInstance { val lock = new AnyRef {} @@ -28,11 +29,11 @@ private[sbt] object Server { val serverSocket = new ServerSocket(port, 50, InetAddress.getByName(host)) serverSocket.setSoTimeout(5000) - println(s"SBT socket server started at $host:$port") + log.info(s"sbt server started at $host:$port") while (running.get()) { try { val socket = serverSocket.accept() - println(s"New client connected from: ${socket.getPort}") + log.info(s"new client connected from: ${socket.getPort}") val connection = new ClientConnection(socket) { override def onCommand(command: Command): Unit = { @@ -63,7 +64,7 @@ private[sbt] object Server { } override def shutdown(): Unit = { - println("Shutting down server") + log.info("shutting down server") running.set(false) } } From 75deb4e55d1dbc8050df79e26ea80c0f9f97fe28 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 21 Mar 2016 18:37:43 -0400 Subject: [PATCH 14/18] Change "command_exec" to just "exec" --- .../src/main/scala/sbt/internal/server/Serialization.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/command/src/main/scala/sbt/internal/server/Serialization.scala b/main/command/src/main/scala/sbt/internal/server/Serialization.scala index e399cd450..f7da4b0f9 100644 --- a/main/command/src/main/scala/sbt/internal/server/Serialization.scala +++ b/main/command/src/main/scala/sbt/internal/server/Serialization.scala @@ -56,7 +56,7 @@ object Serialization { implicit val formats = DefaultFormats (json \ "type").toOption match { - case Some(JString("command_exec")) => + case Some(JString("exec")) => (json \ "command_line").toOption match { case Some(JString(cmd)) => Right(Execution(cmd)) case _ => Left("Missing or invalid command_line field") From fe1a24cf7c201d5506a0198cadb5b27c0b9ed5e9 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 22 Jul 2016 16:25:26 -0400 Subject: [PATCH 15/18] Adjust to 1.0.x --- .../src/main/scala/sbt/internal/CommandChannel.scala | 0 .../src/main/scala/sbt/internal/CommandExchange.scala | 0 .../src/main/scala/sbt/internal/ConsoleChannel.scala | 0 .../src/main/scala/sbt/internal/NetworkChannel.scala | 0 .../src/main/scala/sbt/internal/server/ClientConnection.scala | 0 .../src/main/scala/sbt/internal/server/Serialization.scala | 0 .../src/main/scala/sbt/internal/server/Server.scala | 0 .../src/main/scala/sbt/internal/server/protocol.scala | 0 main/src/main/scala/sbt/Project.scala | 2 +- project/Dependencies.scala | 3 +++ 10 files changed, 4 insertions(+), 1 deletion(-) rename {main/command => main-command}/src/main/scala/sbt/internal/CommandChannel.scala (100%) rename {main/command => main-command}/src/main/scala/sbt/internal/CommandExchange.scala (100%) rename {main/command => main-command}/src/main/scala/sbt/internal/ConsoleChannel.scala (100%) rename {main/command => main-command}/src/main/scala/sbt/internal/NetworkChannel.scala (100%) rename {main/command => main-command}/src/main/scala/sbt/internal/server/ClientConnection.scala (100%) rename {main/command => main-command}/src/main/scala/sbt/internal/server/Serialization.scala (100%) rename {main/command => main-command}/src/main/scala/sbt/internal/server/Server.scala (100%) rename {main/command => main-command}/src/main/scala/sbt/internal/server/protocol.scala (100%) diff --git a/main/command/src/main/scala/sbt/internal/CommandChannel.scala b/main-command/src/main/scala/sbt/internal/CommandChannel.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/CommandChannel.scala rename to main-command/src/main/scala/sbt/internal/CommandChannel.scala diff --git a/main/command/src/main/scala/sbt/internal/CommandExchange.scala b/main-command/src/main/scala/sbt/internal/CommandExchange.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/CommandExchange.scala rename to main-command/src/main/scala/sbt/internal/CommandExchange.scala diff --git a/main/command/src/main/scala/sbt/internal/ConsoleChannel.scala b/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/ConsoleChannel.scala rename to main-command/src/main/scala/sbt/internal/ConsoleChannel.scala diff --git a/main/command/src/main/scala/sbt/internal/NetworkChannel.scala b/main-command/src/main/scala/sbt/internal/NetworkChannel.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/NetworkChannel.scala rename to main-command/src/main/scala/sbt/internal/NetworkChannel.scala diff --git a/main/command/src/main/scala/sbt/internal/server/ClientConnection.scala b/main-command/src/main/scala/sbt/internal/server/ClientConnection.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/server/ClientConnection.scala rename to main-command/src/main/scala/sbt/internal/server/ClientConnection.scala diff --git a/main/command/src/main/scala/sbt/internal/server/Serialization.scala b/main-command/src/main/scala/sbt/internal/server/Serialization.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/server/Serialization.scala rename to main-command/src/main/scala/sbt/internal/server/Serialization.scala diff --git a/main/command/src/main/scala/sbt/internal/server/Server.scala b/main-command/src/main/scala/sbt/internal/server/Server.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/server/Server.scala rename to main-command/src/main/scala/sbt/internal/server/Server.scala diff --git a/main/command/src/main/scala/sbt/internal/server/protocol.scala b/main-command/src/main/scala/sbt/internal/server/protocol.scala similarity index 100% rename from main/command/src/main/scala/sbt/internal/server/protocol.scala rename to main-command/src/main/scala/sbt/internal/server/protocol.scala diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 7ddcc6c82..497d5cf70 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -6,7 +6,7 @@ package sbt import java.io.File import java.net.URI import java.util.Locale -import Project.{ Initialize => _, Setting => _, _ } +import Project._ import Keys.{ appConfiguration, stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, serverPort, thisProject, thisProjectRef, watch } import Scope.{ GlobalScope, ThisScope } import Def.{ Flattened, Initialize, ScopedKey, Setting } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 94a89910d..16ec839e9 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -38,6 +38,9 @@ object Dependencies { lazy val compilerApiInfo = "org.scala-sbt" %% "zinc-apiinfo" % zincVersion lazy val compilerIvyIntegration = "org.scala-sbt" %% "zinc-ivy-integration" % zincVersion + lazy val json4s = "org.json4s" %% "json4s" % "3.2.10" + lazy val json4sNative = "org.json4s" %% "json4s-native" % "3.2.10" + lazy val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.11.4" lazy val specs2 = "org.specs2" %% "specs2" % "2.3.11" lazy val junit = "junit" % "junit" % "4.11" From a94107e673deba700032661be4868539ed1efef9 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 7 Aug 2016 23:49:36 -0400 Subject: [PATCH 16/18] Handle closed socket --- .../scala/sbt/internal/server/Server.scala | 20 ++++++++++++++----- reset.sh | 3 +++ 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100755 reset.sh 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 72f14d2cc..f102cb736 100644 --- a/main-command/src/main/scala/sbt/internal/server/Server.scala +++ b/main-command/src/main/scala/sbt/internal/server/Server.scala @@ -5,10 +5,10 @@ package sbt package internal package server -import java.net.{ SocketTimeoutException, InetAddress, ServerSocket } -import java.util.concurrent.ConcurrentLinkedQueue +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 @@ -20,7 +20,7 @@ private[sbt] object Server { new ServerInstance { val lock = new AnyRef {} - var clients = Vector[ClientConnection]() + val clients: mutable.ListBuffer[ClientConnection] = mutable.ListBuffer.empty val running = new AtomicBoolean(true) val serverThread = new Thread("sbt-socket-server") { @@ -42,7 +42,7 @@ private[sbt] object Server { } lock.synchronized { - clients = clients :+ connection + clients += connection } } catch { @@ -59,7 +59,17 @@ private[sbt] object Server { // TODO do not do this on the calling thread val bytes = Serialization.serialize(event) lock.synchronized { - clients.foreach(_.publish(bytes)) + 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 } } diff --git a/reset.sh b/reset.sh new file mode 100755 index 000000000..ec3465425 --- /dev/null +++ b/reset.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +rm -rf ~/.sbt/boot/scala-2.11.8/org.scala-sbt/sbt/1.0.0-SNAPSHOT/ From e7456b5653d58926dab1c6863f8006a11d04ddb0 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 13 Sep 2016 01:15:57 -0400 Subject: [PATCH 17/18] Use the new FullReader --- build.sbt | 2 +- .../scala/sbt/internal/ConsoleChannel.scala | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/build.sbt b/build.sbt index 20f0b16fa..4f6c079c1 100644 --- a/build.sbt +++ b/build.sbt @@ -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, diff --git a/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala b/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala index 8bf214365..5b0c99497 100644 --- a/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala +++ b/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala @@ -14,17 +14,15 @@ private[sbt] final class ConsoleChannel extends CommandChannel { case Some(pf) => pf(s) case None => "> " } - val reader = new FullReader(history, s.combinedParser) + val reader = new FullReader(history, s.combinedParser, JLine.HandleCONT, true) override def run(): Unit = { - try { - val line = reader.readLine(prompt) - line match { - case Some(cmd) => append(Exec(CommandSource.Human, cmd)) - case None => append(Exec(CommandSource.Human, "exit")) - } - } catch { - case e: InterruptedException => + // 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 } } @@ -33,21 +31,21 @@ private[sbt] final class ConsoleChannel extends CommandChannel { def publishStatus(status: CommandStatus, lastSource: Option[CommandSource]): Unit = if (status.canEnter) { askUserThread match { - case Some(x) if x.isAlive => // + case Some(x) => // case _ => val x = makeAskUserThread(status) - x.start askUserThread = Some(x) + x.start } } else { - shutdown() lastSource match { case Some(src) if src != CommandSource.Human => - val s = status.state - s.remainingCommands.headOption map { - System.out.println(_) + askUserThread match { + case Some(x) => + shutdown() + case _ => } - case _ => // + case _ => } } From 18233ace05186470a2d137797befac21c93997b8 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 27 Oct 2016 09:27:41 -0400 Subject: [PATCH 18/18] Using sbt-datatype + sjson-new for JSON serialization --- build.sbt | 4 +- .../sbt/internal/server/CommandMessage.scala | 39 +++++++ .../sbt/internal/server/EventMessage.scala | 59 ++++++++++ .../server/codec/CommandMessageFormats.scala | 29 +++++ .../server/codec/EventMessageFormats.scala | 39 +++++++ main-command/src/main/datatype/server.json | 73 ++++++++++++ .../sbt/internal/server/Serialization.scala | 104 +++++++++--------- project/Dependencies.scala | 4 +- project/datatype.sbt | 1 + 9 files changed, 297 insertions(+), 55 deletions(-) create mode 100644 main-command/src/main/datatype-scala/sbt/internal/server/CommandMessage.scala create mode 100644 main-command/src/main/datatype-scala/sbt/internal/server/EventMessage.scala create mode 100644 main-command/src/main/datatype-scala/sbt/internal/server/codec/CommandMessageFormats.scala create mode 100644 main-command/src/main/datatype-scala/sbt/internal/server/codec/EventMessageFormats.scala create mode 100644 main-command/src/main/datatype/server.json create mode 100644 project/datatype.sbt diff --git a/build.sbt b/build.sbt index 4f6c079c1..ffd4ae2f7 100644 --- a/build.sbt +++ b/build.sbt @@ -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, json4s, json4sNative) // to transitively get json4s) + 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 diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/CommandMessage.scala b/main-command/src/main/datatype-scala/sbt/internal/server/CommandMessage.scala new file mode 100644 index 000000000..889f8fe7a --- /dev/null +++ b/main-command/src/main/datatype-scala/sbt/internal/server/CommandMessage.scala @@ -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) +} diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/EventMessage.scala b/main-command/src/main/datatype-scala/sbt/internal/server/EventMessage.scala new file mode 100644 index 000000000..55946b0e7 --- /dev/null +++ b/main-command/src/main/datatype-scala/sbt/internal/server/EventMessage.scala @@ -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) +} diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/codec/CommandMessageFormats.scala b/main-command/src/main/datatype-scala/sbt/internal/server/codec/CommandMessageFormats.scala new file mode 100644 index 000000000..17130c90e --- /dev/null +++ b/main-command/src/main/datatype-scala/sbt/internal/server/codec/CommandMessageFormats.scala @@ -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() + } +} +} diff --git a/main-command/src/main/datatype-scala/sbt/internal/server/codec/EventMessageFormats.scala b/main-command/src/main/datatype-scala/sbt/internal/server/codec/EventMessageFormats.scala new file mode 100644 index 000000000..4ce95d9ef --- /dev/null +++ b/main-command/src/main/datatype-scala/sbt/internal/server/codec/EventMessageFormats.scala @@ -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() + } +} +} diff --git a/main-command/src/main/datatype/server.json b/main-command/src/main/datatype/server.json new file mode 100644 index 000000000..3dcc4af17 --- /dev/null +++ b/main-command/src/main/datatype/server.json @@ -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" + } + ] + } + ] +} diff --git a/main-command/src/main/scala/sbt/internal/server/Serialization.scala b/main-command/src/main/scala/sbt/internal/server/Serialization.scala index f7da4b0f9..99142383f 100644 --- a/main-command/src/main/scala/sbt/internal/server/Serialization.scala +++ b/main-command/src/main/scala/sbt/internal/server/Serialization.scala @@ -5,66 +5,68 @@ package sbt package internal package server -import org.json4s.JsonAST.{ JArray, JString } -import org.json4s._ -import org.json4s.JsonDSL._ -import org.json4s.native.JsonMethods._ -import org.json4s.ParserUtil.ParseException +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] = { - compact(render(toJson(event))).getBytes("UTF-8") - } - - def toJson(event: Event): JObject = event match { - case LogEvent(level, message) => - JObject( - "type" -> JString("log_event"), - "level" -> JString(level), - "message" -> JString(message) - ) - - case StatusEvent(Ready) => - JObject( - "type" -> JString("status_event"), - "status" -> JString("ready"), - "command_queue" -> JArray(List.empty) - ) - - case StatusEvent(Processing(command, commandQueue)) => - JObject( - "type" -> JString("status_event"), - "status" -> JString("processing"), - "command_queue" -> JArray(commandQueue.map(JString).toList) - ) - - case ExecutionEvent(command, status) => - JObject( - "type" -> JString("execution_event"), - "command" -> JString(command), - "success" -> JBool(status) - ) - } + 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] = - try { - val json = parse(new String(bytes.toArray, "UTF-8")) - implicit val formats = DefaultFormats - - (json \ "type").toOption match { - case Some(JString("exec")) => - (json \ "command_line").toOption match { - case Some(JString(cmd)) => Right(Execution(cmd)) - case _ => Left("Missing or invalid command_line field") + { + 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 Some(cmd) => Left(s"Unknown command type $cmd") - case None => Left("Invalid command, missing type field") + case Failure(e) => + Left(s"Parse error: ${e.getMessage}") } - } catch { - case e: ParseException => Left(s"Parse error: ${e.getMessage}") } } + +object ServerCodec extends ServerCodec +trait ServerCodec extends codec.EventMessageFormats with codec.CommandMessageFormats with BasicJsonProtocol diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 16ec839e9..5c0e9e9ef 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -37,9 +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 json4s = "org.json4s" %% "json4s" % "3.2.10" - lazy val json4sNative = "org.json4s" %% "json4s-native" % "3.2.10" + 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" diff --git a/project/datatype.sbt b/project/datatype.sbt new file mode 100644 index 000000000..3fd610568 --- /dev/null +++ b/project/datatype.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scala-sbt" % "sbt-datatype" % "0.2.6")