Merge pull request #1066 from jsuereth/wip/launcher-improvements

Launcher can now load servers in addition to regular applications.
This commit is contained in:
Josh Suereth 2014-01-06 13:57:28 -08:00
commit 4207978f89
26 changed files with 1206 additions and 451 deletions

View File

@ -1,6 +1,25 @@
package xsbti;
/**
* The main entry interface for launching applications. Classes which implement this interface
* can be launched via the sbt launcher.
*
* In addition, classes can be adapted into this interface by the launcher if they have a static method
* matching one of these signatures:
*
* - public static void main(String[] args)
* - public static int main(String[] args)
* - public static xsbti.Exit main(String[] args)
*
*/
public interface AppMain
{
/** Run the application and return the result.
*
* @param configuration The configuration used to run the application. Includes arguments and access to launcher features.
* @return
* The result of running this app.
* Note: the result can be things like "Please reboot this application".
*/
public MainResult run(AppConfiguration configuration);
}

View File

@ -3,9 +3,10 @@ package xsbti;
import java.io.File;
/**
* This represents an interface that can generate applications.
* This represents an interface that can generate applications or servers.
*
* An application is somethign which will run and return an exit value.
* This provider grants access to launcher related features associated with
* the id.
*/
public interface AppProvider
{
@ -33,6 +34,8 @@ public interface AppProvider
* It is NOT guaranteed that newMain().getClass() == mainClass().
* The sbt launcher can wrap generic static main methods. In this case, there will be a wrapper class,
* and you must use the `entryPoint` method.
* @throws IncompatibleClassChangeError if the configuration used for this Application does not
* represent a launched application.
*/
public AppMain newMain();

View File

@ -0,0 +1,36 @@
package xsbti;
/** A running server.
*
* A class implementing this must:
*
* 1. Expose an HTTP port that clients can connect to, returned via the uri method.
* 2. Accept HTTP HEAD requests against the returned URI. These are used as "ping" messages to ensure
* a server is still alive, when new clients connect.
* 3. Create a new thread to execute its service
* 4. Block the calling thread until the server is shutdown via awaitTermination()
*/
public interface Server {
/**
* @return
* A URI denoting the Port which clients can connect to.
*
* Note: we use a URI so that the server can bind to different IP addresses (even a public one) if desired.
* Note: To verify that a server is "up", the sbt launcher will attempt to connect to
* this URI's address and port with a socket. If the connection is accepted, the server is assumed to
* be working.
*/
public java.net.URI uri();
/**
* This should block the calling thread until the server is shutdown.
*
* @return
* The result that should occur from the server.
* Can be:
* - xsbti.Exit: Shutdown this launch
* - xsbti.Reboot: Restart the server
*
*
*/
public xsbti.MainResult awaitTermination();
}

View File

@ -0,0 +1,17 @@
package xsbti;
/** The main entry point for a launched service. This allows applciations
* to instantiate server instances.
*/
public interface ServerMain {
/**
* This method should launch one or more thread(s) which run the service. After the service has
* been started, it should return the port/URI it is listening for connections on.
*
* @param configuration
* The configuration used to launch this service.
* @return
* A running server.
*/
public Server start(AppConfiguration configuration);
}

View File

@ -5,36 +5,48 @@
import java.io.File
// The entry point to the launcher
object Boot
{
def main(args: Array[String])
{
args match {
case Array("--version") =>
println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion)
case _ =>
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
CheckProxy()
run(args)
}
val config = parseArgs(args)
// If we havne't exited, we set up some hooks and launch
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
CheckProxy()
run(config)
}
def parseArgs(args: Array[String]): LauncherArguments = {
@annotation.tailrec
def parse(args: List[String], isLocate: Boolean, remaining: List[String]): LauncherArguments =
args match {
case "--version" :: rest =>
println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion)
exit(1)
case "--locate" :: rest => parse(rest, true, remaining)
case next :: rest => parse(rest, isLocate, next :: remaining)
case Nil => new LauncherArguments(remaining.reverse, isLocate)
}
parse(args.toList, false, Nil)
}
// this arrangement is because Scala does not always properly optimize away
// the tail recursion in a catch statement
final def run(args: Array[String]): Unit = runImpl(args) match {
final def run(args: LauncherArguments): Unit = runImpl(args) match {
case Some(newArgs) => run(newArgs)
case None => ()
}
private def runImpl(args: Array[String]): Option[Array[String]] =
private def runImpl(args: LauncherArguments): Option[LauncherArguments] =
try
Launch(args.toList) map exit
Launch(args) map exit
catch
{
case b: BootException => errorAndExit(b.toString)
case r: xsbti.RetrieveException => errorAndExit("Error: " + r.getMessage)
case r: xsbti.FullReload => Some(r.arguments)
case r: xsbti.FullReload => Some(new LauncherArguments(r.arguments.toList, false))
case e: Throwable =>
e.printStackTrace
errorAndExit(Pre.prefixError(e.toString))

View File

@ -10,21 +10,34 @@ import java.util.regex.Pattern
import scala.collection.immutable.List
import annotation.tailrec
object ConfigurationStorageState extends Enumeration {
val PropertiesFile = value("properties-file")
val SerializedFile = value("serialized-file")
}
object Configuration
{
import ConfigurationStorageState._
final val SysPropPrefix = "-D"
def parse(file: URL, baseDirectory: File) = Using( new InputStreamReader(file.openStream, "utf8") )( (new ConfigurationParser).apply )
@tailrec def find(args: List[String], baseDirectory: File): (URL, List[String]) =
/**
* Finds the configuration location.
*
* Note: Configuration may be previously serialized by a launcher.
*/
@tailrec def find(args: List[String], baseDirectory: File): (URL, List[String], ConfigurationStorageState.Value) =
args match
{
case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail)
case head :: tail if head.startsWith("@load:") => (directConfiguration(head.substring(6), baseDirectory), tail, SerializedFile)
case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail, PropertiesFile)
case head :: tail if head.startsWith(SysPropPrefix) =>
setProperty(head stripPrefix SysPropPrefix)
find(tail, baseDirectory)
case _ =>
val propertyConfigured = System.getProperty("sbt.boot.properties")
val url = if(propertyConfigured == null) configurationOnClasspath else configurationFromFile(propertyConfigured, baseDirectory)
(url , args)
(url, args, PropertiesFile)
}
def setProperty(head: String)
{
@ -108,7 +121,7 @@ object Configuration
// We have to hard code them here in order to use them to determine the location of sbt.boot.properties itself
def guessSbtVersion: Option[String] =
{
val props = ResolveValues.readProperties(new File(DefaultBuildProperties))
val props = Pre.readProperties(new File(DefaultBuildProperties))
Option(props.getProperty(SbtVersionProperty))
}

View File

@ -78,11 +78,14 @@ class ConfigurationParser
val (logging, m5) = processSection(m4, "log", getLogging)
val (properties, m6) = processSection(m5, "app-properties", getAppProperties)
val ((ivyHome, checksums, isOverrideRepos, rConfigFile), m7) = processSection(m6, "ivy", getIvy)
check(m7, "section")
val (serverOptions, m8) = processSection(m7, "server", getServer)
check(m8, "section")
val classifiers = Classifiers(scalaClassifiers, appClassifiers)
val repositories = rConfigFile map readRepositoriesConfig getOrElse defaultRepositories
val ivyOptions = IvyOptions(ivyHome, classifiers, repositories, checksums, isOverrideRepos)
new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties)
// TODO - Read server properties...
new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties, serverOptions)
}
def getScala(m: LabelMap) =
{
@ -178,6 +181,16 @@ class ConfigurationParser
val app = new Application(org, name, rev, main, components, LaunchCrossVersion(crossVersioned), classpathExtra)
(app, classifiers)
}
def getServer(m: LabelMap): (Option[ServerConfiguration]) =
{
val (lock, m1) = optfile(m, "lock")
// TODO - JVM args
val (args, m2) = optfile(m1, "jvmargs")
val (props, m3) = optfile(m2, "jvmprops")
lock map { file =>
ServerConfiguration(file, args, props)
}
}
def getRepositories(m: LabelMap): List[Repository.Repository] =
{
import Repository.{Ivy, Maven, Predefined}

View File

@ -33,17 +33,11 @@ object Initialize
def fill(file: File, spec: List[AppProperty]): Unit = process(file, spec, selectFill)
def process(file: File, appProperties: List[AppProperty], select: AppProperty => Option[PropertyInit])
{
val properties = new Properties
if(file.exists)
Using(new FileInputStream(file))( properties.load )
val properties = readProperties(file)
val uninitialized =
for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null) yield
initialize(properties, property.name, init)
if(!uninitialized.isEmpty)
{
file.getParentFile.mkdirs()
Using(new FileOutputStream(file))( out => properties.store(out, "") )
}
if(!uninitialized.isEmpty) writeProperties(properties, file, "")
}
def initialize(properties: Properties, name: String, init: PropertyInit)
{

View File

@ -6,7 +6,7 @@ package xsbt.boot
import Pre._
import scala.collection.immutable.List
class Enumeration
class Enumeration extends Serializable
{
def elements: List[Value] = members
private lazy val members: List[Value] =
@ -25,6 +25,6 @@ class Enumeration
}
def value(s: String) = new Value(s, 0)
def value(s: String, i: Int) = new Value(s, i)
final class Value(override val toString: String, val id: Int)
final class Value(override val toString: String, val id: Int) extends Serializable
def toValue(s: String): Value = elements.find(_.toString == s).getOrElse(error("Expected one of " + elements.mkString(",") + " (got: " + s + ")"))
}

View File

@ -6,20 +6,64 @@ package xsbt.boot
import Pre._
import BootConfiguration.{CompilerModuleName, JAnsiVersion, LibraryModuleName}
import java.io.File
import java.net.{URL, URLClassLoader}
import java.net.{URL, URLClassLoader, URI}
import java.util.concurrent.Callable
import scala.collection.immutable.List
import scala.annotation.tailrec
import ConfigurationStorageState._
class LauncherArguments(val args: List[String], val isLocate: Boolean)
object Launch
{
def apply(arguments: List[String]): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments )
def apply(arguments: LauncherArguments): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments )
def apply(currentDirectory: File, arguments: List[String]): Option[Int] = {
val (configLocation, newArguments) = Configuration.find(arguments, currentDirectory)
val config = parseAndInitializeConfig(configLocation, currentDirectory)
launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArguments))
def apply(currentDirectory: File, arguments: LauncherArguments): Option[Int] = {
val (configLocation, newArgs2, state) = Configuration.find(arguments.args, currentDirectory)
val config = state match {
case SerializedFile => LaunchConfiguration.restore(configLocation)
case PropertiesFile => parseAndInitializeConfig(configLocation, currentDirectory)
}
if(arguments.isLocate) {
if(!newArgs2.isEmpty) {
// TODO - Print the arguments without exploding proguard size.
System.err.println("Warning: --locate option ignores arguments.")
}
locate(currentDirectory, config)
} else {
// First check to see if there are java system properties we need to set. Then launch the application.
updateProperties(config)
launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArgs2))
}
}
/** Locate a server, print where it is, and exit. */
def locate(currentDirectory: File, config: LaunchConfiguration): Option[Int] = {
config.serverConfig match {
case Some(_) =>
val uri = ServerLocator.locate(currentDirectory, config)
System.out.println(uri.toASCIIString)
Some(0)
case None => sys.error(s"${config.app.groupID}-${config.app.main} is not configured as a server.")
}
}
/** Some hackery to allow sys.props to be configured via a file. If this launch config has
* a valid file configured, we load the properties and and apply them to this jvm.
*/
def updateProperties(config: LaunchConfiguration): Unit = {
config.serverConfig match {
case Some(config) =>
config.jvmPropsFile match {
case Some(file) if file.exists =>
try setSystemProperties(readProperties(file))
catch {
case e: Exception => throw new RuntimeException(s"Unable to load server properties file: ${file}", e)
}
case _ =>
}
case None =>
}
}
/** Parses the configuration *and* runs the initialization code that will remove variable references. */
def parseAndInitializeConfig(configLocation: URL, currentDirectory: File): LaunchConfiguration =
{
@ -84,6 +128,10 @@ object Launch
Thread.currentThread.setContextClassLoader(loader)
try { eval } finally { Thread.currentThread.setContextClassLoader(oldLoader) }
}
// Cache of classes for lookup later.
val ServerMainClass = classOf[xsbti.ServerMain]
val AppMainClass = classOf[xsbti.AppMain]
}
final class RunConfiguration(val scalaVersion: Option[String], val app: xsbti.ApplicationID, val workingDirectory: File, val arguments: List[String])
@ -240,27 +288,32 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i
(scalaHome, libDirectory)
}
def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider = new xsbti.AppProvider
{
def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider =
new xsbti.AppProvider {
import Launch.{ServerMainClass,AppMainClass}
val scalaProvider = scalaProvider0
val id = appID
def mainClasspath = app.fullClasspath
lazy val loader = app.createLoader(scalaProvider.loader)
// TODO - For some reason we can't call this from vanilla scala. We get a
// no such method exception UNLESS we're in the same project.
lazy val entryPoint: Class[T] forSome { type T } =
{
val c = Class.forName(id.mainClass, true, loader)
if(classOf[xsbti.AppMain].isAssignableFrom(c)) c
else if(PlainApplication.isPlainApplication(c)) c
else sys.error(s"Class: ${c} is not an instance of xsbti.AppMain nor does it have one of these static methods:\n"+
" * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)")
else if(ServerApplication.isServerApplication(c)) c
else sys.error(s"${c} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have one of these static methods:\n"+
" * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)\n")
}
// Deprecated API. Remove when we can.
def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(classOf[xsbti.AppMain])
def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(AppMainClass)
def newMain(): xsbti.AppMain = {
if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint)
else mainClass.newInstance
if(ServerApplication.isServerApplication(entryPoint)) ServerApplication(this)
else if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint)
else if(AppMainClass.isAssignableFrom(entryPoint)) mainClass.newInstance
else throw new IncompatibleClassChangeError(s"Main class ${entryPoint.getName} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have a valid `main` method.")
}
lazy val components = componentProvider(appHome)
}
def componentProvider(appHome: File) = new ComponentProvider(appHome, lockBoot)

View File

@ -9,27 +9,46 @@ import java.net.URL
import scala.collection.immutable.List
//TODO: use copy constructor, check size change
final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty])
final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty], serverConfig: Option[ServerConfiguration])
{
def isServer: Boolean = serverConfig.isDefined
def getScalaVersion = {
val sv = Value.get(scalaVersion)
if(sv == "auto") None else Some(sv)
}
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties)
def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties)
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties)
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties, serverConfig)
def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties, serverConfig)
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig)
// TODO: withExplicit
def withVersions(newScalaVersion: String, newAppVersion: String, classifiers0: Classifiers) =
LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties)
LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig)
def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties)
def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties, serverConfig.map(_ map f))
}
object LaunchConfiguration {
// Saves a launch configuration into a file. This is only safe if it is loaded by the *same* launcher version.
def save(config: LaunchConfiguration, f: File): Unit = {
val out = new java.io.ObjectOutputStream(new java.io.FileOutputStream(f))
try out.writeObject(config)
finally out.close()
}
// Restores a launch configuration from a file. This is only safe if it is loaded by the *same* launcher version.
def restore(url: URL): LaunchConfiguration = {
val in = new java.io.ObjectInputStream(url.openConnection.getInputStream)
try in.readObject.asInstanceOf[LaunchConfiguration]
finally in.close()
}
}
final case class ServerConfiguration(lockFile: File, jvmArgs: Option[File], jvmPropsFile: Option[File]) {
def map(f: File => File) =
ServerConfiguration(f(lockFile), jvmArgs map f, jvmPropsFile map f)
}
final case class IvyOptions(ivyHome: Option[File], classifiers: Classifiers, repositories: List[Repository.Repository], checksums: List[String], isOverrideRepositories: Boolean)
{
def map(f: File => File) = IvyOptions(ivyHome.map(f), classifiers, repositories, checksums, isOverrideRepositories)
}
sealed trait Value[T]
sealed trait Value[T] extends Serializable
final class Explicit[T](val value: T) extends Value[T] {
override def toString = value.toString
}
@ -130,7 +149,7 @@ sealed trait PropertyInit
final class SetProperty(val value: String) extends PropertyInit
final class PromptProperty(val label: String, val default: Option[String]) extends PropertyInit
final class Logging(level: LogLevel.Value)
final class Logging(level: LogLevel.Value) extends Serializable
{
def log(s: => String, at: LogLevel.Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s)
def debug(s: => String) = log(s, LogLevel.Debug)

View File

@ -70,6 +70,10 @@ object Pre
classes.toList.filter(classMissing)
}
def toURLs(files: Array[File]): Array[URL] = files.map(_.toURI.toURL)
def toFile(url: URL): File =
try { new File(url.toURI) }
catch { case _: java.net.URISyntaxException => new File(url.getPath) }
def delete(f: File)
{
@ -82,4 +86,25 @@ object Pre
}
final val isWindows: Boolean = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")
final val isCygwin: Boolean = isWindows && java.lang.Boolean.getBoolean("sbt.cygwin")
import java.util.Properties
import java.io.{FileInputStream,FileOutputStream}
private[boot] def readProperties(propertiesFile: File) =
{
val properties = new Properties
if(propertiesFile.exists)
Using( new FileInputStream(propertiesFile) )( properties.load )
properties
}
private[boot] def writeProperties(properties: Properties, file: File, msg: String): Unit = {
file.getParentFile.mkdirs()
Using(new FileOutputStream(file))( out => properties.store(out, msg) )
}
private[boot] def setSystemProperties(properties: Properties): Unit = {
val nameItr = properties.stringPropertyNames.iterator
while(nameItr.hasNext) {
val propName = nameItr.next
System.setProperty(propName, properties.getProperty(propName))
}
}
}

