diff --git a/Client-server-discovery-lifecycle.md b/Client-server-discovery-lifecycle.md index ec019f5..fb54aea 100644 --- a/Client-server-discovery-lifecycle.md +++ b/Client-server-discovery-lifecycle.md @@ -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. \ No newline at end of file +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[, 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 + + socket.getLocalPort + } + +``` \ No newline at end of file