Updated Client server discovery lifecycle (markdown)

Mark Harrah 2013-10-16 06:41:34 -07:00
parent be27184c5b
commit 187398e978
1 changed files with 115 additions and 1 deletions

@ -112,4 +112,118 @@ The interface has two methods. One responsible for starting the Server, and ano
Port bindings come in java.net.URI, which includes an address, a protocol and a port. The server will actually also write down the PID (process ID) of the server (assuming it can obtain the pid). This enables clients to "kill rogue sbt server" directly.
Clients should be able to be notified of all open ServerPort bindings created by a server.
Clients should be able to be notified of all open ServerPort bindings created by a server.
## Sample code
The following accomplishes:
```text
client: I want the application defined by boot.properties to run in cwd and use version X of protocol N
server-service: the application is running and listening on port P
```
```scala
client:
val bootProperties: File = ...
val connectionConfiguration = Map("x" -> "y")
val dir: File = ...
val (output: String, err: String, exit: Int) =
(stdout, stderr, exitCode) of { "java -jar serverProvider.jar bootProperties connectionConfiguration(as key=value args)".in(dir).!!! }
if(exit == 0)
println("Server running on port: " + output.toInt)
else
println("Server connection failed: " + err)
server-provider:
def main(args: Array[String])
{
val bootProperties: File = new File(args(0))
val cwd: File = (new File(".")).getAbsoluteFile
val connectionConfiguration: Map[String, String] =
args.drop(1).map(_.split("=", 2) match { case Array(key,value) => (key,value) }).toMap
val serverFile = serverFileLocation(bootProperties, cwd)
val connection = withLockFile( serverFile ) {
readServerInfo( serverFile ) match {
case None => startAndConnect(boot.properties, cwd, connectionConfiguration)
case Some(info) => connect(info, connectionConfiguration)
}
}
System.out.println(connection.port)
System.exit(0)
}
def connect(info: ServerInfo, connectionConfiguration: Map[String,String]): ConnectionInfo =
requestConnection(info, connectionConfiguration) match {
case Left(NoServerPresent) => startAndConnect(bootProperties, cwd, connectionConfiguration)
case Left(e) => fail(e.toString)
case Right(newConnection) => newConnection
}
def requestConnection(info: ServerInfo, connectionConfiguration: Map[String, String]): Either[<error>, ConnectionInfo] =
try {
openSocket(info.port) match {
case Left(err) => Left(NoServerPresent)
case Right(socket) =>
sendConfiguration(socket, connectionConfiguration)
ConnectionInfo(receivePort(socket))
}
} catch {
case e: Exception => Left(e)
}
def fail(msg: String) {
System.err.println(msg)
System.exit(1)
}
def startAndConnect(bootProperties: File, cwd: File, connectionConfiguration: Map[String,String]): ConnectionInfo =
connect(newServer(bootProperties, cwd), connectionConfiguration)
def newServer(bootProperties: File, cwd: File, connectionConfiguration: Map[String,String]): ServerInfo =
{
val process = "java -jar sbt-launch.jar @bootProperties".in(cwd).run
// this line is not quite right- reading a line from std output would be part of configuring the above line
val readPort = fromStdOut(process)
val info = ServerInfo(readPort, "faked")
if(code != 0) fail("Couldn't start server")
writeServerInfo(serverFile, info)
info
}
def sendConfiguration(socket: Socket, connectionConfiguration: Map[String,String]): Unit
def receivePort(socket: Socket): Int
def withLockFile[T](file: File)(run: => T): T
def serverFileLocation(bootProperties: File, cwd: File): File
def writeServerInfo(file: File, info: ServerInfo): Unit
def readServerInfo(file: File): Option[ServerInfo]
final case class ServerInfo(port: Int, pid: String)
final case class ConnectionInfo(port: Int)
server:
val socket: ServerSocket = localSocket
System.out.println(socket.getLocalPort)
...
// probably part of an sbt command that is run initially
while(true) {
val config: Map[String, String] = receiveConfiguration(socket)
val port = newClient(config)
writePort(socket, port)
}
def receiveConfiguration(socket: ServerSocket): Map[String,String] =
{
// I forget the details- not sure if you have to recreate a ServerSocket
// for every accept or if you can accept multiple times on the same ServerSocket
val s = socket.accept()
readConfiguration(s)
}
def localSocket: ServerSocket = new ServerSocket(0, 0, InetAddress.getByName(null))
def readConfiguration(socket: Socket): Map[String,String]
def writePort(socket: Socket, port: Int): Unit
def newClient(config: Map[String, String]): Int = {
val socket: ServerSocket = localSocket
<start a thread listening on socket, connect it to command processing, etc...>
socket.getLocalPort
}
```