View File

@ -12,16 +12,9 @@ object ResolveValues
def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveValues(conf))()
private def trim(s: String) = if(s eq null) None else notEmpty(s.trim)
private def notEmpty(s: String) = if(isEmpty(s)) None else Some(s)
private[boot] def readProperties(propertiesFile: File) =
{
val properties = new Properties
if(propertiesFile.exists)
Using( new FileInputStream(propertiesFile) )( properties.load )
properties
}
}
import ResolveValues.{readProperties, trim}
import ResolveValues.{trim}
final class ResolveValues(conf: LaunchConfiguration)
{
private def propertiesFile = conf.boot.properties

View File

@ -0,0 +1,200 @@
package xsbt
package boot
import java.io.File
import scala.util.control.NonFatal
import java.net.URI
import java.io.IOException
import Pre._
import scala.annotation.tailrec
/** A wrapper around 'raw' static methods to meet the sbt application interface. */
class ServerApplication private (provider: xsbti.AppProvider) extends xsbti.AppMain {
import ServerApplication._
override def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
val serverMain = provider.entryPoint.asSubclass(ServerMainClass).newInstance
val server = serverMain.start(configuration)
System.out.println(s"${SERVER_SYNCH_TEXT}${server.uri}")
server.awaitTermination()
}
}
/** An object that lets us detect compatible "plain" applications and launch them reflectively. */
object ServerApplication {
val SERVER_SYNCH_TEXT = "[SERVER-URI]"
val ServerMainClass = classOf[xsbti.ServerMain]
// TODO - We should also adapt friendly static methods into servers, perhaps...
// We could even structurally type things that have a uri + awaitTermination method...
def isServerApplication(clazz: Class[_]): Boolean =
ServerMainClass.isAssignableFrom(clazz)
def apply(provider: xsbti.AppProvider): xsbti.AppMain =
new ServerApplication(provider)
}
object ServerLocator {
// TODO - Probably want to drop this to reduce classfile size
private def locked[U](file: File)(f: => U): U = {
Locks(file, new java.util.concurrent.Callable[U] {
def call(): U = f
})
}
// We use the lock file they give us to write the server info. However,
// it seems we cannot both use the server info file for locking *and*
// read from it successfully. Locking seems to blank the file. SO, we create
// another file near the info file to lock.a
def makeLockFile(f: File): File =
new File(f.getParentFile, s"${f.getName}.lock")
// Launch the process and read the port...
def locate(currentDirectory: File, config: LaunchConfiguration): URI =
config.serverConfig match {
case None => sys.error("No server lock file configured. Cannot locate server.")
case Some(sc) => locked(makeLockFile(sc.lockFile)) {
readProperties(sc.lockFile) match {
case Some(uri) if isReachable(uri) => uri
case _ =>
val uri = ServerLauncher.startServer(currentDirectory, config)
writeProperties(sc.lockFile, uri)
uri
}
}
}
private val SERVER_URI_PROPERTY = "server.uri"
def readProperties(f: File): Option[java.net.URI] = {
try {
val props = Pre.readProperties(f)
props.getProperty(SERVER_URI_PROPERTY) match {
case null => None
case uri => Some(new java.net.URI(uri))
}
} catch {
case e: IOException => None
}
}
def writeProperties(f: File, uri: URI): Unit = {
val props = new java.util.Properties
props.setProperty(SERVER_URI_PROPERTY, uri.toASCIIString)
val output = new java.io.FileOutputStream(f)
val df = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ")
df.setTimeZone(java.util.TimeZone.getTimeZone("UTC"))
Pre.writeProperties(props, f, s"Server Startup at ${df.format(new java.util.Date)}")
}
def isReachable(uri: java.net.URI): Boolean =
try {
// TODO - For now we assume if we can connect, it means
// that the server is working...
val socket = new java.net.Socket(uri.getHost, uri.getPort)
try socket.isConnected
finally socket.close()
} catch {
case e: IOException => false
}
}
/** A helper class that dumps incoming values into a print stream. */
class StreamDumper(in: java.io.BufferedReader, out: java.io.PrintStream) extends Thread {
// Don't block the application for this thread.
setDaemon(true)
private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
override def run(): Unit = {
def read(): Unit = if(running.get) in.readLine match {
case null => ()
case line =>
out.println(line)
read()
}
read()
out.close()
}
def close(): Unit = running.set(false)
}
object ServerLauncher {
import ServerApplication.SERVER_SYNCH_TEXT
def startServer(currentDirectory: File, config: LaunchConfiguration): URI = {
val serverConfig = config.serverConfig match {
case Some(c) => c
case None => throw new RuntimeException("Logic Failure: Attempting to start a server that isn't configured to be a server. Please report a bug.")
}
val launchConfig = java.io.File.createTempFile("sbtlaunch", "config")
launchConfig.deleteOnExit()
LaunchConfiguration.save(config, launchConfig)
val jvmArgs: List[String] = serverConfig.jvmArgs map readLines match {
case Some(args) => args
case None => Nil
}
val cmd: List[String] =
("java" :: jvmArgs) ++
("-jar" :: defaultLauncherLookup.getCanonicalPath :: s"@load:${launchConfig.toURI.toURL.toString}" :: Nil)
launchProcessAndGetUri(cmd, currentDirectory)
}
// Here we try to isolate all the stupidity of dealing with Java processes.
def launchProcessAndGetUri(cmd: List[String], cwd: File): URI = {
// TODO - Handle windows path stupidity in arguments.
val pb = new java.lang.ProcessBuilder()
pb.command(cmd:_*)
pb.directory(cwd)
val process = pb.start()
// First we need to grab all the input streams, and close the ones we don't care about.
process.getOutputStream.close()
val stderr = process.getErrorStream
val stdout = process.getInputStream
// Now we start dumping out errors.
val errorDumper = new StreamDumper(new java.io.BufferedReader(new java.io.InputStreamReader(stderr)), System.err)
errorDumper.start()
// Now we look for the URI synch value, and then make sure we close the output files.
try readUntilSynch(new java.io.BufferedReader(new java.io.InputStreamReader(stdout))) match {
case Some(uri) => uri
case _ => sys.error("Failed to start server!")
} finally {
errorDumper.close()
stdout.close()
stderr.close()
}
}
object ServerUriLine {
def unapply(in: String): Option[URI] =
if(in startsWith SERVER_SYNCH_TEXT) {
Some(new URI(in.substring(SERVER_SYNCH_TEXT.size)))
} else None
}
/** Reads an input steam until it hits the server synch text and server URI. */
def readUntilSynch(in: java.io.BufferedReader): Option[URI] = {
@tailrec
def read(): Option[URI] = in.readLine match {
case null => None
case ServerUriLine(uri) => Some(uri)
case line => read()
}
try read()
finally in.close()
}
/** Reads all the lines in a file. If it doesn't exist, returns an empty list. Forces UTF-8 strings. */
def readLines(f: File): List[String] =
if(!f.exists) Nil else {
val reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"))
@tailrec
def read(current: List[String]): List[String] =
reader.readLine match {
case null => current.reverse
case line => read(line :: current)
}
try read(Nil)
finally reader.close()
}
def defaultLauncherLookup: File =
try {
val classInLauncher = classOf[AppConfiguration]
val fileOpt = for {
domain <- Option(classInLauncher.getProtectionDomain)
source <- Option(domain.getCodeSource)
location = source.getLocation
} yield toFile(location)
fileOpt.getOrElse(throw new RuntimeException("Could not inspect protection domain or code source"))
} catch {
case e: Throwable => throw new RuntimeException("Unable to find sbt-launch.jar.", e)
}
}

View File

@ -55,7 +55,7 @@ final class Update(config: UpdateConfiguration)
val optionProps =
Option(System.getProperty("sbt.boot.credentials")) orElse
Option(System.getenv("SBT_CREDENTIALS")) map ( path =>
ResolveValues.readProperties(new File(path))
Pre.readProperties(new File(path))
)
optionProps match {
case Some(props) => extractCredentials("realm","host","user","password")(props)

View File

@ -0,0 +1,53 @@
package xsbt.boot
import java.io.{File,InputStream}
import java.net.URL
import java.util.Properties
import xsbti._
import org.specs2._
import mutable.Specification
import LaunchTest._
import sbt.IO.{createDirectory, touch,withTemporaryDirectory}
import java.net.URI
object ServerLocatorTest extends Specification
{
"ServerLocator" should {
// TODO - Maybe use scalacheck to randomnly generate URIs
"read and write server URI properties" in {
withTemporaryDirectory { dir =>
val propFile = new File(dir, "server.properties")
val expected = new java.net.URI("http://localhost:8080")
ServerLocator.writeProperties(propFile, expected)
ServerLocator.readProperties(propFile) must equalTo(Some(expected))
}
}
"detect listening ports" in {
val serverSocket = new java.net.ServerSocket(0)
object serverThread extends Thread {
override def run(): Unit = {
// Accept one connection.
val result = serverSocket.accept()
result.close()
serverSocket.close()
}
}
serverThread.start()
val uri = new java.net.URI(s"http://${serverSocket.getInetAddress.getHostAddress}:${serverSocket.getLocalPort}")
ServerLocator.isReachable(uri) must beTrue
}
}
"ServerLauncher" should {
"detect start URI from reader" in {
val expected = new java.net.URI("http://localhost:8080")
val input = s"""|Some random text
|to start the server
|${ServerApplication.SERVER_SYNCH_TEXT}${expected.toASCIIString}
|Some more output.""".stripMargin
val inputStream = new java.io.BufferedReader(new java.io.StringReader(input))
val result = try ServerLauncher.readUntilSynch(inputStream)
finally inputStream.close()
result must equalTo(Some(expected))
}
}
}

View File

@ -0,0 +1,74 @@
/** These are packaged and published locally and the resulting artifact is used to test the launcher.*/
package xsbt.boot.test
import java.net.Socket
import java.net.SocketTimeoutException
class EchoServer extends xsbti.ServerMain
{
def start(configuration: xsbti.AppConfiguration): xsbti.Server =
{
object server extends xsbti.Server {
// TODO - Start a server.
val serverSocket = new java.net.ServerSocket(0)
val port = serverSocket.getLocalPort
val addr = serverSocket.getInetAddress.getHostAddress
override val uri =new java.net.URI(s"http://${addr}:${port}")
// Check for stop every second.
serverSocket.setSoTimeout(1000)
object serverThread extends Thread {
private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
override def run(): Unit = {
while(running.get) try {
val clientSocket = serverSocket.accept()
// Handle client connections
object clientSocketThread extends Thread {
override def run(): Unit = {
echoTo(clientSocket)
}
}
clientSocketThread.start()
} catch {
case e: SocketTimeoutException => // Ignore
}
}
// Simple mechanism to dump input to output.
private def echoTo(socket: Socket): Unit = {
val input = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream))
val output = new java.io.BufferedWriter(new java.io.OutputStreamWriter(socket.getOutputStream))
import scala.util.control.Breaks._
try {
// Lame way to break out.
breakable {
def read(): Unit = input.readLine match {
case null => ()
case "kill" =>
running.set(false)
serverSocket.close()
break()
case line =>
output.write(line)
output.flush()
read()
}
read()
}
} finally {
output.close()
input.close()
socket.close()
}
}
}
// Start the thread immediately
serverThread.start()
override def awaitTermination(): xsbti.MainResult = {
serverThread.join()
new Exit(0)
}
}
server
}
}

View File

@ -9,7 +9,6 @@ Before reading anything in here, you will need the information in the
.. toctree::
:maxdepth: 2
Launcher
Scripts
TaskInputs
Understanding-incremental-recompilation

View File

