mirror of https://github.com/sbt/sbt.git
Merge pull request #1066 from jsuereth/wip/launcher-improvements
Launcher can now load servers in addition to regular applications.
This commit is contained in:
commit
4207978f89
|
|
@ -1,6 +1,25 @@
|
||||||
package xsbti;
|
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
|
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);
|
public MainResult run(AppConfiguration configuration);
|
||||||
}
|
}
|
||||||
|
|
@ -3,9 +3,10 @@ package xsbti;
|
||||||
import java.io.File;
|
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
|
public interface AppProvider
|
||||||
{
|
{
|
||||||
|
|
@ -33,6 +34,8 @@ public interface AppProvider
|
||||||
* It is NOT guaranteed that newMain().getClass() == mainClass().
|
* 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,
|
* 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.
|
* 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();
|
public AppMain newMain();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -5,36 +5,48 @@
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
// The entry point to the launcher
|
// The entry point to the launcher
|
||||||
object Boot
|
object Boot
|
||||||
{
|
{
|
||||||
def main(args: Array[String])
|
def main(args: Array[String])
|
||||||
{
|
{
|
||||||
args match {
|
val config = parseArgs(args)
|
||||||
case Array("--version") =>
|
// If we havne't exited, we set up some hooks and launch
|
||||||
println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion)
|
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
|
||||||
case _ =>
|
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
|
||||||
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
|
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
|
||||||
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
|
CheckProxy()
|
||||||
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
|
run(config)
|
||||||
CheckProxy()
|
|
||||||
run(args)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
// this arrangement is because Scala does not always properly optimize away
|
||||||
// the tail recursion in a catch statement
|
// 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 Some(newArgs) => run(newArgs)
|
||||||
case None => ()
|
case None => ()
|
||||||
}
|
}
|
||||||
private def runImpl(args: Array[String]): Option[Array[String]] =
|
private def runImpl(args: LauncherArguments): Option[LauncherArguments] =
|
||||||
try
|
try
|
||||||
Launch(args.toList) map exit
|
Launch(args) map exit
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
case b: BootException => errorAndExit(b.toString)
|
case b: BootException => errorAndExit(b.toString)
|
||||||
case r: xsbti.RetrieveException => errorAndExit("Error: " + r.getMessage)
|
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 =>
|
case e: Throwable =>
|
||||||
e.printStackTrace
|
e.printStackTrace
|
||||||
errorAndExit(Pre.prefixError(e.toString))
|
errorAndExit(Pre.prefixError(e.toString))
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,34 @@ import java.util.regex.Pattern
|
||||||
import scala.collection.immutable.List
|
import scala.collection.immutable.List
|
||||||
import annotation.tailrec
|
import annotation.tailrec
|
||||||
|
|
||||||
|
object ConfigurationStorageState extends Enumeration {
|
||||||
|
val PropertiesFile = value("properties-file")
|
||||||
|
val SerializedFile = value("serialized-file")
|
||||||
|
}
|
||||||
|
|
||||||
object Configuration
|
object Configuration
|
||||||
{
|
{
|
||||||
|
import ConfigurationStorageState._
|
||||||
final val SysPropPrefix = "-D"
|
final val SysPropPrefix = "-D"
|
||||||
def parse(file: URL, baseDirectory: File) = Using( new InputStreamReader(file.openStream, "utf8") )( (new ConfigurationParser).apply )
|
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
|
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) =>
|
case head :: tail if head.startsWith(SysPropPrefix) =>
|
||||||
setProperty(head stripPrefix SysPropPrefix)
|
setProperty(head stripPrefix SysPropPrefix)
|
||||||
find(tail, baseDirectory)
|
find(tail, baseDirectory)
|
||||||
case _ =>
|
case _ =>
|
||||||
val propertyConfigured = System.getProperty("sbt.boot.properties")
|
val propertyConfigured = System.getProperty("sbt.boot.properties")
|
||||||
val url = if(propertyConfigured == null) configurationOnClasspath else configurationFromFile(propertyConfigured, baseDirectory)
|
val url = if(propertyConfigured == null) configurationOnClasspath else configurationFromFile(propertyConfigured, baseDirectory)
|
||||||
(url , args)
|
(url, args, PropertiesFile)
|
||||||
}
|
}
|
||||||
def setProperty(head: String)
|
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
|
// 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] =
|
def guessSbtVersion: Option[String] =
|
||||||
{
|
{
|
||||||
val props = ResolveValues.readProperties(new File(DefaultBuildProperties))
|
val props = Pre.readProperties(new File(DefaultBuildProperties))
|
||||||
Option(props.getProperty(SbtVersionProperty))
|
Option(props.getProperty(SbtVersionProperty))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,14 @@ class ConfigurationParser
|
||||||
val (logging, m5) = processSection(m4, "log", getLogging)
|
val (logging, m5) = processSection(m4, "log", getLogging)
|
||||||
val (properties, m6) = processSection(m5, "app-properties", getAppProperties)
|
val (properties, m6) = processSection(m5, "app-properties", getAppProperties)
|
||||||
val ((ivyHome, checksums, isOverrideRepos, rConfigFile), m7) = processSection(m6, "ivy", getIvy)
|
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 classifiers = Classifiers(scalaClassifiers, appClassifiers)
|
||||||
val repositories = rConfigFile map readRepositoriesConfig getOrElse defaultRepositories
|
val repositories = rConfigFile map readRepositoriesConfig getOrElse defaultRepositories
|
||||||
val ivyOptions = IvyOptions(ivyHome, classifiers, repositories, checksums, isOverrideRepos)
|
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) =
|
def getScala(m: LabelMap) =
|
||||||
{
|
{
|
||||||
|
|
@ -178,6 +181,16 @@ class ConfigurationParser
|
||||||
val app = new Application(org, name, rev, main, components, LaunchCrossVersion(crossVersioned), classpathExtra)
|
val app = new Application(org, name, rev, main, components, LaunchCrossVersion(crossVersioned), classpathExtra)
|
||||||
(app, classifiers)
|
(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] =
|
def getRepositories(m: LabelMap): List[Repository.Repository] =
|
||||||
{
|
{
|
||||||
import Repository.{Ivy, Maven, Predefined}
|
import Repository.{Ivy, Maven, Predefined}
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,11 @@ object Initialize
|
||||||
def fill(file: File, spec: List[AppProperty]): Unit = process(file, spec, selectFill)
|
def fill(file: File, spec: List[AppProperty]): Unit = process(file, spec, selectFill)
|
||||||
def process(file: File, appProperties: List[AppProperty], select: AppProperty => Option[PropertyInit])
|
def process(file: File, appProperties: List[AppProperty], select: AppProperty => Option[PropertyInit])
|
||||||
{
|
{
|
||||||
val properties = new Properties
|
val properties = readProperties(file)
|
||||||
if(file.exists)
|
|
||||||
Using(new FileInputStream(file))( properties.load )
|
|
||||||
val uninitialized =
|
val uninitialized =
|
||||||
for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null) yield
|
for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null) yield
|
||||||
initialize(properties, property.name, init)
|
initialize(properties, property.name, init)
|
||||||
if(!uninitialized.isEmpty)
|
if(!uninitialized.isEmpty) writeProperties(properties, file, "")
|
||||||
{
|
|
||||||
file.getParentFile.mkdirs()
|
|
||||||
Using(new FileOutputStream(file))( out => properties.store(out, "") )
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
def initialize(properties: Properties, name: String, init: PropertyInit)
|
def initialize(properties: Properties, name: String, init: PropertyInit)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ package xsbt.boot
|
||||||
import Pre._
|
import Pre._
|
||||||
import scala.collection.immutable.List
|
import scala.collection.immutable.List
|
||||||
|
|
||||||
class Enumeration
|
class Enumeration extends Serializable
|
||||||
{
|
{
|
||||||
def elements: List[Value] = members
|
def elements: List[Value] = members
|
||||||
private lazy val members: List[Value] =
|
private lazy val members: List[Value] =
|
||||||
|
|
@ -25,6 +25,6 @@ class Enumeration
|
||||||
}
|
}
|
||||||
def value(s: String) = new Value(s, 0)
|
def value(s: String) = new Value(s, 0)
|
||||||
def value(s: String, i: Int) = new Value(s, i)
|
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 + ")"))
|
def toValue(s: String): Value = elements.find(_.toString == s).getOrElse(error("Expected one of " + elements.mkString(",") + " (got: " + s + ")"))
|
||||||
}
|
}
|
||||||
|
|
@ -6,20 +6,64 @@ package xsbt.boot
|
||||||
import Pre._
|
import Pre._
|
||||||
import BootConfiguration.{CompilerModuleName, JAnsiVersion, LibraryModuleName}
|
import BootConfiguration.{CompilerModuleName, JAnsiVersion, LibraryModuleName}
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.{URL, URLClassLoader}
|
import java.net.{URL, URLClassLoader, URI}
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
import scala.collection.immutable.List
|
import scala.collection.immutable.List
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
|
import ConfigurationStorageState._
|
||||||
|
|
||||||
|
class LauncherArguments(val args: List[String], val isLocate: Boolean)
|
||||||
|
|
||||||
object Launch
|
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] = {
|
def apply(currentDirectory: File, arguments: LauncherArguments): Option[Int] = {
|
||||||
val (configLocation, newArguments) = Configuration.find(arguments, currentDirectory)
|
val (configLocation, newArgs2, state) = Configuration.find(arguments.args, currentDirectory)
|
||||||
val config = parseAndInitializeConfig(configLocation, currentDirectory)
|
val config = state match {
|
||||||
launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArguments))
|
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. */
|
/** Parses the configuration *and* runs the initialization code that will remove variable references. */
|
||||||
def parseAndInitializeConfig(configLocation: URL, currentDirectory: File): LaunchConfiguration =
|
def parseAndInitializeConfig(configLocation: URL, currentDirectory: File): LaunchConfiguration =
|
||||||
{
|
{
|
||||||
|
|
@ -84,6 +128,10 @@ object Launch
|
||||||
Thread.currentThread.setContextClassLoader(loader)
|
Thread.currentThread.setContextClassLoader(loader)
|
||||||
try { eval } finally { Thread.currentThread.setContextClassLoader(oldLoader) }
|
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])
|
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)
|
(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 scalaProvider = scalaProvider0
|
||||||
val id = appID
|
val id = appID
|
||||||
def mainClasspath = app.fullClasspath
|
def mainClasspath = app.fullClasspath
|
||||||
lazy val loader = app.createLoader(scalaProvider.loader)
|
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 } =
|
lazy val entryPoint: Class[T] forSome { type T } =
|
||||||
{
|
{
|
||||||
val c = Class.forName(id.mainClass, true, loader)
|
val c = Class.forName(id.mainClass, true, loader)
|
||||||
if(classOf[xsbti.AppMain].isAssignableFrom(c)) c
|
if(classOf[xsbti.AppMain].isAssignableFrom(c)) c
|
||||||
else if(PlainApplication.isPlainApplication(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"+
|
else if(ServerApplication.isServerApplication(c)) c
|
||||||
" * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)")
|
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.
|
// 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 = {
|
def newMain(): xsbti.AppMain = {
|
||||||
if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint)
|
if(ServerApplication.isServerApplication(entryPoint)) ServerApplication(this)
|
||||||
else mainClass.newInstance
|
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)
|
lazy val components = componentProvider(appHome)
|
||||||
}
|
}
|
||||||
def componentProvider(appHome: File) = new ComponentProvider(appHome, lockBoot)
|
def componentProvider(appHome: File) = new ComponentProvider(appHome, lockBoot)
|
||||||
|
|
|
||||||
|
|
@ -9,27 +9,46 @@ import java.net.URL
|
||||||
import scala.collection.immutable.List
|
import scala.collection.immutable.List
|
||||||
|
|
||||||
//TODO: use copy constructor, check size change
|
//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 = {
|
def getScalaVersion = {
|
||||||
val sv = Value.get(scalaVersion)
|
val sv = Value.get(scalaVersion)
|
||||||
if(sv == "auto") None else Some(sv)
|
if(sv == "auto") None else Some(sv)
|
||||||
}
|
}
|
||||||
|
|
||||||
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, 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)
|
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)
|
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig)
|
||||||
// TODO: withExplicit
|
// TODO: withExplicit
|
||||||
def withVersions(newScalaVersion: String, newAppVersion: String, classifiers0: Classifiers) =
|
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)
|
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)
|
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] {
|
final class Explicit[T](val value: T) extends Value[T] {
|
||||||
override def toString = value.toString
|
override def toString = value.toString
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +149,7 @@ sealed trait PropertyInit
|
||||||
final class SetProperty(val value: String) extends PropertyInit
|
final class SetProperty(val value: String) extends PropertyInit
|
||||||
final class PromptProperty(val label: String, val default: Option[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 log(s: => String, at: LogLevel.Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s)
|
||||||
def debug(s: => String) = log(s, LogLevel.Debug)
|
def debug(s: => String) = log(s, LogLevel.Debug)
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ object Pre
|
||||||
classes.toList.filter(classMissing)
|
classes.toList.filter(classMissing)
|
||||||
}
|
}
|
||||||
def toURLs(files: Array[File]): Array[URL] = files.map(_.toURI.toURL)
|
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)
|
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 isWindows: Boolean = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")
|
||||||
final val isCygwin: Boolean = isWindows && java.lang.Boolean.getBoolean("sbt.cygwin")
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,9 @@ object ResolveValues
|
||||||
def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveValues(conf))()
|
def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveValues(conf))()
|
||||||
private def trim(s: String) = if(s eq null) None else notEmpty(s.trim)
|
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 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)
|
final class ResolveValues(conf: LaunchConfiguration)
|
||||||
{
|
{
|
||||||
private def propertiesFile = conf.boot.properties
|
private def propertiesFile = conf.boot.properties
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,7 +55,7 @@ final class Update(config: UpdateConfiguration)
|
||||||
val optionProps =
|
val optionProps =
|
||||||
Option(System.getProperty("sbt.boot.credentials")) orElse
|
Option(System.getProperty("sbt.boot.credentials")) orElse
|
||||||
Option(System.getenv("SBT_CREDENTIALS")) map ( path =>
|
Option(System.getenv("SBT_CREDENTIALS")) map ( path =>
|
||||||
ResolveValues.readProperties(new File(path))
|
Pre.readProperties(new File(path))
|
||||||
)
|
)
|
||||||
optionProps match {
|
optionProps match {
|
||||||
case Some(props) => extractCredentials("realm","host","user","password")(props)
|
case Some(props) => extractCredentials("realm","host","user","password")(props)
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ Before reading anything in here, you will need the information in the
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
Launcher
|
|
||||||
Scripts
|
Scripts
|
||||||
TaskInputs
|
TaskInputs
|
||||||
Understanding-incremental-recompilation
|
Understanding-incremental-recompilation
|
||||||
|
|
|
||||||
|
|
@ -1,387 +1,5 @@
|
||||||
======================
|
============
|
||||||
Launcher Specification
|
Sbt Launcher
|
||||||
======================
|
============
|
||||||
|
|
||||||
The sbt launcher component is a self-contained jar that boots a Scala
|
This docuemntation has been moved to :doc:`The Launcher section </Launcher/index>`.
|
||||||
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`
|
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,4 @@ Other resources include the :doc:`Examples </Examples/index>` and
|
||||||
Tasks-and-Commands
|
Tasks-and-Commands
|
||||||
Plugins-and-Best-Practices
|
Plugins-and-Best-Practices
|
||||||
Advanced-Index
|
Advanced-Index
|
||||||
|
/Launcher/index
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 |
|
|
@ -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
|
||||||
|
|
@ -22,7 +22,6 @@ the :doc:`index of names and types <Name-Index>`.
|
||||||
Examples/index
|
Examples/index
|
||||||
Name-Index
|
Name-Index
|
||||||
|
|
||||||
|
|
||||||
.. The following includes documents that are not important enough to be in a visible toctree
|
.. The following includes documents that are not important enough to be in a visible toctree
|
||||||
They are linked from other documents, which is enough.
|
They are linked from other documents, which is enough.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue