diff --git a/launch/interface/src/main/java/xsbti/AppMain.java b/launch/interface/src/main/java/xsbti/AppMain.java index ffd1e4c36..b24e02212 100644 --- a/launch/interface/src/main/java/xsbti/AppMain.java +++ b/launch/interface/src/main/java/xsbti/AppMain.java @@ -1,6 +1,25 @@ package xsbti; +/** + * The main entry interface for launching applications. Classes which implement this interface + * can be launched via the sbt launcher. + * + * In addition, classes can be adapted into this interface by the launcher if they have a static method + * matching one of these signatures: + * + * - public static void main(String[] args) + * - public static int main(String[] args) + * - public static xsbti.Exit main(String[] args) + * + */ public interface AppMain { + /** Run the application and return the result. + * + * @param configuration The configuration used to run the application. Includes arguments and access to launcher features. + * @return + * The result of running this app. + * Note: the result can be things like "Please reboot this application". + */ public MainResult run(AppConfiguration configuration); } \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/AppProvider.java b/launch/interface/src/main/java/xsbti/AppProvider.java index 24744c83c..ab3914210 100644 --- a/launch/interface/src/main/java/xsbti/AppProvider.java +++ b/launch/interface/src/main/java/xsbti/AppProvider.java @@ -3,9 +3,10 @@ package xsbti; import java.io.File; /** - * This represents an interface that can generate applications. + * This represents an interface that can generate applications or servers. * - * An application is somethign which will run and return an exit value. + * This provider grants access to launcher related features associated with + * the id. */ public interface AppProvider { @@ -33,6 +34,8 @@ public interface AppProvider * It is NOT guaranteed that newMain().getClass() == mainClass(). * The sbt launcher can wrap generic static main methods. In this case, there will be a wrapper class, * and you must use the `entryPoint` method. + * @throws IncompatibleClassChangeError if the configuration used for this Application does not + * represent a launched application. */ public AppMain newMain(); diff --git a/launch/interface/src/main/java/xsbti/Server.java b/launch/interface/src/main/java/xsbti/Server.java new file mode 100644 index 000000000..d4eca176d --- /dev/null +++ b/launch/interface/src/main/java/xsbti/Server.java @@ -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(); +} \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/ServerMain.java b/launch/interface/src/main/java/xsbti/ServerMain.java new file mode 100644 index 000000000..da3c8ce2b --- /dev/null +++ b/launch/interface/src/main/java/xsbti/ServerMain.java @@ -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); +} \ No newline at end of file diff --git a/launch/src/main/scala/xsbt/boot/Boot.scala b/launch/src/main/scala/xsbt/boot/Boot.scala index 06ee1ba82..665407cff 100644 --- a/launch/src/main/scala/xsbt/boot/Boot.scala +++ b/launch/src/main/scala/xsbt/boot/Boot.scala @@ -5,36 +5,48 @@ import java.io.File + // The entry point to the launcher object Boot { def main(args: Array[String]) { - args match { - case Array("--version") => - println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion) - case _ => - System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM - System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks - System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise - CheckProxy() - run(args) - } + val config = parseArgs(args) + // If we havne't exited, we set up some hooks and launch + System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM + System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks + System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise + CheckProxy() + run(config) } + def parseArgs(args: Array[String]): LauncherArguments = { + @annotation.tailrec + def parse(args: List[String], isLocate: Boolean, remaining: List[String]): LauncherArguments = + args match { + case "--version" :: rest => + println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion) + exit(1) + case "--locate" :: rest => parse(rest, true, remaining) + case next :: rest => parse(rest, isLocate, next :: remaining) + case Nil => new LauncherArguments(remaining.reverse, isLocate) + } + parse(args.toList, false, Nil) + } + // this arrangement is because Scala does not always properly optimize away // the tail recursion in a catch statement - final def run(args: Array[String]): Unit = runImpl(args) match { + final def run(args: LauncherArguments): Unit = runImpl(args) match { case Some(newArgs) => run(newArgs) case None => () } - private def runImpl(args: Array[String]): Option[Array[String]] = + private def runImpl(args: LauncherArguments): Option[LauncherArguments] = try - Launch(args.toList) map exit + Launch(args) map exit catch { case b: BootException => errorAndExit(b.toString) case r: xsbti.RetrieveException => errorAndExit("Error: " + r.getMessage) - case r: xsbti.FullReload => Some(r.arguments) + case r: xsbti.FullReload => Some(new LauncherArguments(r.arguments.toList, false)) case e: Throwable => e.printStackTrace errorAndExit(Pre.prefixError(e.toString)) diff --git a/launch/src/main/scala/xsbt/boot/Configuration.scala b/launch/src/main/scala/xsbt/boot/Configuration.scala index e9464406a..4028e89cf 100644 --- a/launch/src/main/scala/xsbt/boot/Configuration.scala +++ b/launch/src/main/scala/xsbt/boot/Configuration.scala @@ -10,21 +10,34 @@ import java.util.regex.Pattern import scala.collection.immutable.List import annotation.tailrec +object ConfigurationStorageState extends Enumeration { + val PropertiesFile = value("properties-file") + val SerializedFile = value("serialized-file") +} + object Configuration { + import ConfigurationStorageState._ final val SysPropPrefix = "-D" def parse(file: URL, baseDirectory: File) = Using( new InputStreamReader(file.openStream, "utf8") )( (new ConfigurationParser).apply ) - @tailrec def find(args: List[String], baseDirectory: File): (URL, List[String]) = + + /** + * Finds the configuration location. + * + * Note: Configuration may be previously serialized by a launcher. + */ + @tailrec def find(args: List[String], baseDirectory: File): (URL, List[String], ConfigurationStorageState.Value) = args match { - case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail) + case head :: tail if head.startsWith("@load:") => (directConfiguration(head.substring(6), baseDirectory), tail, SerializedFile) + case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail, PropertiesFile) case head :: tail if head.startsWith(SysPropPrefix) => setProperty(head stripPrefix SysPropPrefix) find(tail, baseDirectory) case _ => val propertyConfigured = System.getProperty("sbt.boot.properties") val url = if(propertyConfigured == null) configurationOnClasspath else configurationFromFile(propertyConfigured, baseDirectory) - (url , args) + (url, args, PropertiesFile) } def setProperty(head: String) { @@ -108,7 +121,7 @@ object Configuration // We have to hard code them here in order to use them to determine the location of sbt.boot.properties itself def guessSbtVersion: Option[String] = { - val props = ResolveValues.readProperties(new File(DefaultBuildProperties)) + val props = Pre.readProperties(new File(DefaultBuildProperties)) Option(props.getProperty(SbtVersionProperty)) } diff --git a/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala b/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala index 8b1252e4a..659573550 100644 --- a/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala +++ b/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala @@ -78,11 +78,14 @@ class ConfigurationParser val (logging, m5) = processSection(m4, "log", getLogging) val (properties, m6) = processSection(m5, "app-properties", getAppProperties) val ((ivyHome, checksums, isOverrideRepos, rConfigFile), m7) = processSection(m6, "ivy", getIvy) - check(m7, "section") + val (serverOptions, m8) = processSection(m7, "server", getServer) + check(m8, "section") val classifiers = Classifiers(scalaClassifiers, appClassifiers) val repositories = rConfigFile map readRepositoriesConfig getOrElse defaultRepositories val ivyOptions = IvyOptions(ivyHome, classifiers, repositories, checksums, isOverrideRepos) - new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties) + + // TODO - Read server properties... + new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties, serverOptions) } def getScala(m: LabelMap) = { @@ -178,6 +181,16 @@ class ConfigurationParser val app = new Application(org, name, rev, main, components, LaunchCrossVersion(crossVersioned), classpathExtra) (app, classifiers) } + def getServer(m: LabelMap): (Option[ServerConfiguration]) = + { + val (lock, m1) = optfile(m, "lock") + // TODO - JVM args + val (args, m2) = optfile(m1, "jvmargs") + val (props, m3) = optfile(m2, "jvmprops") + lock map { file => + ServerConfiguration(file, args, props) + } + } def getRepositories(m: LabelMap): List[Repository.Repository] = { import Repository.{Ivy, Maven, Predefined} diff --git a/launch/src/main/scala/xsbt/boot/Create.scala b/launch/src/main/scala/xsbt/boot/Create.scala index b22cd2324..17e549781 100644 --- a/launch/src/main/scala/xsbt/boot/Create.scala +++ b/launch/src/main/scala/xsbt/boot/Create.scala @@ -33,17 +33,11 @@ object Initialize def fill(file: File, spec: List[AppProperty]): Unit = process(file, spec, selectFill) def process(file: File, appProperties: List[AppProperty], select: AppProperty => Option[PropertyInit]) { - val properties = new Properties - if(file.exists) - Using(new FileInputStream(file))( properties.load ) + val properties = readProperties(file) val uninitialized = for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null) yield initialize(properties, property.name, init) - if(!uninitialized.isEmpty) - { - file.getParentFile.mkdirs() - Using(new FileOutputStream(file))( out => properties.store(out, "") ) - } + if(!uninitialized.isEmpty) writeProperties(properties, file, "") } def initialize(properties: Properties, name: String, init: PropertyInit) { diff --git a/launch/src/main/scala/xsbt/boot/Enumeration.scala b/launch/src/main/scala/xsbt/boot/Enumeration.scala index 3e5a6f89d..e65309f2a 100644 --- a/launch/src/main/scala/xsbt/boot/Enumeration.scala +++ b/launch/src/main/scala/xsbt/boot/Enumeration.scala @@ -6,7 +6,7 @@ package xsbt.boot import Pre._ import scala.collection.immutable.List -class Enumeration +class Enumeration extends Serializable { def elements: List[Value] = members private lazy val members: List[Value] = @@ -25,6 +25,6 @@ class Enumeration } def value(s: String) = new Value(s, 0) def value(s: String, i: Int) = new Value(s, i) - final class Value(override val toString: String, val id: Int) + final class Value(override val toString: String, val id: Int) extends Serializable def toValue(s: String): Value = elements.find(_.toString == s).getOrElse(error("Expected one of " + elements.mkString(",") + " (got: " + s + ")")) } \ No newline at end of file diff --git a/launch/src/main/scala/xsbt/boot/Launch.scala b/launch/src/main/scala/xsbt/boot/Launch.scala index 4604ce2ef..f27441918 100644 --- a/launch/src/main/scala/xsbt/boot/Launch.scala +++ b/launch/src/main/scala/xsbt/boot/Launch.scala @@ -6,20 +6,64 @@ package xsbt.boot import Pre._ import BootConfiguration.{CompilerModuleName, JAnsiVersion, LibraryModuleName} import java.io.File -import java.net.{URL, URLClassLoader} +import java.net.{URL, URLClassLoader, URI} import java.util.concurrent.Callable import scala.collection.immutable.List import scala.annotation.tailrec +import ConfigurationStorageState._ + +class LauncherArguments(val args: List[String], val isLocate: Boolean) object Launch { - def apply(arguments: List[String]): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments ) + def apply(arguments: LauncherArguments): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments ) - def apply(currentDirectory: File, arguments: List[String]): Option[Int] = { - val (configLocation, newArguments) = Configuration.find(arguments, currentDirectory) - val config = parseAndInitializeConfig(configLocation, currentDirectory) - launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArguments)) + def apply(currentDirectory: File, arguments: LauncherArguments): Option[Int] = { + val (configLocation, newArgs2, state) = Configuration.find(arguments.args, currentDirectory) + val config = state match { + case SerializedFile => LaunchConfiguration.restore(configLocation) + case PropertiesFile => parseAndInitializeConfig(configLocation, currentDirectory) + } + if(arguments.isLocate) { + if(!newArgs2.isEmpty) { + // TODO - Print the arguments without exploding proguard size. + System.err.println("Warning: --locate option ignores arguments.") + } + locate(currentDirectory, config) + } else { + // First check to see if there are java system properties we need to set. Then launch the application. + updateProperties(config) + launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArgs2)) + } } + /** Locate a server, print where it is, and exit. */ + def locate(currentDirectory: File, config: LaunchConfiguration): Option[Int] = { + config.serverConfig match { + case Some(_) => + val uri = ServerLocator.locate(currentDirectory, config) + System.out.println(uri.toASCIIString) + Some(0) + case None => sys.error(s"${config.app.groupID}-${config.app.main} is not configured as a server.") + } + } + /** Some hackery to allow sys.props to be configured via a file. If this launch config has + * a valid file configured, we load the properties and and apply them to this jvm. + */ + def updateProperties(config: LaunchConfiguration): Unit = { + config.serverConfig match { + case Some(config) => + config.jvmPropsFile match { + case Some(file) if file.exists => + try setSystemProperties(readProperties(file)) + catch { + case e: Exception => throw new RuntimeException(s"Unable to load server properties file: ${file}", e) + } + case _ => + } + case None => + } + } + /** Parses the configuration *and* runs the initialization code that will remove variable references. */ def parseAndInitializeConfig(configLocation: URL, currentDirectory: File): LaunchConfiguration = { @@ -84,6 +128,10 @@ object Launch Thread.currentThread.setContextClassLoader(loader) try { eval } finally { Thread.currentThread.setContextClassLoader(oldLoader) } } + + // Cache of classes for lookup later. + val ServerMainClass = classOf[xsbti.ServerMain] + val AppMainClass = classOf[xsbti.AppMain] } final class RunConfiguration(val scalaVersion: Option[String], val app: xsbti.ApplicationID, val workingDirectory: File, val arguments: List[String]) @@ -240,27 +288,32 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i (scalaHome, libDirectory) } - def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider = new xsbti.AppProvider - { + def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider = + new xsbti.AppProvider { + import Launch.{ServerMainClass,AppMainClass} val scalaProvider = scalaProvider0 val id = appID def mainClasspath = app.fullClasspath lazy val loader = app.createLoader(scalaProvider.loader) + // TODO - For some reason we can't call this from vanilla scala. We get a + // no such method exception UNLESS we're in the same project. lazy val entryPoint: Class[T] forSome { type T } = { val c = Class.forName(id.mainClass, true, loader) if(classOf[xsbti.AppMain].isAssignableFrom(c)) c else if(PlainApplication.isPlainApplication(c)) c - else sys.error(s"Class: ${c} is not an instance of xsbti.AppMain nor does it have one of these static methods:\n"+ - " * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)") + else if(ServerApplication.isServerApplication(c)) c + else sys.error(s"${c} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have one of these static methods:\n"+ + " * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)\n") } // Deprecated API. Remove when we can. - def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(classOf[xsbti.AppMain]) + def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(AppMainClass) def newMain(): xsbti.AppMain = { - if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint) - else mainClass.newInstance + if(ServerApplication.isServerApplication(entryPoint)) ServerApplication(this) + else if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint) + else if(AppMainClass.isAssignableFrom(entryPoint)) mainClass.newInstance + else throw new IncompatibleClassChangeError(s"Main class ${entryPoint.getName} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have a valid `main` method.") } - lazy val components = componentProvider(appHome) } def componentProvider(appHome: File) = new ComponentProvider(appHome, lockBoot) diff --git a/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala b/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala index f8ccd1782..be1f0fc4a 100644 --- a/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala +++ b/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala @@ -9,27 +9,46 @@ import java.net.URL import scala.collection.immutable.List //TODO: use copy constructor, check size change -final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty]) +final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty], serverConfig: Option[ServerConfiguration]) { + def isServer: Boolean = serverConfig.isDefined def getScalaVersion = { val sv = Value.get(scalaVersion) if(sv == "auto") None else Some(sv) } - def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties) - def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties) - def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties) + def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties, serverConfig) + def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties, serverConfig) + def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig) // TODO: withExplicit def withVersions(newScalaVersion: String, newAppVersion: String, classifiers0: Classifiers) = - LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties) + LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig) - def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties) + def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties, serverConfig.map(_ map f)) +} +object LaunchConfiguration { + // Saves a launch configuration into a file. This is only safe if it is loaded by the *same* launcher version. + def save(config: LaunchConfiguration, f: File): Unit = { + val out = new java.io.ObjectOutputStream(new java.io.FileOutputStream(f)) + try out.writeObject(config) + finally out.close() + } + // Restores a launch configuration from a file. This is only safe if it is loaded by the *same* launcher version. + def restore(url: URL): LaunchConfiguration = { + val in = new java.io.ObjectInputStream(url.openConnection.getInputStream) + try in.readObject.asInstanceOf[LaunchConfiguration] + finally in.close() + } +} +final case class ServerConfiguration(lockFile: File, jvmArgs: Option[File], jvmPropsFile: Option[File]) { + def map(f: File => File) = + ServerConfiguration(f(lockFile), jvmArgs map f, jvmPropsFile map f) } final case class IvyOptions(ivyHome: Option[File], classifiers: Classifiers, repositories: List[Repository.Repository], checksums: List[String], isOverrideRepositories: Boolean) { def map(f: File => File) = IvyOptions(ivyHome.map(f), classifiers, repositories, checksums, isOverrideRepositories) } -sealed trait Value[T] +sealed trait Value[T] extends Serializable final class Explicit[T](val value: T) extends Value[T] { override def toString = value.toString } @@ -130,7 +149,7 @@ sealed trait PropertyInit final class SetProperty(val value: String) extends PropertyInit final class PromptProperty(val label: String, val default: Option[String]) extends PropertyInit -final class Logging(level: LogLevel.Value) +final class Logging(level: LogLevel.Value) extends Serializable { def log(s: => String, at: LogLevel.Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s) def debug(s: => String) = log(s, LogLevel.Debug) diff --git a/launch/src/main/scala/xsbt/boot/Pre.scala b/launch/src/main/scala/xsbt/boot/Pre.scala index 05a9585d1..26b83aee9 100644 --- a/launch/src/main/scala/xsbt/boot/Pre.scala +++ b/launch/src/main/scala/xsbt/boot/Pre.scala @@ -70,6 +70,10 @@ object Pre classes.toList.filter(classMissing) } def toURLs(files: Array[File]): Array[URL] = files.map(_.toURI.toURL) + def toFile(url: URL): File = + try { new File(url.toURI) } + catch { case _: java.net.URISyntaxException => new File(url.getPath) } + def delete(f: File) { @@ -82,4 +86,25 @@ object Pre } final val isWindows: Boolean = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows") final val isCygwin: Boolean = isWindows && java.lang.Boolean.getBoolean("sbt.cygwin") + + import java.util.Properties + import java.io.{FileInputStream,FileOutputStream} + private[boot] def readProperties(propertiesFile: File) = + { + val properties = new Properties + if(propertiesFile.exists) + Using( new FileInputStream(propertiesFile) )( properties.load ) + properties + } + private[boot] def writeProperties(properties: Properties, file: File, msg: String): Unit = { + file.getParentFile.mkdirs() + Using(new FileOutputStream(file))( out => properties.store(out, msg) ) + } + private[boot] def setSystemProperties(properties: Properties): Unit = { + val nameItr = properties.stringPropertyNames.iterator + while(nameItr.hasNext) { + val propName = nameItr.next + System.setProperty(propName, properties.getProperty(propName)) + } + } } diff --git a/launch/src/main/scala/xsbt/boot/ResolveValues.scala b/launch/src/main/scala/xsbt/boot/ResolveValues.scala index b04cdb949..952d9d970 100644 --- a/launch/src/main/scala/xsbt/boot/ResolveValues.scala +++ b/launch/src/main/scala/xsbt/boot/ResolveValues.scala @@ -12,16 +12,9 @@ object ResolveValues def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveValues(conf))() private def trim(s: String) = if(s eq null) None else notEmpty(s.trim) private def notEmpty(s: String) = if(isEmpty(s)) None else Some(s) - private[boot] def readProperties(propertiesFile: File) = - { - val properties = new Properties - if(propertiesFile.exists) - Using( new FileInputStream(propertiesFile) )( properties.load ) - properties - } } -import ResolveValues.{readProperties, trim} +import ResolveValues.{trim} final class ResolveValues(conf: LaunchConfiguration) { private def propertiesFile = conf.boot.properties diff --git a/launch/src/main/scala/xsbt/boot/ServerApplication.scala b/launch/src/main/scala/xsbt/boot/ServerApplication.scala new file mode 100644 index 000000000..3f592b151 --- /dev/null +++ b/launch/src/main/scala/xsbt/boot/ServerApplication.scala @@ -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) + } +} \ No newline at end of file diff --git a/launch/src/main/scala/xsbt/boot/Update.scala b/launch/src/main/scala/xsbt/boot/Update.scala index 92e1bec92..8d1c2e206 100644 --- a/launch/src/main/scala/xsbt/boot/Update.scala +++ b/launch/src/main/scala/xsbt/boot/Update.scala @@ -55,7 +55,7 @@ final class Update(config: UpdateConfiguration) val optionProps = Option(System.getProperty("sbt.boot.credentials")) orElse Option(System.getenv("SBT_CREDENTIALS")) map ( path => - ResolveValues.readProperties(new File(path)) + Pre.readProperties(new File(path)) ) optionProps match { case Some(props) => extractCredentials("realm","host","user","password")(props) diff --git a/launch/src/test/scala/ServerLocatorTest.scala b/launch/src/test/scala/ServerLocatorTest.scala new file mode 100644 index 000000000..7e0b30c36 --- /dev/null +++ b/launch/src/test/scala/ServerLocatorTest.scala @@ -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)) + } + } +} \ No newline at end of file diff --git a/launch/test-sample/src/main/scala/xsbt/boot/test/Servers.scala b/launch/test-sample/src/main/scala/xsbt/boot/test/Servers.scala new file mode 100644 index 000000000..930e565a9 --- /dev/null +++ b/launch/test-sample/src/main/scala/xsbt/boot/test/Servers.scala @@ -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 + } + + +} \ No newline at end of file diff --git a/src/sphinx/Detailed-Topics/Advanced-Index.rst b/src/sphinx/Detailed-Topics/Advanced-Index.rst index 28928ed7e..884a96292 100644 --- a/src/sphinx/Detailed-Topics/Advanced-Index.rst +++ b/src/sphinx/Detailed-Topics/Advanced-Index.rst @@ -9,7 +9,6 @@ Before reading anything in here, you will need the information in the .. toctree:: :maxdepth: 2 - Launcher Scripts TaskInputs Understanding-incremental-recompilation diff --git a/src/sphinx/Detailed-Topics/Launcher.rst b/src/sphinx/Detailed-Topics/Launcher.rst index 6573f2348..eced0102b 100644 --- a/src/sphinx/Detailed-Topics/Launcher.rst +++ b/src/sphinx/Detailed-Topics/Launcher.rst @@ -1,387 +1,5 @@ -====================== -Launcher Specification -====================== +============ +Sbt Launcher +============ -The sbt launcher component is a self-contained jar that boots a Scala -application without Scala or the application already existing on the -system. The only prerequisites are the launcher jar itself, an optional -configuration file, and a java runtime version 1.6 or greater. - -Overview -======== - -A user downloads the launcher jar and creates a script to run it. In -this documentation, the script will be assumed to be called `launch`. -For unix, the script would look like: -`java -jar sbt-launcher.jar "$@"` - -The user then downloads the configuration file for the application (call -it `my.app.configuration`) and creates a script to launch it (call it -`myapp`): `launch @my.app.configuration "$@"` - -The user can then launch the application using `myapp arg1 arg2 ...` - -Like the launcher used to distribute `sbt`, the downloaded launcher -jar will retrieve Scala and the application according to the provided -configuration file. The versions may be fixed or read from a different -configuration file (the location of which is also configurable). The -location to which the Scala and application jars are downloaded is -configurable as well. The repositories searched are configurable. -Optional initialization of a properties file on launch is configurable. - -Once the launcher has downloaded the necessary jars, it loads the -application and calls its entry point. The application is passed -information about how it was called: command line arguments, current -working directory, Scala version, and application ID (organization, -name, version). In addition, the application can ask the launcher to -perform operations such as obtaining the Scala jars and a -`ClassLoader` for any version of Scala retrievable from the -repositories specified in the configuration file. It can request that -other applications be downloaded and run. When the application -completes, it can tell the launcher to exit with a specific exit code or -to reload the application with a different version of Scala, a different -version of the application, or different arguments. - -There are some other options for setup, such as putting the -configuration file inside the launcher jar and distributing that as a -single download. The rest of this documentation describes the details of -configuring, writing, distributing, and running the application. - -Configuration -------------- - -The launcher may be configured in one of the following ways in -increasing order of precedence: - -- Replace the `/sbt/sbt.boot.properties` file in the jar -- Put a configuration file named `sbt.boot.properties` on the - classpath. Put it in the classpath root without the `/sbt` prefix. -- Specify the location of an alternate configuration on the command - line, either as a path or an absolute URI. This can be done by - either specifying the location as the system property - `sbt.boot.properties` or as the first argument to the launcher - prefixed by `'@'`. The system property has lower precedence. - Resolution of a relative path is first attempted against the current - working directory, then against the user's home directory, and then - against the directory containing the launcher jar. An error is - generated if none of these attempts succeed. - -Syntax -~~~~~~ - -The configuration file is line-based, read as UTF-8 encoded, and defined -by the following grammar. `'nl'` is a newline or end of file and -`'text'` is plain text without newlines or the surrounding delimiters -(such as parentheses or square brackets): - -.. productionlist:: - configuration: `scala` `app` `repositories` `boot` `log` `appProperties` - scala: "[" "scala" "]" `nl` `version` `nl` `classifiers` `nl` - app: "[" "app" "]" `nl` `org` `nl` `name` `nl` `version` `nl` `components` `nl` `class` `nl` `crossVersioned` `nl` `resources` `nl` `classifiers` `nl` - repositories: "[" "repositories" "]" `nl` (`repository` `nl`)* - boot: "[" "boot" "]" `nl` `directory` `nl` `bootProperties` `nl` `search` `nl` `promptCreate` `nl` `promptFill` `nl` `quickOption` `nl` - log: "["' "log" "]" `nl` `logLevel` `nl` - appProperties: "[" "app-properties" "]" nl (property nl)* - ivy: "[" "ivy" "]" `nl` `homeDirectory` `nl` `checksums` `nl` `overrideRepos` `nl` `repoConfig` `nl` - directory: "directory" ":" `path` - bootProperties: "properties" ":" `path` - search: "search" ":" ("none" | "nearest" | "root-first" | "only" ) ("," `path`)* - logLevel: "level" ":" ("debug" | "info" | "warn" | "error") - promptCreate: "prompt-create" ":" `label` - promptFill: "prompt-fill" ":" `boolean` - quickOption: "quick-option" ":" `boolean` - version: "version" ":" `versionSpecification` - versionSpecification: `readProperty` | `fixedVersion` - readProperty: "read" "(" `propertyName` ")" "[" `default` "]" - fixedVersion: text - classifiers: "classifiers" ":" text ("," text)* - homeDirectory: "ivy-home" ":" `path` - checksums: "checksums" ":" `checksum` ("," `checksum`)* - overrideRepos: "override-build-repos" ":" `boolean` - repoConfig: "repository-config" ":" `path` - org: "org" ":" text - name: "name" ":" text - class: "class" ":" text - components: "components" ":" `component` ("," `component`)* - crossVersioned: "cross-versioned" ":" ("true" | "false" | "none" | "binary" | "full") - resources: "resources" ":" `path` ("," `path`)* - repository: ( `predefinedRepository` | `customRepository` ) `nl` - predefinedRepository: "local" | "maven-local" | "maven-central" - customRepository: `label` ":" `url` [ ["," `ivyPattern`] ["," `artifactPattern`] [", mavenCompatible"] [", bootOnly"]] - property: `label` ":" `propertyDefinition` ("," `propertyDefinition`)* - propertyDefinition: `mode` "=" (`set` | `prompt`) - mode: "quick" | "new" | "fill" - set: "set" "(" value ")" - prompt: "prompt" "(" `label` ")" ("[" `default` "]")? - boolean: "true" | "false" - nl: "\r\n" | "\n" | "\r" - path: text - propertyName: text - label: text - default: text - checksum: text - ivyPattern: text - artifactPattern: text - url: text - component: text - -In addition to the grammar specified here, property values may include -variable substitutions. A variable substitution has one of these forms: - -- `${variable.name}` -- `${variable.name-default}` - -where `variable.name` is the name of a system property. If a system -property by that name exists, the value is substituted. If it does not -exists and a default is specified, the default is substituted after -recursively substituting variables in it. If the system property does -not exist and no default is specified, the original string is not -substituted. - -Example -~~~~~~~ - -The default configuration file for sbt looks like: - -.. parsed-literal:: - - [scala] - version: ${sbt.scala.version-auto} - - [app] - org: ${sbt.organization-org.scala-sbt} - name: sbt - version: ${sbt.version-read(sbt.version)[\ |release|\ ]} - class: ${sbt.main.class-sbt.xMain} - components: xsbti,extra - cross-versioned: ${sbt.cross.versioned-false} - - [repositories] - local - typesafe-ivy-releases: http://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly - maven-central - sonatype-snapshots: https://oss.sonatype.org/content/repositories/snapshots - - [boot] - directory: ${sbt.boot.directory-${sbt.global.base-${user.home}/.sbt}/boot/} - - [ivy] - ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/} - checksums: ${sbt.checksums-sha1,md5} - override-build-repos: ${sbt.override.build.repos-false} - repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories} - -Semantics -~~~~~~~~~ - -The `scala.version` property specifies the version of Scala used to -run the application. If the application is not cross-built, this may be -set to `auto` and it will be auto-detected from the application's -dependencies. If specified, the `scala.classifiers` property defines -classifiers, such as 'sources', of extra Scala artifacts to retrieve. - -The `app.org`, `app.name`, and `app.version` properties specify -the organization, module ID, and version of the application, -respectively. These are used to resolve and retrieve the application -from the repositories listed in `[repositories]`. If -`app.cross-versioned` is `binary`, the resolved module ID is -`{app.name+'_'+CrossVersion.binaryScalaVersion(scala.version)}`. -If `app.cross-versioned` is `true` or `full`, the resolved module ID is -`{app.name+'_'+scala.version}`. The `scala.version` property must be -specified and cannot be `auto` when cross-versioned. The paths given -in `app.resources` are added to the application's classpath. If the -path is relative, it is resolved against the application's working -directory. If specified, the `app.classifiers` property defines -classifiers, like 'sources', of extra artifacts to retrieve for the -application. - -Jars are retrieved to the directory given by `boot.directory`. By -default, this is an absolute path that is shared by all launched -instances on the machine. If multiple versions access it simultaneously. -, you might see messages like: - -.. code-block:: console - - Waiting for lock on to be available... - -This boot directory may be relative to the current directory instead. In -this case, the launched application will have a separate boot directory -for each directory it is launched in. - -The `boot.properties` property specifies the location of the -properties file to use if `app.version` or `scala.version` is -specified as `read`. The `prompt-create`, `prompt-fill`, and -`quick-option` properties together with the property definitions in -`[app.properties]` can be used to initialize the `boot.properties` -file. - -The app.class property specifies the name of the entry point to the -application. An application entry point must be a public class with a -no-argument constructor that implements `xsbti.AppMain`. The -`AppMain` interface specifies the entry method signature 'run'. The -run method is passed an instance of AppConfiguration, which provides -access to the startup environment. `AppConfiguration` also provides an -interface to retrieve other versions of Scala or other applications. -Finally, the return type of the run method is `xsbti.MainResult`, -which has two subtypes: `xsbti.Reboot` and `xsbti.Exit`. To exit -with a specific code, return an instance of `xsbti.Exit` with the -requested code. To restart the application, return an instance of -Reboot. You can change some aspects of the configuration with a reboot, -such as the version of Scala, the application ID, and the arguments. - -The `ivy.cache-directory` property provides an alternative location -for the Ivy cache used by the launcher. This does not automatically set -the Ivy cache for the application, but the application is provided this -location through the AppConfiguration instance. The `checksums` -property selects the checksum algorithms (sha1 or md5) that are used to -verify artifacts downloaded by the launcher. `override-build-repos` is -a flag that can inform the application that the repositories configured -for the launcher should be used in the application. If -`repository-config` is defined, the file it specifies should contain a -`[repositories]` section that is used in place of the section in the -original configuration file. - -Execution ---------- - -On startup, the launcher searches for its configuration in the order -described in the Configuration section and then parses it. If either the -Scala version or the application version are specified as 'read', the -launcher determines them in the following manner. The file given by the -'boot.properties' property is read as a Java properties file to obtain -the version. The expected property names are `${app.name}.version` for -the application version (where `${app.name}` is replaced with the -value of the `app.name` property from the boot configuration file) and -`scala.version` for the Scala version. If the properties file does not -exist, the default value provided is used. If no default was provided, -an error is generated. - -Once the final configuration is resolved, the launcher proceeds to -obtain the necessary jars to launch the application. The -`boot.directory` property is used as a base directory to retrieve jars -to. Locking is done on the directory, so it can be shared system-wide. -The launcher retrieves the requested version of Scala to - -.. code-block:: console - - ${boot.directory}/${scala.version}/lib/ - -If this directory already exists, the launcher takes a shortcut for -startup performance and assumes that the jars have already been -downloaded. If the directory does not exist, the launcher uses Apache -Ivy to resolve and retrieve the jars. A similar process occurs for the -application itself. It and its dependencies are retrieved to - -.. code-block:: console - - ${boot.directory}/${scala.version}/${app.org}/${app.name}/. - -Once all required code is downloaded, the class loaders are set up. The -launcher creates a class loader for the requested version of Scala. It -then creates a child class loader containing the jars for the requested -'app.components' and with the paths specified in `app.resources`. An -application that does not use components will have all of its jars in -this class loader. - -The main class for the application is then instantiated. It must be a -public class with a public no-argument constructor and must conform to -xsbti.AppMain. The `run` method is invoked and execution passes to the -application. The argument to the 'run' method provides configuration -information and a callback to obtain a class loader for any version of -Scala that can be obtained from a repository in [repositories]. The -return value of the run method determines what is done after the -application executes. It can specify that the launcher should restart -the application or that it should exit with the provided exit code. - -Creating a Launched Application -------------------------------- - -This section shows how to make an application that is launched by this -launcher. First, declare a dependency on the launcher-interface. Do not -declare a dependency on the launcher itself. The launcher interface -consists strictly of Java interfaces in order to avoid binary -incompatibility between the version of Scala used to compile the -launcher and the version used to compile your application. The launcher -interface class will be provided by the launcher, so it is only a -compile-time dependency. If you are building with sbt, your dependency -definition would be: - -.. parsed-literal:: - - libraryDependencies += "org.scala-sbt" % "launcher-interface" % "|release|" % "provided" - - resolvers += sbtResolver.value - -Make the entry point to your class implement 'xsbti.AppMain'. An example -that uses some of the information: - -.. code-block:: scala - - package xsbt.test - class Main extends xsbti.AppMain - { - def run(configuration: xsbti.AppConfiguration) = - { - // get the version of Scala used to launch the application - val scalaVersion = configuration.provider.scalaProvider.version - - // Print a message and the arguments to the application - println("Hello world! Running Scala " + scalaVersion) - configuration.arguments.foreach(println) - - // demonstrate the ability to reboot the application into different versions of Scala - // and how to return the code to exit with - scalaVersion match - { - case "2.9.3" => - new xsbti.Reboot { - def arguments = configuration.arguments - def baseDirectory = configuration.baseDirectory - def scalaVersion = "2.10.2 - def app = configuration.provider.id - } - case "2.10.2" => new Exit(1) - case _ => new Exit(0) - } - } - class Exit(val code: Int) extends xsbti.Exit - } - -Next, define a configuration file for the launcher. For the above class, -it might look like: - -.. parsed-literal:: - - [scala] - version: |scalaRelease| - [app] - org: org.scala-sbt - name: xsbt-test - version: |release| - class: xsbt.test.Main - cross-versioned: binary - [repositories] - local - maven-central - [boot] - directory: ${user.home}/.myapp/boot - -Then, `publishLocal` or `+publishLocal` the application to make it -available. - -Running an Application ----------------------- - -As mentioned above, there are a few options to actually run the -application. The first involves providing a modified jar for download. -The second two require providing a configuration file for download. - -- Replace the /sbt/sbt.boot.properties file in the launcher jar and - distribute the modified jar. The user would need a script to run - `java -jar your-launcher.jar arg1 arg2 ...`. -- The user downloads the launcher jar and you provide the configuration - file. - - - The user needs to run `java -Dsbt.boot.properties=your.boot.properties -jar launcher.jar`. - - The user already has a script to run the launcher (call it - 'launch'). The user needs to run `launch @your.boot.properties your-arg-1 your-arg-2` +This docuemntation has been moved to :doc:`The Launcher section `. diff --git a/src/sphinx/Detailed-Topics/index.rst b/src/sphinx/Detailed-Topics/index.rst index f7bebe4fc..7b551dd7d 100644 --- a/src/sphinx/Detailed-Topics/index.rst +++ b/src/sphinx/Detailed-Topics/index.rst @@ -19,3 +19,4 @@ Other resources include the :doc:`Examples ` and Tasks-and-Commands Plugins-and-Best-Practices Advanced-Index + /Launcher/index diff --git a/src/sphinx/Launcher/Architecture.rst b/src/sphinx/Launcher/Architecture.rst new file mode 100644 index 000000000..2e62f84b7 --- /dev/null +++ b/src/sphinx/Launcher/Architecture.rst @@ -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_2.10.3/ + lib/ + + /// + / + +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`. diff --git a/src/sphinx/Launcher/Configuration.rst b/src/sphinx/Launcher/Configuration.rst new file mode 100644 index 000000000..b110d5411 --- /dev/null +++ b/src/sphinx/Launcher/Configuration.rst @@ -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 diff --git a/src/sphinx/Launcher/GettingStarted.rst b/src/sphinx/Launcher/GettingStarted.rst new file mode 100644 index 000000000..66b8f6494 --- /dev/null +++ b/src/sphinx/Launcher/GettingStarted.rst @@ -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 ` + + +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 ` + +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. diff --git a/src/sphinx/Launcher/classloaders.png b/src/sphinx/Launcher/classloaders.png new file mode 100644 index 000000000..6f0c1b003 Binary files /dev/null and b/src/sphinx/Launcher/classloaders.png differ diff --git a/src/sphinx/Launcher/index.rst b/src/sphinx/Launcher/index.rst new file mode 100644 index 000000000..fcc5802e9 --- /dev/null +++ b/src/sphinx/Launcher/index.rst @@ -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 \ No newline at end of file diff --git a/src/sphinx/index.rst b/src/sphinx/index.rst index 3169538b7..8a07fbe72 100644 --- a/src/sphinx/index.rst +++ b/src/sphinx/index.rst @@ -22,7 +22,6 @@ the :doc:`index of names and types `. Examples/index Name-Index - .. The following includes documents that are not important enough to be in a visible toctree They are linked from other documents, which is enough.