@ -1,387 +1,5 @@
======================
Launcher Specification
======================
============
Sbt Launcher
============
The sbt launcher component is a self-contained jar that boots a Scala
application without Scala or the application already existing on the
system. The only prerequisites are the launcher jar itself, an optional
configuration file, and a java runtime version 1.6 or greater.
Overview
========
A user downloads the launcher jar and creates a script to run it. In
this documentation, the script will be assumed to be called `launch`.
For unix, the script would look like:
`java -jar sbt-launcher.jar "$@"`
The user then downloads the configuration file for the application (call
it `my.app.configuration`) and creates a script to launch it (call it
`myapp`): `launch @my.app.configuration "$@"`
The user can then launch the application using `myapp arg1 arg2 ...`
Like the launcher used to distribute `sbt`, the downloaded launcher
jar will retrieve Scala and the application according to the provided
configuration file. The versions may be fixed or read from a different
configuration file (the location of which is also configurable). The
location to which the Scala and application jars are downloaded is
configurable as well. The repositories searched are configurable.
Optional initialization of a properties file on launch is configurable.
Once the launcher has downloaded the necessary jars, it loads the
application and calls its entry point. The application is passed
information about how it was called: command line arguments, current
working directory, Scala version, and application ID (organization,
name, version). In addition, the application can ask the launcher to
perform operations such as obtaining the Scala jars and a
`ClassLoader` for any version of Scala retrievable from the
repositories specified in the configuration file. It can request that
other applications be downloaded and run. When the application
completes, it can tell the launcher to exit with a specific exit code or
to reload the application with a different version of Scala, a different
version of the application, or different arguments.
There are some other options for setup, such as putting the
configuration file inside the launcher jar and distributing that as a
single download. The rest of this documentation describes the details of
configuring, writing, distributing, and running the application.
Configuration
-------------
The launcher may be configured in one of the following ways in
increasing order of precedence:
- Replace the `/sbt/sbt.boot.properties` file in the jar
- Put a configuration file named `sbt.boot.properties` on the
classpath. Put it in the classpath root without the `/sbt` prefix.
- Specify the location of an alternate configuration on the command
line, either as a path or an absolute URI. This can be done by
either specifying the location as the system property
`sbt.boot.properties` or as the first argument to the launcher
prefixed by `'@'`. The system property has lower precedence.
Resolution of a relative path is first attempted against the current
working directory, then against the user's home directory, and then
against the directory containing the launcher jar. An error is
generated if none of these attempts succeed.
Syntax
~~~~~~
The configuration file is line-based, read as UTF-8 encoded, and defined
by the following grammar. `'nl'` is a newline or end of file and
`'text'` is plain text without newlines or the surrounding delimiters
(such as parentheses or square brackets):
.. productionlist::
configuration: `scala` `app` `repositories` `boot` `log` `appProperties`
scala: "[" "scala" "]" `nl` `version` `nl` `classifiers` `nl`
app: "[" "app" "]" `nl` `org` `nl` `name` `nl` `version` `nl` `components` `nl` `class` `nl` `crossVersioned` `nl` `resources` `nl` `classifiers` `nl`
repositories: "[" "repositories" "]" `nl` (`repository` `nl`)*
boot: "[" "boot" "]" `nl` `directory` `nl` `bootProperties` `nl` `search` `nl` `promptCreate` `nl` `promptFill` `nl` `quickOption` `nl`
log: "["' "log" "]" `nl` `logLevel` `nl`
appProperties: "[" "app-properties" "]" nl (property nl)*
ivy: "[" "ivy" "]" `nl` `homeDirectory` `nl` `checksums` `nl` `overrideRepos` `nl` `repoConfig` `nl`
directory: "directory" ":" `path`
bootProperties: "properties" ":" `path`
search: "search" ":" ("none" | "nearest" | "root-first" | "only" ) ("," `path`)*
logLevel: "level" ":" ("debug" | "info" | "warn" | "error")
promptCreate: "prompt-create" ":" `label`
promptFill: "prompt-fill" ":" `boolean`
quickOption: "quick-option" ":" `boolean`
version: "version" ":" `versionSpecification`
versionSpecification: `readProperty` | `fixedVersion`
readProperty: "read" "(" `propertyName` ")" "[" `default` "]"
fixedVersion: text
classifiers: "classifiers" ":" text ("," text)*
homeDirectory: "ivy-home" ":" `path`
checksums: "checksums" ":" `checksum` ("," `checksum`)*
overrideRepos: "override-build-repos" ":" `boolean`
repoConfig: "repository-config" ":" `path`
org: "org" ":" text
name: "name" ":" text
class: "class" ":" text
components: "components" ":" `component` ("," `component`)*
crossVersioned: "cross-versioned" ":" ("true" | "false" | "none" | "binary" | "full")
resources: "resources" ":" `path` ("," `path`)*
repository: ( `predefinedRepository` | `customRepository` ) `nl`
predefinedRepository: "local" | "maven-local" | "maven-central"
customRepository: `label` ":" `url` [ ["," `ivyPattern`] ["," `artifactPattern`] [", mavenCompatible"] [", bootOnly"]]
property: `label` ":" `propertyDefinition` ("," `propertyDefinition`)*
propertyDefinition: `mode` "=" (`set` | `prompt`)
mode: "quick" | "new" | "fill"
set: "set" "(" value ")"
prompt: "prompt" "(" `label` ")" ("[" `default` "]")?
boolean: "true" | "false"
nl: "\r\n" | "\n" | "\r"
path: text
propertyName: text
label: text
default: text
checksum: text
ivyPattern: text
artifactPattern: text
url: text
component: text
In addition to the grammar specified here, property values may include
variable substitutions. A variable substitution has one of these forms:
- `${variable.name}`
- `${variable.name-default}`
where `variable.name` is the name of a system property. If a system
property by that name exists, the value is substituted. If it does not
exists and a default is specified, the default is substituted after
recursively substituting variables in it. If the system property does
not exist and no default is specified, the original string is not
substituted.
Example
~~~~~~~
The default configuration file for sbt looks like:
.. parsed-literal::
[scala]
version: ${sbt.scala.version-auto}
[app]
org: ${sbt.organization-org.scala-sbt}
name: sbt
version: ${sbt.version-read(sbt.version)[\ |release|\ ]}
class: ${sbt.main.class-sbt.xMain}
components: xsbti,extra
cross-versioned: ${sbt.cross.versioned-false}
[repositories]
local
typesafe-ivy-releases: http://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly
maven-central
sonatype-snapshots: https://oss.sonatype.org/content/repositories/snapshots
[boot]
directory: ${sbt.boot.directory-${sbt.global.base-${user.home}/.sbt}/boot/}
[ivy]
ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/}
checksums: ${sbt.checksums-sha1,md5}
override-build-repos: ${sbt.override.build.repos-false}
repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories}
Semantics
~~~~~~~~~
The `scala.version` property specifies the version of Scala used to
run the application. If the application is not cross-built, this may be
set to `auto` and it will be auto-detected from the application's
dependencies. If specified, the `scala.classifiers` property defines
classifiers, such as 'sources', of extra Scala artifacts to retrieve.
The `app.org`, `app.name`, and `app.version` properties specify
the organization, module ID, and version of the application,
respectively. These are used to resolve and retrieve the application
from the repositories listed in `[repositories]`. If
`app.cross-versioned` is `binary`, the resolved module ID is
`{app.name+'_'+CrossVersion.binaryScalaVersion(scala.version)}`.
If `app.cross-versioned` is `true` or `full`, the resolved module ID is
`{app.name+'_'+scala.version}`. The `scala.version` property must be
specified and cannot be `auto` when cross-versioned. The paths given
in `app.resources` are added to the application's classpath. If the
path is relative, it is resolved against the application's working
directory. If specified, the `app.classifiers` property defines
classifiers, like 'sources', of extra artifacts to retrieve for the
application.
Jars are retrieved to the directory given by `boot.directory`. By
default, this is an absolute path that is shared by all launched
instances on the machine. If multiple versions access it simultaneously.
, you might see messages like:
.. code-block:: console
Waiting for lock on <lock-file> to be available...
This boot directory may be relative to the current directory instead. In
this case, the launched application will have a separate boot directory
for each directory it is launched in.
The `boot.properties` property specifies the location of the
properties file to use if `app.version` or `scala.version` is
specified as `read`. The `prompt-create`, `prompt-fill`, and
`quick-option` properties together with the property definitions in
`[app.properties]` can be used to initialize the `boot.properties`
file.
The app.class property specifies the name of the entry point to the
application. An application entry point must be a public class with a
no-argument constructor that implements `xsbti.AppMain`. The
`AppMain` interface specifies the entry method signature 'run'. The
run method is passed an instance of AppConfiguration, which provides
access to the startup environment. `AppConfiguration` also provides an
interface to retrieve other versions of Scala or other applications.
Finally, the return type of the run method is `xsbti.MainResult`,
which has two subtypes: `xsbti.Reboot` and `xsbti.Exit`. To exit
with a specific code, return an instance of `xsbti.Exit` with the
requested code. To restart the application, return an instance of
Reboot. You can change some aspects of the configuration with a reboot,
such as the version of Scala, the application ID, and the arguments.
The `ivy.cache-directory` property provides an alternative location
for the Ivy cache used by the launcher. This does not automatically set
the Ivy cache for the application, but the application is provided this
location through the AppConfiguration instance. The `checksums`
property selects the checksum algorithms (sha1 or md5) that are used to
verify artifacts downloaded by the launcher. `override-build-repos` is
a flag that can inform the application that the repositories configured
for the launcher should be used in the application. If
`repository-config` is defined, the file it specifies should contain a
`[repositories]` section that is used in place of the section in the
original configuration file.
Execution
---------
On startup, the launcher searches for its configuration in the order
described in the Configuration section and then parses it. If either the
Scala version or the application version are specified as 'read', the
launcher determines them in the following manner. The file given by the
'boot.properties' property is read as a Java properties file to obtain
the version. The expected property names are `${app.name}.version` for
the application version (where `${app.name}` is replaced with the
value of the `app.name` property from the boot configuration file) and
`scala.version` for the Scala version. If the properties file does not
exist, the default value provided is used. If no default was provided,
an error is generated.
Once the final configuration is resolved, the launcher proceeds to
obtain the necessary jars to launch the application. The
`boot.directory` property is used as a base directory to retrieve jars
to. Locking is done on the directory, so it can be shared system-wide.
The launcher retrieves the requested version of Scala to
.. code-block:: console
${boot.directory}/${scala.version}/lib/
If this directory already exists, the launcher takes a shortcut for
startup performance and assumes that the jars have already been
downloaded. If the directory does not exist, the launcher uses Apache
Ivy to resolve and retrieve the jars. A similar process occurs for the
application itself. It and its dependencies are retrieved to
.. code-block:: console
${boot.directory}/${scala.version}/${app.org}/${app.name}/.
Once all required code is downloaded, the class loaders are set up. The
launcher creates a class loader for the requested version of Scala. It
then creates a child class loader containing the jars for the requested
'app.components' and with the paths specified in `app.resources`. An
application that does not use components will have all of its jars in
this class loader.
The main class for the application is then instantiated. It must be a
public class with a public no-argument constructor and must conform to
xsbti.AppMain. The `run` method is invoked and execution passes to the
application. The argument to the 'run' method provides configuration
information and a callback to obtain a class loader for any version of
Scala that can be obtained from a repository in [repositories]. The
return value of the run method determines what is done after the
application executes. It can specify that the launcher should restart
the application or that it should exit with the provided exit code.
Creating a Launched Application
-------------------------------
This section shows how to make an application that is launched by this
launcher. First, declare a dependency on the launcher-interface. Do not
declare a dependency on the launcher itself. The launcher interface
consists strictly of Java interfaces in order to avoid binary
incompatibility between the version of Scala used to compile the
launcher and the version used to compile your application. The launcher
interface class will be provided by the launcher, so it is only a
compile-time dependency. If you are building with sbt, your dependency
definition would be:
.. parsed-literal::
libraryDependencies += "org.scala-sbt" % "launcher-interface" % "|release|" % "provided"
resolvers += sbtResolver.value
Make the entry point to your class implement 'xsbti.AppMain'. An example
that uses some of the information:
.. code-block:: scala
package xsbt.test
class Main extends xsbti.AppMain
{
def run(configuration: xsbti.AppConfiguration) =
{
// get the version of Scala used to launch the application
val scalaVersion = configuration.provider.scalaProvider.version
// Print a message and the arguments to the application
println("Hello world! Running Scala " + scalaVersion)
configuration.arguments.foreach(println)
// demonstrate the ability to reboot the application into different versions of Scala
// and how to return the code to exit with
scalaVersion match
{
case "2.9.3" =>
new xsbti.Reboot {
def arguments = configuration.arguments
def baseDirectory = configuration.baseDirectory
def scalaVersion = "2.10.2
def app = configuration.provider.id
}
case "2.10.2" => new Exit(1)
case _ => new Exit(0)
}
}
class Exit(val code: Int) extends xsbti.Exit
}
Next, define a configuration file for the launcher. For the above class,
it might look like:
.. parsed-literal::
[scala]
version: |scalaRelease|
[app]
org: org.scala-sbt
name: xsbt-test
version: |release|
class: xsbt.test.Main
cross-versioned: binary
[repositories]
local
maven-central
[boot]
directory: ${user.home}/.myapp/boot
Then, `publishLocal` or `+publishLocal` the application to make it
available.
Running an Application
----------------------
As mentioned above, there are a few options to actually run the
application. The first involves providing a modified jar for download.
The second two require providing a configuration file for download.
- Replace the /sbt/sbt.boot.properties file in the launcher jar and
distribute the modified jar. The user would need a script to run
`java -jar your-launcher.jar arg1 arg2 ...`.
- The user downloads the launcher jar and you provide the configuration
file.
- The user needs to run `java -Dsbt.boot.properties=your.boot.properties -jar launcher.jar`.
- The user already has a script to run the launcher (call it
'launch'). The user needs to run `launch @your.boot.properties your-arg-1 your-arg-2`
This docuemntation has been moved to :doc:`The Launcher section </Launcher/index>`.

