|
Play Framework/Scala example source code file (PlayRun.scala)
The PlayRun.scala Play Framework example source code
/*
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
*/
package play
import java.io.Closeable
import com.typesafe.sbt.web.SbtWeb
import sbt._
import Keys._
import play.PlayImport._
import PlayKeys._
import play.sbtplugin.Colors
import play.core.{ BuildLink, BuildDocHandler }
import play.core.classloader._
import annotation.tailrec
import scala.collection.JavaConverters._
import java.net.URLClassLoader
import java.util.jar.JarFile
import com.typesafe.sbt.SbtNativePackager._
import com.typesafe.sbt.packager.Keys._
import com.typesafe.sbt.web.SbtWeb.autoImport._
import play.sbtplugin.run.AssetsClassLoader
/**
* Provides mechanisms for running a Play application in SBT
*/
trait PlayRun extends PlayInternalKeys {
this: PlayReloader =>
/**
* Configuration for the Play docs application's dependencies. Used to build a classloader for
* that application. Hidden so that it isn't exposed when the user application is published.
*/
val DocsApplication = config("docs").hide
// Regex to match Java System Properties of the format -Dfoo=bar
private val SystemProperty = "-D([^=]+)=(.*)".r
/**
* Take all the options in javaOptions of the format "-Dfoo=bar" and return them as a Seq of key value pairs of the format ("foo" -> "bar")
*/
private def extractSystemProperties(javaOptions: Seq[String]): Seq[(String, String)] = {
javaOptions.collect { case SystemProperty(key, value) => key -> value }
}
private def parsePort(portString: String): Int = {
try {
Integer.parseInt(portString)
} catch {
case e: NumberFormatException => sys.error("Invalid port argument: " + portString)
}
}
private def filterArgs(args: Seq[String], defaultHttpPort: Int): (Seq[(String, String)], Option[Int], Option[Int]) = {
val (properties, others) = args.span(_.startsWith("-D"))
val javaProperties = properties.map(_.drop(2).split('=')).map(a => a(0) -> a(1)).toSeq
// collect arguments plus config file property if present
val httpPort = Option(System.getProperty("http.port"))
val httpsPort = Option(System.getProperty("https.port"))
//port can be defined as a numeric argument or as disabled, -Dhttp.port argument or a generic sys property
val maybePort = others.headOption.orElse(javaProperties.toMap.get("http.port")).orElse(httpPort)
val maybeHttpsPort = javaProperties.toMap.get("https.port").orElse(httpsPort).map(parsePort)
if (maybePort.exists(_ == "disabled")) (javaProperties, Option.empty[Int], maybeHttpsPort)
else (javaProperties, maybePort.map(parsePort).orElse(Some(defaultHttpPort)), maybeHttpsPort)
}
val createURLClassLoader: ClassLoaderCreator = (name, urls, parent) => new java.net.URLClassLoader(urls, parent) {
override def toString = name + "{" + getURLs.map(_.toString).mkString(", ") + "}"
}
val createDelegatedResourcesClassLoader: ClassLoaderCreator = (name, urls, parent) => new java.net.URLClassLoader(urls, parent) {
require(parent ne null)
override def getResources(name: String): java.util.Enumeration[java.net.URL] = getParent.getResources(name)
override def toString = name + "{" + getURLs.map(_.toString).mkString(", ") + "}"
}
val playDefaultRunTask = playRunTask(playRunHooks, playDependencyClasspath, playDependencyClassLoader,
playReloaderClasspath, playReloaderClassLoader, playAssetsClassLoader)
/**
* This method is public API, used by sbt-echo, which is used by Activator:
*
* https://github.com/typesafehub/sbt-echo/blob/v0.1.3/play/src/main/scala-sbt-0.13/com/typesafe/sbt/echo/EchoPlaySpecific.scala#L20
*
* Do not change its signature without first consulting the Activator team. Do not change its signature in a minor
* release.
*/
def playRunTask(runHooks: TaskKey[Seq[play.PlayRunHook]],
dependencyClasspath: TaskKey[Classpath], dependencyClassLoader: TaskKey[ClassLoaderCreator],
reloaderClasspath: TaskKey[Classpath], reloaderClassLoader: TaskKey[ClassLoaderCreator],
assetsClassLoader: TaskKey[ClassLoader => ClassLoader]): Def.Initialize[InputTask[Unit]] = Def.inputTask {
val args = Def.spaceDelimited().parsed
val state = Keys.state.value
val interaction = playInteractionMode.value
lazy val devModeServer = startDevMode(
state,
runHooks.value,
(javaOptions in Runtime).value,
dependencyClasspath.value,
dependencyClassLoader.value,
reloaderClasspath,
reloaderClassLoader.value,
assetsClassLoader.value,
playCommonClassloader.value,
playMonitoredFiles.value,
(target in LocalRootProject).value,
(managedClasspath in DocsApplication).value,
interaction,
playDefaultPort.value,
args
)
interaction match {
case nonBlocking: PlayNonBlockingInteractionMode =>
nonBlocking.start(devModeServer)
case blocking =>
devModeServer
println()
println(Colors.green("(Server started, use Ctrl+D to stop and go back to the console...)"))
println()
// If we have both Watched.Configuration and Watched.ContinuousState
// attributes and if Watched.ContinuousState.count is 1 then we assume
// we're in ~ run mode
val maybeContinuous = for {
watched <- state.get(Watched.Configuration)
watchState <- state.get(Watched.ContinuousState)
if watchState.count == 1
} yield watched
maybeContinuous match {
case Some(watched) =>
// ~ run mode
interaction doWithoutEcho {
twiddleRunMonitor(watched, state, devModeServer.buildLink, Some(WatchState.empty))
}
case None =>
// run mode
interaction.waitForCancel()
}
devModeServer.close()
println()
}
}
/**
* Monitor changes in ~run mode.
*/
@tailrec
private def twiddleRunMonitor(watched: Watched, state: State, reloader: BuildLink, ws: Option[WatchState] = None): Unit = {
val ContinuousState = AttributeKey[WatchState]("watch state", "Internal: tracks state for continuous execution.")
def isEOF(c: Int): Boolean = c == 4
@tailrec def shouldTerminate: Boolean = (System.in.available > 0) && (isEOF(System.in.read()) || shouldTerminate)
val sourcesFinder = PathFinder { watched watchPaths state }
val watchState = ws.getOrElse(state get ContinuousState getOrElse WatchState.empty)
val (triggered, newWatchState, newState) =
try {
val (triggered, newWatchState) = SourceModificationWatch.watch(sourcesFinder, watched.pollInterval, watchState)(shouldTerminate)
(triggered, newWatchState, state)
} catch {
case e: Exception =>
val log = state.log
log.error("Error occurred obtaining files to watch. Terminating continuous execution...")
(false, watchState, state.fail)
}
if (triggered) {
//Then launch compile
Project.synchronized {
val start = System.currentTimeMillis
Project.runTask(compile in Compile, newState).get._2.toEither.right.map { _ =>
val duration = System.currentTimeMillis - start
val formatted = duration match {
case ms if ms < 1000 => ms + "ms"
case seconds => (seconds / 1000) + "s"
}
println("[" + Colors.green("success") + "] Compiled in " + formatted)
}
}
// Avoid launching too much compilation
Thread.sleep(Watched.PollDelayMillis)
// Call back myself
twiddleRunMonitor(watched, newState, reloader, Some(newWatchState))
} else {
()
}
}
/**
* Play dev server
*/
private trait PlayDevServer extends Closeable {
val buildLink: BuildLink
}
/**
* Start the Play server in dev mode
*
* @return A closeable that can be closed to stop the server
*/
private def startDevMode(state: State, runHooks: Seq[play.PlayRunHook], javaOptions: Seq[String],
dependencyClasspath: Classpath, dependencyClassLoader: ClassLoaderCreator,
reloaderClasspathTask: TaskKey[Classpath], reloaderClassLoader: ClassLoaderCreator,
assetsClassLoader: ClassLoader => ClassLoader, commonClassLoader: ClassLoader,
monitoredFiles: Seq[String], targetDirectory: File,
docsClasspath: Classpath, interaction: PlayInteractionMode, defaultHttpPort: Int,
args: Seq[String]): PlayDevServer = {
val (properties, httpPort, httpsPort) = filterArgs(args, defaultHttpPort = defaultHttpPort)
val systemProperties = extractSystemProperties(javaOptions)
require(httpPort.isDefined || httpsPort.isDefined, "You have to specify https.port when http.port is disabled")
// Set Java properties
(properties ++ systemProperties).foreach {
case (key, value) => System.setProperty(key, value)
}
println()
/*
* We need to do a bit of classloader magic to run the Play application.
*
* There are seven classloaders:
*
* 1. buildLoader, the classloader of sbt and the Play sbt plugin.
* 2. commonLoader, a classloader that persists across calls to run.
* This classloader is stored inside the
* PlayInternalKeys.playCommonClassloader task. This classloader will
* load the classes for the H2 database if it finds them in the user's
* classpath. This allows H2's in-memory database state to survive across
* calls to run.
* 3. delegatingLoader, a special classloader that overrides class loading
* to delegate shared classes for build link to the buildLoader, and accesses
* the reloader.currentApplicationClassLoader for resource loading to
* make user resources available to dependency classes.
* Has the commonLoader as its parent.
* 4. applicationLoader, contains the application dependencies. Has the
* delegatingLoader as its parent. Classes from the commonLoader and
* the delegatingLoader are checked for loading first.
* 5. docsLoader, the classloader for the special play-docs application
* that is used to serve documentation when running in development mode.
* Has the applicationLoader as its parent for Play dependencies and
* delegation to the shared sbt doc link classes.
* 6. playAssetsClassLoader, serves assets from all projects, prefixed as
* configured. It does no caching, and doesn't need to be reloaded each
* time the assets are rebuilt.
* 7. reloader.currentApplicationClassLoader, contains the user classes
* and resources. Has applicationLoader as its parent, where the
* application dependencies are found, and which will delegate through
* to the buildLoader via the delegatingLoader for the shared link.
* Resources are actually loaded by the delegatingLoader, where they
* are available to both the reloader and the applicationLoader.
* This classloader is recreated on reload. See PlayReloader.
*
* Someone working on this code in the future might want to tidy things up
* by splitting some of the custom logic out of the URLClassLoaders and into
* their own simpler ClassLoader implementations. The curious cycle between
* applicationLoader and reloader.currentApplicationClassLoader could also
* use some attention.
*/
// Get the URLs for the resources in a classpath
def urls(cp: Classpath): Array[URL] = cp.map(_.data.toURI.toURL).toArray
val buildLoader = this.getClass.getClassLoader
/**
* ClassLoader that delegates loading of shared build link classes to the
* buildLoader. Also accesses the reloader resources to make these available
* to the applicationLoader, creating a full circle for resource loading.
*/
lazy val delegatingLoader: ClassLoader = new DelegatingClassLoader(commonClassLoader, buildLoader, new ApplicationClassLoaderProvider {
def get: ClassLoader = { reloader.getClassLoader.orNull }
})
lazy val applicationLoader = dependencyClassLoader("PlayDependencyClassLoader", urls(dependencyClasspath), delegatingLoader)
lazy val assetsLoader = assetsClassLoader(applicationLoader)
lazy val reloader = newReloader(state, playReload, reloaderClassLoader, reloaderClasspathTask, assetsLoader,
monitoredFiles, targetDirectory)
try {
// Now we're about to start, let's call the hooks:
runHooks.run(_.beforeStarted())
// Get a handler for the documentation. The documentation content lives in play/docs/content
// within the play-docs JAR.
val docsLoader = new URLClassLoader(urls(docsClasspath), applicationLoader)
val docsJarFile = {
val f = docsClasspath.map(_.data).filter(_.getName.startsWith("play-docs")).head
new JarFile(f)
}
val buildDocHandler = {
val docHandlerFactoryClass = docsLoader.loadClass("play.docs.BuildDocHandlerFactory")
val factoryMethod = docHandlerFactoryClass.getMethod("fromJar", classOf[JarFile], classOf[String])
factoryMethod.invoke(null, docsJarFile, "play/docs/content").asInstanceOf[BuildDocHandler]
}
val server = {
val mainClass = applicationLoader.loadClass("play.core.server.NettyServer")
if (httpPort.isDefined) {
val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[BuildDocHandler], classOf[Int])
mainDev.invoke(null, reloader, buildDocHandler, httpPort.get: java.lang.Integer).asInstanceOf[play.core.server.ServerWithStop]
} else {
val mainDev = mainClass.getMethod("mainDevOnlyHttpsMode", classOf[BuildLink], classOf[BuildDocHandler], classOf[Int])
mainDev.invoke(null, reloader, buildDocHandler, httpsPort.get: java.lang.Integer).asInstanceOf[play.core.server.ServerWithStop]
}
}
// Notify hooks
runHooks.run(_.afterStarted(server.mainAddress))
new PlayDevServer {
val buildLink = reloader
def close() = {
server.stop()
docsJarFile.close()
reloader.close()
// Notify hooks
runHooks.run(_.afterStopped())
// Remove Java properties
properties.foreach {
case (key, _) => System.clearProperty(key)
}
}
}
} catch {
case e: Throwable =>
// Let hooks clean up
runHooks.foreach { hook =>
try {
hook.onError()
} catch {
case e: Throwable => // Swallow any exceptions so that all `onError`s get called.
}
}
throw e
}
}
val playPrefixAndAssetsSetting = playPrefixAndAssets := {
assetsPrefix.value -> (WebKeys.public in Assets).value
}
val playAllAssetsSetting = playAllAssets := {
(playPrefixAndAssets ?).all(ScopeFilter(
inDependencies(ThisProject),
inConfigurations(Compile)
)).value.flatten
}
val playAssetsClassLoaderSetting = playAssetsClassLoader := { parent =>
new AssetsClassLoader(parent, playAllAssets.value)
}
val playPrefixAndPipelineSetting = playPrefixAndPipeline := {
assetsPrefix.value -> (WebKeys.pipeline in Assets).value
}
val playPackageAssetsMappingsSetting = playPackageAssetsMappings := {
val allPipelines = (playPrefixAndPipeline ?).all(ScopeFilter(
inDependencies(ThisProject),
inConfigurations(Compile)
)).value.flatten
val allMappings = allPipelines.flatMap {
case (prefix, pipeline) => pipeline.map {
case (file, path) => file -> (prefix + path)
}
}
SbtWeb.deduplicateMappings(allMappings, Seq(_.headOption))
}
val playStartCommand = Command.args("start", "<port>") { (state: State, args: Seq[String]) =>
val extracted = Project.extract(state)
val interaction = extracted.get(playInteractionMode)
// Parse HTTP port argument
val (properties, httpPort, httpsPort) = filterArgs(args, defaultHttpPort = extracted.get(playDefaultPort))
require(httpPort.isDefined || httpsPort.isDefined, "You have to specify https.port when http.port is disabled")
Project.runTask(stage, state).get._2.toEither match {
case Left(_) =>
println()
println("Cannot start with errors.")
println()
state.fail
case Right(_) =>
val stagingBin = Some(extracted.get(stagingDirectory in Universal) / "bin" / extracted.get(normalizedName in Universal)).map {
f =>
if (System.getProperty("os.name").toLowerCase.contains("win")) f.getAbsolutePath + ".bat" else f.getAbsolutePath
}.get
val javaProductionOptions = Project.runTask(javaOptions in Production, state).get._2.toEither.right.getOrElse(Seq[String]())
// Note that I'm unable to pass system properties along with properties... if I do then I receive:
// java.nio.charset.IllegalCharsetNameException: "UTF-8"
// Things are working without passing system properties, and I'm unsure that they need to be passed explicitly. If def main(args: Array[String]){
// problem occurs in this area then at least we know what to look at.
val args = Seq(stagingBin) ++
properties.map {
case (key, value) => s"-D$key=$value"
} ++
javaProductionOptions ++
Seq("-Dhttp.port=" + httpPort.getOrElse("disabled"))
val builder = new java.lang.ProcessBuilder(args.asJava)
new Thread {
override def run() {
System.exit(Process(builder) !)
}
}.start()
println(Colors.green(
"""|
|(Starting server. Type Ctrl+D to exit logs, the server will remain in background)
| """.stripMargin))
interaction.waitForCancel()
println()
state.copy(remainingCommands = Seq.empty)
}
}
}
Other Play Framework source code examplesHere is a short list of links related to this Play Framework PlayRun.scala source code file: |
| ... this post is sponsored by my books ... | |
#1 New Release! |
FP Best Seller |
Copyright 1998-2024 Alvin Alexander, alvinalexander.com
All Rights Reserved.
A percentage of advertising revenue from
pages under the /java/jwarehouse
URI on this website is
paid back to open source projects.