View File

@ -19,3 +19,4 @@ Other resources include the :doc:`Examples </Examples/index>` and
Tasks-and-Commands
Plugins-and-Best-Practices
Advanced-Index
/Launcher/index

View File

@ -0,0 +1,108 @@
=========================
Sbt Launcher Architecture
=========================
The sbt launcher is a mechanism whereby modules can be loaded from ivy and
executed within a jvm. It abstracts the mechanism of grabbing and caching jars,
allowing users to focus on what application they want and control its versions.
The launcher's primary goal is to take configuration for applications, mostly
just ivy coordinates and a main class, and start the application. The
launcher resolves the ivy module, caches the required runtime jars and
starts the application.
The sbt launcher provides the application with the means to load a different
application when it completes, exit normally, or load additional applications
from inside another.
The sbt launcher provides these core functions:
* Module Resolution
* Classloader Caching and Isolation
* File Locking
* Service Discovery and Isolation
Module Resolution
~~~~~~~~~~~~~~~~~
The primary purpose of the sbt launcher is to resolve applications and run them.
This is done through the `[app]` configuration section. See :doc:Configuration
for more information on how to configure module resolution.
Module resolution is performed using the Ivy dependency managemnet library. This
library supports loading artifacts from Maven repositories as well.
Classloader Caching and Isolation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The sbt launcher's classloading structure is different than just starting an
application in the standard java mechanism. Every application loaded by
by the launcher is given its own classloader. This classloader is a child
of the Scala classloader used by the application. The Scala classloader can see
all of the `xsbti.*` classes from the launcher itself.
Here's an example classloader layout from an sbt launched application.
.. image:: classloaders.png
In this diagram, three different applications were loaded. Two of these use the
same version of Scala (2.9.2). In this case, sbt can share the same classloader
for these applications. This has the benefit that any JIT optimisations performed
on scala classes can be re-used between applications thanks to the shared
classloader.
Caching
~~~~~~~
The sbt launcher creates a secondary cache on top of Ivy's own cache. This helps
isolate applications from errors resulting from unstable revisions, like
`-SNAPSHOT`. For any launched application, the launcher creates a directory
to store all its jars. Here's an example layout.
.. parsed-literal::
${boot.directory}/
scala_2.9.2/
lib/
<scala library jars>
<org>/<name>/<version>/
<application-1 jars>
<org>/<name>/<version>/
<application-2 jars>
scala_2.10.3/
lib/
<scala library jars>
<org>/<name>/<version>/
<application-3 jars>/
Locking
~~~~~~~
In addition to providing a secondary cache, the launcher also provides a mechanism
of safely doing file-based locks. This is used in two places directly by the
launcher:
1. Locking the boot directory.
2. Ensuring located servers have at most one active process.
This feature requires a filesystem which supports locking. It is exposed via the
`xsbti.GlobalLock` interface.
*Note: This is both a thread and file lock. Not only are we limiting access to a single process, but also a single thread within that process.*
Service Discovery and Isolation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The launcher also provides a mechanism to ensure that only one instance of a
server is running, while dynamically starting it when a client requests. This
is done through the `--locate` flag on the launcher. When the launcher is
started with the `--locate` flag it will do the following:
1. Lock on the configured server lock file.
2. Read the server properties to find the URI of the previous server.
3. If the port is still listening to connection requests, print this URI
on the command line.
4. If the port is not listening, start a new server and write the URI
on the command line.
5. Release all locks and shutdown.
The configured `server.lock` file is thus used to prevent multiple servers from
running. Sbt itself uses this to prevent more than one server running on any
given project directory by configuring `server.lock` to be
`${user.dir}/.sbtserver`.

View File

@ -0,0 +1,260 @@
==========================
Sbt Launcher Configuration
==========================
The launcher may be configured in one of the following ways in
increasing order of precedence:
- Replace the `/sbt/sbt.boot.properties` file in the launcher jar
- Put a configuration file named `sbt.boot.properties` on the
classpath. Put it in the classpath root without the `/sbt` prefix.
- Specify the location of an alternate configuration on the command
line, either as a path or an absolute URI. This can be done by
either specifying the location as the system property
`sbt.boot.properties` or as the first argument to the launcher
prefixed by `'@'`. The system property has lower precedence.
Resolution of a relative path is first attempted against the current
working directory, then against the user's home directory, and then
against the directory containing the launcher jar.
An error is generated if none of these attempts succeed.
Example
~~~~~~~
The default configuration file for sbt as an application looks like:
.. parsed-literal::
[scala]
version: ${sbt.scala.version-auto}
[app]
org: ${sbt.organization-org.scala-sbt}
name: sbt
version: ${sbt.version-read(sbt.version)[\ |release|\ ]}
class: ${sbt.main.class-sbt.xMain}
components: xsbti,extra
cross-versioned: ${sbt.cross.versioned-false}
[repositories]
local
typesafe-ivy-releases: http://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly
maven-central
sonatype-snapshots: https://oss.sonatype.org/content/repositories/snapshots
[boot]
directory: ${sbt.boot.directory-${sbt.global.base-${user.home}/.sbt}/boot/}
[ivy]
ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/}
checksums: ${sbt.checksums-sha1,md5}
override-build-repos: ${sbt.override.build.repos-false}
repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories}
Let's look at all the launcher configuration sections in detail:
1. Scala Configuration
----------------------
The `[scala]` section is used to configure the version of Scala.
It has one property:
* `version` - The version of scala an application uses, or `auto` if the
application is not cross-versioned.
* `classifiers` - The (optional) list of additional scala artifacts to resolve,
e.g. `sources`.
2. Applicaiton Identification
-----------------------------
The `[app]` section configures how the launcher will look for your application
using the Ivy dependency manager. It consists of the following properties:
* `org` - The organization associated with the Ivy module.
(`groupId` in maven vernacular)
* `name` - The name of the Ivy module. (`artifactId` in maven vernacular)
* `version` - The revision of the Ivy module.
* `class` - The name of the "entry point" into the application. An entry
point must be a class which meets one of the following critera
- Extends the `xsbti.AppMain` interface.
- Extends the `xsbti.ServerMain` interfaces.
- Contains a method with the signature `static void main(String[])`
- Contains a method with the signature `static int main(String[])`
- Contains a method with the signature `static xsbti.Exit main(String[])`
* `components` - An optional list of additional components that Ivy should
resolve.
* `cross-versioned` - An optional string denoting how this application is
published.
If `app.cross-versioned` is `binary`, the resolved module ID is
`{app.name+'_'+CrossVersion.binaryScalaVersion(scala.version)}`.
If `app.cross-versioned` is `true` or `full`, the resolved module ID is
`{app.name+'_'+scala.version}`. The `scala.version` property must be
specified and cannot be `auto` when cross-versioned.
* `resources` - An optional list of jar files that should be added to
the application's classpath.
* `classifiers` - An optional list of additional classifiers that should be
resolved with this application, e.g. `sources`.
3. Repositories Section
-----------------------
The `[repositories]` section configures where and how Ivy will look for
your application. Each line denotes a repository where Ivy will look.
*Note: This section configured the default location where Ivy will look, but
this can be overriden via user configuration.*
There are several built-in strings that can be used for common repositories:
* `local` - the local ivy repository `~/.ivy2/local`.
* `maven-local` - The local maven repository `~/.ivy2/local`.
* `maven-central` - The maven central repository `repo.maven.org`.
Besides built in repositories, other repositories can be configured using
the following syntax:
.. parsed-literal::
name: url(, pattern)(,descriptorOptional)(,skipConsistencyCheck)
The `name` property is an identifier which Ivy uses to cache modules
resolved from this location. The `name` should be unique across all
repositories.
The `url` property is the base `url` where Ivy should look for modules.
The `pattern` property is an optional specification of *how* Ivy should
look for modules. By default, the launcher assumes repositories are in
the maven style format.
The `skipConsistencyCheck` string is used to tell ivy not to validate checksums
and signatures of files it resolves.
4. The Boot section
-------------------
The `[boot]` section is used to configure where the sbt launcher will store
its cache and configuration information. It consists of the following properties:
* `directory` - The directory defined here is used to store all cached JARs
resolved launcher.
* `properties` - (optional) A properties file to use for any `read` variables.
5. The Ivy section
------------------
The `[ivy]` section is used to configure the Ivy dependency manager for
resolving applications. It consists of the following properties:
* `ivy-home` - The home directory for Ivy. This determines where the
`ivy-local` repository is located, and also where the ivy cache is
stored. Defaults to `~/.ivy2`
* `ivy.cache-directory` - provides an alternative location for the Ivy
cache used by the launcher. This does not automatically set the Ivy
cache for the application, but the application is provided this location
through the AppConfiguration instance.
* `checksums` - The comma-separated list of checksums that Ivy should use
to verify artifacts have correctly resolved, e.g. `md5` or `sha1`.
* `override-build-repos` - If this is set, then the `isOverrideRepositories`
method on `xsbti.Launcher` interface will return its value. The use of this
method is application specific, but in the case of sbt denotes that the
configuration of repositories in the launcher should override those used
by any build. Applications should respect this convention if they can.
* `repository-config` - This specifies a configuration location where
ivy repositories can also be configured. If this file exists, then its contents
override the `[repositories]` section.
6. The Server Section
---------------------
When using the `--locate` feature of the launcher, this section configures
how a server is started. It consists of the following properties:
* `lock` - The file that controls access to the running server. This file
will contain the active port used by a server and must be located on a
a filesystem that supports locking.
* `jvmargs` - A file that contains line-separated JVM arguments that where
use when starting the server.
* `jvmprops` - The location of a properties file that will define override
properties in the server. All properties defined in this file will
be set as `-D` java properties.
Variable Substitution
~~~~~~~~~~~~~~~~~~~~~
Property values may include variable substitutions. A variable substitution has
one of these forms:
- `${variable.name}`
- `${variable.name-default}`
where `variable.name` is the name of a system property. If a system
property by that name exists, the value is substituted. If it does not
exists and a default is specified, the default is substituted after
recursively substituting variables in it. If the system property does
not exist and no default is specified, the original string is not
substituted.
There is also a special variable substitution:
- `read(property.name)[default]`
This will look in the file configured by `boot.properties` for a value. If
there is no `boot.properties` file configured, or the property does not existt,
then the default value is chosen.
Syntax
~~~~~~
The configuration file is line-based, read as UTF-8 encoded, and defined
by the following grammar. `'nl'` is a newline or end of file and
`'text'` is plain text without newlines or the surrounding delimiters
(such as parentheses or square brackets):
.. productionlist::
configuration: `scala` `app` `repositories` `boot` `log` `appProperties`
scala: "[" "scala" "]" `nl` `version` `nl` `classifiers` `nl`
app: "[" "app" "]" `nl` `org` `nl` `name` `nl` `version` `nl` `components` `nl` `class` `nl` `crossVersioned` `nl` `resources` `nl` `classifiers` `nl`
repositories: "[" "repositories" "]" `nl` (`repository` `nl`)*
boot: "[" "boot" "]" `nl` `directory` `nl` `bootProperties` `nl` `search` `nl` `promptCreate` `nl` `promptFill` `nl` `quickOption` `nl`
log: "["' "log" "]" `nl` `logLevel` `nl`
appProperties: "[" "app-properties" "]" nl (property nl)*
ivy: "[" "ivy" "]" `nl` `homeDirectory` `nl` `checksums` `nl` `overrideRepos` `nl` `repoConfig` `nl`
directory: "directory" ":" `path`
bootProperties: "properties" ":" `path`
search: "search" ":" ("none" | "nearest" | "root-first" | "only" ) ("," `path`)*
logLevel: "level" ":" ("debug" | "info" | "warn" | "error")
promptCreate: "prompt-create" ":" `label`
promptFill: "prompt-fill" ":" `boolean`
quickOption: "quick-option" ":" `boolean`
version: "version" ":" `versionSpecification`
versionSpecification: `readProperty` | `fixedVersion`
readProperty: "read" "(" `propertyName` ")" "[" `default` "]"
fixedVersion: text
classifiers: "classifiers" ":" text ("," text)*
homeDirectory: "ivy-home" ":" `path`
checksums: "checksums" ":" `checksum` ("," `checksum`)*
overrideRepos: "override-build-repos" ":" `boolean`
repoConfig: "repository-config" ":" `path`
org: "org" ":" text
name: "name" ":" text
class: "class" ":" text
components: "components" ":" `component` ("," `component`)*
crossVersioned: "cross-versioned" ":" ("true" | "false" | "none" | "binary" | "full")
resources: "resources" ":" `path` ("," `path`)*
repository: ( `predefinedRepository` | `customRepository` ) `nl`
predefinedRepository: "local" | "maven-local" | "maven-central"
customRepository: `label` ":" `url` [ ["," `ivyPattern`] ["," `artifactPattern`] [", mavenCompatible"] [", bootOnly"]]
property: `label` ":" `propertyDefinition` ("," `propertyDefinition`)*
propertyDefinition: `mode` "=" (`set` | `prompt`)
mode: "quick" | "new" | "fill"
set: "set" "(" value ")"
prompt: "prompt" "(" `label` ")" ("[" `default` "]")?
boolean: "true" | "false"
nl: "\r\n" | "\n" | "\r"
path: text
propertyName: text
label: text
default: text
checksum: text
ivyPattern: text
artifactPattern: text
url: text
component: text

View File

@ -0,0 +1,232 @@
=====================================
Getting Started with the Sbt Launcher
=====================================
The sbt launcher component is a self-contained jar that boots a Scala
application or server without Scala or the application already existing
on the system. The only prerequisites are the launcher jar itself, an
optional configuration file, and a java runtime version 1.6 or greater.
Overview
========
A user downloads the launcher jar and creates a script to run it. In
this documentation, the script will be assumed to be called `launch`.
For unix, the script would look like:
`java -jar sbt-launcher.jar "$@"`
The user can now launch servers and applications which provide sbt
launcher configuration.
Applications
------------
To launch an application, the user then downloads the configuration
file for the application (call it `my.app.configuration`) and creates
a script to launch it (call it `myapp`): `launch @my.app.configuration "$@"`
The user can then launch the application using `myapp arg1 arg2 ...`
More on launcher configuration can be found at :doc:`Launcher Configuration </Launcher/Configuration>`
Servers
-------
The sbt launcher can be used to launch and discover running servers
on the system. The launcher can be used to launch servers similarly to
applications. However, if desired, the launcher can also be used to
ensure that only one instance of a server is running at time. This is done
by having clients always use the launcher as a *service locator*.
To discover where a server is running (or launch it if it is not running),
the user downloads the configuration file for the server
(call it `my.server.configuration`) and creates a script to discover
the server (call it `find-myserver`): `launch --locate @my.server.properties`.
This command will print out one string, the URI at which to reach the server,
e.g. `sbt://127.0.0.1:65501`. Clients should use the IP/port to connect to
to the server and initiate their connection.
When using the `locate` feature, the sbt launcher makes these following
restrictions to servers:
- The Server must have a starting class that extends
the `xsbti.ServerMain` class
- The Server must have an entry point (URI) that clients
can use to detect the server
- The server must have defined a lock file which the launcher can
use to ensure that only one instance is running at a time
- The filesystem on which the lock file resides must support
locking.
- The server must allow the launcher to open a socket against the port
without sending any data. This is used to check if a previous
server is still alive.
Resolving Applications/Servers
------------------------------
Like the launcher used to distribute `sbt`, the downloaded launcher
jar will retrieve Scala and the application according to the provided
configuration file. The versions may be fixed or read from a different
configuration file (the location of which is also configurable). The
location to which the Scala and application jars are downloaded is
configurable as well. The repositories searched are configurable.
Optional initialization of a properties file on launch is configurable.
Once the launcher has downloaded the necessary jars, it loads the
application/server and calls its entry point. The application is passed
information about how it was called: command line arguments, current
working directory, Scala version, and application ID (organization,
name, version). In addition, the application can ask the launcher to
perform operations such as obtaining the Scala jars and a
`ClassLoader` for any version of Scala retrievable from the
repositories specified in the configuration file. It can request that
other applications be downloaded and run. When the application
completes, it can tell the launcher to exit with a specific exit code or
to reload the application with a different version of Scala, a different
version of the application, or different arguments.
There are some other options for setup, such as putting the
configuration file inside the launcher jar and distributing that as a
single download. The rest of this documentation describes the details of
configuring, writing, distributing, and running the application.
Creating a Launched Application
-------------------------------
This section shows how to make an application that is launched by this
launcher. First, declare a dependency on the launcher-interface. Do not
declare a dependency on the launcher itself. The launcher interface
consists strictly of Java interfaces in order to avoid binary
incompatibility between the version of Scala used to compile the
launcher and the version used to compile your application. The launcher
interface class will be provided by the launcher, so it is only a
compile-time dependency. If you are building with sbt, your dependency
definition would be:
.. parsed-literal::
libraryDependencies += "org.scala-sbt" % "launcher-interface" % "|release|" % "provided"
resolvers += sbtResolver.value
Make the entry point to your class implement 'xsbti.AppMain'. An example
that uses some of the information:
.. code-block:: scala
package xsbt.test
class Main extends xsbti.AppMain
{
def run(configuration: xsbti.AppConfiguration) =
{
// get the version of Scala used to launch the application
val scalaVersion = configuration.provider.scalaProvider.version
// Print a message and the arguments to the application
println("Hello world! Running Scala " + scalaVersion)
configuration.arguments.foreach(println)
// demonstrate the ability to reboot the application into different versions of Scala
// and how to return the code to exit with
scalaVersion match
{
case "2.9.3" =>
new xsbti.Reboot {
def arguments = configuration.arguments
def baseDirectory = configuration.baseDirectory
def scalaVersion = "2.10.2
def app = configuration.provider.id
}
case "2.10.2" => new Exit(1)
case _ => new Exit(0)
}
}
class Exit(val code: Int) extends xsbti.Exit
}
Next, define a configuration file for the launcher. For the above class,
it might look like:
.. parsed-literal::
[scala]
version: |scalaRelease|
[app]
org: org.scala-sbt
name: xsbt-test
version: |release|
class: xsbt.test.Main
cross-versioned: binary
[repositories]
local
maven-central
[boot]
directory: ${user.home}/.myapp/boot
Then, `publishLocal` or `+publishLocal` the application to make it
available. For more information, please see :doc:`Launcher Configuration </Detailed-Topics/Launcher/Configuration>`
Running an Application
----------------------
As mentioned above, there are a few options to actually run the
application. The first involves providing a modified jar for download.
The second two require providing a configuration file for download.
- Replace the /sbt/sbt.boot.properties file in the launcher jar and
distribute the modified jar. The user would need a script to run
`java -jar your-launcher.jar arg1 arg2 ...`.
- The user downloads the launcher jar and you provide the configuration
file.
- The user needs to run `java -Dsbt.boot.properties=your.boot.properties -jar launcher.jar`.
- The user already has a script to run the launcher (call it
'launch'). The user needs to run `launch @your.boot.properties your-arg-1 your-arg-2`
Execution
---------
Let's review what's happening when the launcher starts your application.
On startup, the launcher searches for its configuration and then
parses it. Once the final configuration is resolved, the launcher
proceeds to obtain the necessary jars to launch the application. The
`boot.directory` property is used as a base directory to retrieve jars
to. Locking is done on the directory, so it can be shared system-wide.
The launcher retrieves the requested version of Scala to
.. code-block:: console
${boot.directory}/${scala.version}/lib/
If this directory already exists, the launcher takes a shortcut for
startup performance and assumes that the jars have already been
downloaded. If the directory does not exist, the launcher uses Apache
Ivy to resolve and retrieve the jars. A similar process occurs for the
application itself. It and its dependencies are retrieved to
.. code-block:: console
${boot.directory}/${scala.version}/${app.org}/${app.name}/.
Once all required code is downloaded, the class loaders are set up. The
launcher creates a class loader for the requested version of Scala. It
then creates a child class loader containing the jars for the requested
'app.components' and with the paths specified in `app.resources`. An
application that does not use components will have all of its jars in
this class loader.
The main class for the application is then instantiated. It must be a
public class with a public no-argument constructor and must conform to
xsbti.AppMain. The `run` method is invoked and execution passes to the
application. The argument to the 'run' method provides configuration
information and a callback to obtain a class loader for any version of
Scala that can be obtained from a repository in [repositories]. The
return value of the run method determines what is done after the
application executes. It can specify that the launcher should restart
the application or that it should exit with the provided exit code.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,14 @@
==============
Sbt Launcher
==============
The sbt launcher provides a generic container that can load and run programs
resolved using the Ivy dependency manager. Sbt uses this as its own deployment
mechanism.
.. toctree::
:maxdepth: 2
GettingStarted
Configuration
Architecture

View File

@ -22,7 +22,6 @@ the :doc:`index of names and types <Name-Index>`.
Examples/index
Name-Index
.. The following includes documents that are not important enough to be in a visible toctree
They are linked from other documents, which is enough.