diff --git a/README.md b/README.md index 88c6950..5d186e1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ sbt-kanela-runner ========= -This project contains [sbt] plugins that automatically configure your build to perform enable the Kanela -instrumentation agent when running your application from within SBT, both for regular applications and Play Framework -projects in development mode. +This project contains [sbt] plugins that automatically configures your build to enable the Kanela instrumentation agent +when running your application from within SBT. It works with regular applications with a `main` method, Play Framework, +and Lagom projects in development mode. + +Starting on version 2.1.x this plugin requires SBT 1.9.0+. See details below for compatibility with older SBT and +Play/Lagom versions. -SBT versions 1.2.x and 1.3.x are supported. The last version supporting SBT 0.13 was 2.0.5. ## Why this plugin? @@ -14,7 +16,6 @@ doing so can be challenging when running from SBT. These plugins take care of th `run` will just work, regardless your project type or whether you are forking the JVM or not. - ## Regular Projects (non-Play) ### Configuring @@ -22,11 +23,11 @@ doing so can be challenging when running from SBT. These plugins take care of th Add the `sbt-kanela-runner` plugin to your `project/plugins.sbt` file using the code bellow: ```scala -addSbtPlugin("io.kamon" % "sbt-kanela-runner" % "2.0.14") +addSbtPlugin("io.kamon" % "sbt-kanela-runner" % "2.1.0") ``` -### Important -This plugin will only work properly on SBT 1.3 if the `classLoaderLayeringStrategy` setting is set to `Flat` or you set +#### Important +This plugin will only work properly on SBT 1.3+ if the `classLoaderLayeringStrategy` setting is set to `Flat` or you set `fork in run := true`. ### Running @@ -35,15 +36,25 @@ Just `run`, like you do all the time! Here is what the plugin will do depending on your `fork` settings: * **fork in run := true**: The forked process will run with the `-javaagent:` and that's all. -* **fork in run := false**: The plugin will try to attach the Kanela agent to the current process, targetting the +* **fork in run := false**: The plugin will try to attach the Kanela agent to the current process, targeting the ClassLoader where your application is placed. ## Play Projects -### Configuring +### Installing the Plugin -For Play Framework 2.6 and 2.7 projects add the `sbt-kanela-runner-play-2.x` to your `project/plugins.sbt` file: +For Play Framework 2.9 and 3.0, add the appropriate `sbt-kanela-runner-play-x.x` to your `project/plugins.sbt` file: + +```scala +// For Play Framework 2.9 +addSbtPlugin("io.kamon" % "sbt-kanela-runner-play-2.9" % "2.1.0") + +// For Play Framework 3.0 +addSbtPlugin("io.kamon" % "sbt-kanela-runner-play-3.0" % "2.1.0") +``` + +For older Play Framework versions: ```scala // For Play Framework 2.6 @@ -66,7 +77,7 @@ file and call `.enablePlugins(JavaAgent)` on it, it will probably look like this lazy val root = (project in file(".")).enablePlugins(PlayScala, JavaAgent) ``` -This plugin has been tested with **Play 2.8.8**, **Play 2.7.3** and **Play 2.6.23**. +This plugin has been tested with **Play Framework 2.9.1**, **3.0.1**, **2.8.8**, **2.7.3**, and **2.6.23**. ### Running @@ -74,7 +85,8 @@ Just `run`, like you do all the time! A notice will be shown saying that you are ## Lagom Projects -### Configuration + +### Installing the Plugin For Lagom Framework 1.6 add the `sbt-kanela-runner-lagom-1.6` plugin to your `project/plugins.sbt` file: diff --git a/build.sbt b/build.sbt index aa10ba6..c95d39b 100644 --- a/build.sbt +++ b/build.sbt @@ -32,62 +32,36 @@ def crossSbtDependency(module: ModuleID, sbtVersion: String, scalaVersion: Strin Defaults.sbtPluginExtra(module, sbtVersion, scalaVersion) } -val playSbtPluginFor26 = "com.typesafe.play" % "sbt-plugin" % "2.6.25" -val playSbtPluginFor27 = "com.typesafe.play" % "sbt-plugin" % "2.7.9" -val playSbtPluginFor28 = "com.typesafe.play" % "sbt-plugin" % "2.8.8" -val lagomSbtPluginFor16 = "com.lightbend.lagom" % "lagom-sbt-plugin" % "1.6.7" - +val playSbtPluginFor29 = "com.typesafe.play" % "sbt-plugin" % "2.9.1" +val playSbtPluginFor30 = "org.playframework" % "sbt-plugin" % "3.0.1" +val sbtJavaAgentPlugin = "com.github.sbt" % "sbt-javaagent" % "0.1.8" lazy val sbtKanelaRunner = Project("sbt-kanela-runner", file(".")) .settings( noPublishing: _* - ).aggregate(kanelaRunner, kanelaRunnerPlay26, kanelaRunnerPlay27, kanelaRunnerPlay28, kanelaRunnerLagom16) + ).aggregate(kanelaRunner, kanelaRunnerPlay29, kanelaRunnerPlay30) lazy val kanelaRunner = Project("kanela-runner", file("sbt-kanela-runner")) .settings( moduleName := "sbt-kanela-runner", - libraryDependencies += "net.bytebuddy" % "byte-buddy-agent" % "1.9.12" + libraryDependencies += "net.bytebuddy" % "byte-buddy-agent" % "1.14.2" ) -lazy val kanelaRunnerPlay26 = Project("kanela-runner-play-26", file("sbt-kanela-runner-play-2.6")) - .dependsOn(kanelaRunner) - .settings( - name := "sbt-kanela-runner-play-2.6", - moduleName := "sbt-kanela-runner-play-2.6", - libraryDependencies ++= Seq( - crossSbtDependency(playSbtPluginFor26, (sbtBinaryVersion in pluginCrossBuild).value, scalaBinaryVersion.value) - ) - ) -lazy val kanelaRunnerPlay27 = Project("kanela-runner-play-27", file("sbt-kanela-runner-play-2.7")) +lazy val kanelaRunnerPlay29 = Project("kanela-runner-play-29", file("sbt-kanela-runner-play-2.9")) .dependsOn(kanelaRunner) .settings( - name := "sbt-kanela-runner-play-2.7", - moduleName := "sbt-kanela-runner-play-2.7", - libraryDependencies ++= Seq( - crossSbtDependency(playSbtPluginFor27, (sbtBinaryVersion in pluginCrossBuild).value, scalaBinaryVersion.value) - ) + name := "sbt-kanela-runner-play-2.9", + moduleName := "sbt-kanela-runner-play-2.9", + libraryDependencies += crossSbtDependency(playSbtPluginFor29, (pluginCrossBuild / sbtBinaryVersion).value, scalaBinaryVersion.value), + libraryDependencies += crossSbtDependency(sbtJavaAgentPlugin, (pluginCrossBuild / sbtBinaryVersion).value, scalaBinaryVersion.value) ) -lazy val kanelaRunnerPlay28 = Project("kanela-runner-play-28", file("sbt-kanela-runner-play-2.8")) +lazy val kanelaRunnerPlay30 = Project("kanela-runner-play-30", file("sbt-kanela-runner-play-3.0")) .dependsOn(kanelaRunner) .settings( - name := "sbt-kanela-runner-play-2.8", - moduleName := "sbt-kanela-runner-play-2.8", - libraryDependencies ++= Seq( - crossSbtDependency(playSbtPluginFor28, (sbtBinaryVersion in pluginCrossBuild).value, scalaBinaryVersion.value) - ) + name := "sbt-kanela-runner-play-3.0", + moduleName := "sbt-kanela-runner-play-3.0", + libraryDependencies += crossSbtDependency(playSbtPluginFor30, (pluginCrossBuild / sbtBinaryVersion).value, scalaBinaryVersion.value), + libraryDependencies += crossSbtDependency(sbtJavaAgentPlugin, (pluginCrossBuild / sbtBinaryVersion).value, scalaBinaryVersion.value) ) - -lazy val kanelaRunnerLagom16 = Project("kanela-runner-lagom-16", file("sbt-kanela-runner-lagom-1.6")) - .dependsOn(kanelaRunner) - .settings( - name := "sbt-kanela-runner-lagom-1.6", - moduleName := "sbt-kanela-runner-lagom-1.6", - libraryDependencies ++= Seq( - crossSbtDependency(lagomSbtPluginFor16, (sbtBinaryVersion in pluginCrossBuild).value, scalaBinaryVersion.value) - ) - ) - -// remove this? -//crossSbtVersions := Seq("1.3.8") diff --git a/project/KamonSbtUmbrella.scala b/project/KamonSbtUmbrella.scala index a79fb05..41e365e 100644 --- a/project/KamonSbtUmbrella.scala +++ b/project/KamonSbtUmbrella.scala @@ -19,7 +19,7 @@ object KamonSbtUmbrella extends AutoPlugin { def optionalScope(deps: ModuleID*): Seq[ModuleID] = deps map (_ % "compile,optional") val noPublishing = Seq( - skip in publish := true, + publish / skip := true, publishLocal := {}, publishArtifact := false ) diff --git a/project/build.properties b/project/build.properties index a919a9b..abbbce5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.8 +sbt.version=1.9.8 diff --git a/project/plugins.sbt b/project/plugins.sbt index 2936224..259cbbb 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,2 @@ addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") -addSbtPlugin("com.lightbend.sbt" % "sbt-javaagent" % "0.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") \ No newline at end of file diff --git a/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/dev/KanelaReloader.scala b/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/dev/KanelaReloader.scala deleted file mode 100644 index 83b4105..0000000 --- a/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/dev/KanelaReloader.scala +++ /dev/null @@ -1,404 +0,0 @@ -/* - * This file has been copied and modified from the official SBT Lagom Plugin - */ - -package com.lightbend.lagom.dev - -import java.io.File -import java.net.{URL, URLClassLoader} -import java.security.AccessController -import java.security.PrivilegedAction -import java.time.Instant -import java.util -import java.util.Timer -import java.util.TimerTask -import java.util.concurrent.atomic.AtomicReference -import play.core.Build -import play.core.BuildLink -import play.core.server.ReloadableServer -import play.dev.filewatch.FileWatchService -import play.dev.filewatch.SourceModificationWatch -import play.dev.filewatch.WatchState - -import scala.collection.JavaConverters._ -import better.files.{File => _, _} -import com.lightbend.lagom.dev.Reloader.{CompileResult, DevServer, DevServerBinding} -import kamon.instrumentation.sbt.SbtKanelaRunner -import org.slf4j.LoggerFactory - -object KanelaReloader { - - case class Source(file: File, original: Option[File]) - - private val accessControlContext = AccessController.getContext - - /** - * Execute f with context ClassLoader of Reloader - */ - private def withReloaderContextClassLoader[T](f: => T): T = { - val thread = Thread.currentThread - val oldLoader = thread.getContextClassLoader - // we use accessControlContext & AccessController to avoid a ClassLoader leak (ProtectionDomain class) - AccessController.doPrivileged( - new PrivilegedAction[T]() { - def run: T = { - try { - thread.setContextClassLoader(classOf[Reloader].getClassLoader) - f - } finally { - thread.setContextClassLoader(oldLoader) - } - } - }, - accessControlContext - ) - } - - private def urls(cp: Seq[File]): Array[URL] = cp.map(_.toURI.toURL).toArray - - /** - * Start the Lagom server in dev mode. - */ - def startDevMode( - parentClassLoader: ClassLoader, - dependencyClasspath: Seq[File], - reloadCompile: () => CompileResult, - classLoaderDecorator: ClassLoader => ClassLoader, - monitoredFiles: Seq[File], - fileWatchService: FileWatchService, - projectPath: File, - devSettings: Seq[(String, String)], - httpAddress: String, - httpPort: Int, - httpsPort: Int, - reloadLock: AnyRef, - kanelaAgentJar: File - ): DevServer = { - /* - * We need to do a bit of classloader magic to run the Play application. - * - * There are six classloaders: - * - * 1. buildLoader, the classloader of the build tool plugin (sbt/maven lagom plugin). - * 2. parentClassLoader, a possibly shared classloader that may contain artifacts - * that are known to not share state, eg Scala itself. - * 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. - * 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. decoratedClassloader, allows the classloader to be decorated. - * 6. 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. - */ - - - /** - * 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. - */ - - // Forces the Scala Library to be loaded from the dependency classpath by skipping - // the SBT ScalaInstance class loader. This will ensure that Kamon can apply Future-related - // instrumentation in development mode. - lazy val grandParentClassLoader = parentClassLoader - .getParent // Skips ScalaLoader - .getParent // Skips CachedClassLoader (also contains the Scala Library) - - lazy val delegatingLoader: ClassLoader = buildDelegating(grandParentClassLoader, reloader.getClassLoader _) - lazy val applicationLoader = buildForApplication(dependencyClasspath, delegatingLoader) - lazy val decoratedLoader = classLoaderDecorator(applicationLoader) - - lazy val reloader = new KanelaReloader( - reloadCompile, - decoratedLoader, - projectPath, - devSettings, - monitoredFiles, - fileWatchService, - reloadLock, - kanelaAgentJar - ) - - SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, applicationLoader, clearRegistry = true) - - val server: ReloadableServer = mainDev(applicationLoader, reloader, httpAddress, httpPort, httpsPort) - val _bindings = bindings(httpAddress, httpPort, httpsPort) - - new DevServer { - val buildLink: BuildLink = reloader - def addChangeListener(f: () => Unit): Unit = reloader.addChangeListener(f) - def reload(): Unit = server.reload() - def close(): Unit = { - server.stop() - reloader.close() - } - def bindings(): Seq[DevServerBinding] = _bindings - } - } - - /** - * Start the Lagom server without hot reloading - */ - def startNoReload( - parentClassLoader: ClassLoader, - dependencyClasspath: Seq[File], - buildProjectPath: File, - devSettings: Seq[(String, String)], - httpAddress: String, - httpPort: Int, - httpsPort: Int, - kanelaAgentJar: File - ): DevServer = { - lazy val delegatingLoader: ClassLoader = buildDelegating(parentClassLoader, () => Some(applicationLoader)) - lazy val applicationLoader = buildForApplication(dependencyClasspath, delegatingLoader) - - val _buildLink = new BuildLink { - private val initialized = new java.util.concurrent.atomic.AtomicBoolean(false) - override def reload(): AnyRef = { - if (initialized.compareAndSet(false, true)) applicationLoader - else null // this means nothing to reload - } - override def projectPath(): File = buildProjectPath - override def settings(): util.Map[String, String] = devSettings.toMap.asJava - override def forceReload(): Unit = () - override def findSource(className: String, line: Integer): Array[AnyRef] = null - } - - SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, applicationLoader, clearRegistry = true) - val server: ReloadableServer = mainDev(applicationLoader, _buildLink, httpAddress, httpPort, httpsPort) - - server.reload() // it's important to initialize the server - - val _bindings = bindings(httpAddress, httpPort, httpsPort) - - new DevServer { - val buildLink: BuildLink = _buildLink - - /** Allows to register a listener that will be triggered a monitored file is changed. */ - def addChangeListener(f: () => Unit): Unit = () - - /** Reloads the application.*/ - def reload(): Unit = () - - /** List of bindings this server is exposing.*/ - def bindings(): Seq[DevServerBinding] = _bindings - - def close(): Unit = server.stop() - } - } - - private def buildDelegating( - parentClassLoader: ClassLoader, - applicationClassLoader: () => Option[ClassLoader] - ): ClassLoader = { - val buildLoader = this.getClass.getClassLoader - val sharedClasses = Build.sharedClasses.asScala.toSet - new DelegatingClassLoader(parentClassLoader, sharedClasses, buildLoader, applicationClassLoader) - } - - private def buildForApplication(dependencyClasspath: Seq[File], delegatingLoader: => ClassLoader): ClassLoader = - new NamedURLClassLoader("LagomDependencyClassLoader", urls(dependencyClasspath), delegatingLoader) - - private def mainDev( - applicationLoader: ClassLoader, - buildLink: BuildLink, - httpAddress: String, - httpPort: Int, - httpsPort: Int - ): ReloadableServer = { - val mainClass = applicationLoader.loadClass("play.core.server.LagomReloadableDevServerStart") - val mainDev = mainClass.getMethod("mainDev", classOf[BuildLink], classOf[String], classOf[Int], classOf[Int]) - mainDev - .invoke(null, buildLink, httpAddress, httpPort: java.lang.Integer, httpsPort: java.lang.Integer) - .asInstanceOf[ReloadableServer] - } - - private def bindings(httpAddress: String, httpPort: Int, httpsPort: Int): Seq[DevServerBinding] = { - val bindings = Seq.newBuilder[DevServerBinding] - - bindings += DevServerBinding("HTTP", httpAddress, httpPort) - - if (httpsPort > 0) - bindings += DevServerBinding("HTTPS", httpAddress, httpsPort) - - bindings.result() - } - - class NamedURLClassLoader(name: String, urls: Array[URL], parent: ClassLoader) extends URLClassLoader(urls, parent) { - override def toString: String = name + "{" + getURLs.map(_.toString).mkString(", ") + "}" - } -} - -import Reloader._ - -class KanelaReloader( - reloadCompile: () => CompileResult, - baseLoader: ClassLoader, - val projectPath: File, - devSettings: Seq[(String, String)], - monitoredFiles: Seq[File], - fileWatchService: FileWatchService, - reloadLock: AnyRef, - kanelaAgentJar: File - ) extends BuildLink { - // The current classloader for the application - @volatile private var currentApplicationClassLoader: Option[ClassLoader] = None - // Flag to force a reload on the next request. - // This is set if a compile error occurs, and also by the forceReload method on BuildLink, which is called for - // example when evolutions have been applied. - @volatile private var forceReloadNextTime = false - // Whether any source files have changed since the last request. - @volatile private var changed = false - // The last successful compile results. Used for rendering nice errors. - @volatile private var currentSourceMap = Option.empty[Map[String, Source]] - // A watch state for the classpath. Used to determine whether anything on the classpath has changed as a result - // of compilation, and therefore a new classloader is needed and the app needs to be reloaded. - @volatile private var watchState: WatchState = WatchState.empty - - // Stores the most recent time that a file was changed - private val fileLastChanged = new AtomicReference[Instant]() - - // Create the watcher, updates the changed boolean when a file has changed. - private val watcher = fileWatchService.watch(monitoredFiles, () => { - changed = true - onChange() - }) - private val classLoaderVersion = new java.util.concurrent.atomic.AtomicInteger(0) - - private val quietTimeTimer = new Timer("reloader-timer", true) - - private val listeners = new java.util.concurrent.CopyOnWriteArrayList[() => Unit]() - - private val quietPeriodMs = 200L - private def onChange(): Unit = { - val now = Instant.now() - fileLastChanged.set(now) - // set timer task - quietTimeTimer.schedule(new TimerTask { - override def run(): Unit = quietPeriodFinished(now) - }, quietPeriodMs) - } - - private def quietPeriodFinished(start: Instant): Unit = { - // If our start time is equal to the most recent start time stored, then execute the handlers and set the most - // recent time to null, otherwise don't do anything. - if (fileLastChanged.compareAndSet(start, null)) { - import scala.collection.JavaConverters._ - listeners.iterator().asScala.foreach(listener => listener()) - } - } - - def addChangeListener(f: () => Unit): Unit = listeners.add(f) - - /** - * Contrary to its name, this doesn't necessarily reload the app. It is invoked on every request, and will only - * trigger a reload of the app if something has changed. - * - * Since this communicates across classloaders, it must return only simple objects. - * - * - * @return Either - * - Throwable - If something went wrong (eg, a compile error). - * - ClassLoader - If the classloader has changed, and the application should be reloaded. - * - null - If nothing changed. - */ - def reload: AnyRef = { - reloadLock.synchronized { - if (changed || forceReloadNextTime || currentSourceMap.isEmpty || currentApplicationClassLoader.isEmpty) { - val shouldReload = forceReloadNextTime - - changed = false - forceReloadNextTime = false - - // use Reloader context ClassLoader to avoid ClassLoader leaks in sbt/scala-compiler threads - KanelaReloader.withReloaderContextClassLoader { - // Run the reload task, which will trigger everything to compile - reloadCompile() match { - case CompileFailure(exception) => - // We force reload next time because compilation failed this time - forceReloadNextTime = true - exception - - case CompileSuccess(sourceMap, classpath) => - currentSourceMap = Some(sourceMap) - - // We only want to reload if the classpath has changed. Assets don't live on the classpath, so - // they won't trigger a reload. - // Use the SBT watch service, passing true as the termination to force it to break after one check - val (_, newState) = SourceModificationWatch.watch( - () => - classpath.iterator - .filter(_.exists()) - .flatMap(_.toScala.listRecursively), - 0, - watchState - )(true) - // SBT has a quiet wait period, if that's set to true, sources were modified - val triggered = newState.awaitingQuietPeriod - watchState = newState - - if (triggered || shouldReload || currentApplicationClassLoader.isEmpty) { - // Create a new classloader - val version = classLoaderVersion.incrementAndGet - val name = "ReloadableClassLoader(v" + version + ")" - val urls = KanelaReloader.urls(classpath) - SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, baseLoader, clearRegistry = false) - val loader = new DelegatedResourcesClassLoader(name, urls, baseLoader) - currentApplicationClassLoader = Some(loader) - loader - } else { - null // null means nothing changed - } - } - } - } else { - null // null means nothing changed - } - } - } - - lazy val settings: util.Map[String, String] = { - import scala.collection.JavaConverters._ - devSettings.toMap.asJava - } - - def forceReload() { - forceReloadNextTime = true - } - - def findSource(className: String, line: java.lang.Integer): Array[java.lang.Object] = { - val topType = className.split('$').head - currentSourceMap.flatMap { sources => - sources.get(topType).map { source => - Array[java.lang.Object](source.original.getOrElse(source.file), line) - } - }.orNull - } - - def runTask(task: String): AnyRef = - throw new UnsupportedOperationException("This BuildLink does not support running arbitrary tasks") - - def close(): Unit = { - currentApplicationClassLoader = None - currentSourceMap = None - watcher.stop() - quietTimeTimer.cancel() - } - - def getClassLoader: Option[ClassLoader] = currentApplicationClassLoader -} diff --git a/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/sbt/SbtKanelaRunnerLagom.scala b/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/sbt/SbtKanelaRunnerLagom.scala deleted file mode 100644 index 494de00..0000000 --- a/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/sbt/SbtKanelaRunnerLagom.scala +++ /dev/null @@ -1,159 +0,0 @@ -package com.lightbend.lagom.sbt - -import com.lightbend.lagom.dev.{MiniLogger, StaticServiceLocations} -import com.lightbend.lagom.dev.Reloader.DevServer -import com.lightbend.lagom.dev.Servers.ServerContainer -import com.lightbend.lagom.sbt.LagomPlugin.autoImport.{lagomCassandraPort, lagomKafkaAddress, lagomRun, lagomServiceGatewayAddress, lagomServiceGatewayImpl, lagomServiceGatewayPort, lagomServiceLocatorAddress, lagomServiceLocatorPort, lagomServiceLocatorStart, lagomServiceLocatorStop, lagomUnmanagedServices} -import com.lightbend.lagom.sbt.run.KanelaRunSupport -import com.lightbend.sbt.javaagent.JavaAgent -import com.lightbend.sbt.javaagent.JavaAgent.JavaAgentKeys.javaAgents -import kamon.instrumentation.sbt.{KanelaOnSystemClassLoader, SbtKanelaRunner} -import kamon.instrumentation.sbt.SbtKanelaRunner.Keys.kanelaVersion -import sbt.Def.Initialize -import sbt.Keys.{managedClasspath, name, state} -import sbt.{Def, inScope, _} - -import java.io.Closeable -import java.net.{URI, URL, URLClassLoader} -import java.util.{Map => JMap} -import scala.collection.JavaConverters._ - -object SbtKanelaRunnerLagom extends AutoPlugin { - - // override def trigger = AllRequirements - override def requires = Lagom && SbtKanelaRunner && JavaAgent - - override def projectSettings: Seq[Setting[_]] = Seq( - javaAgents += "io.kamon" % "kanela-agent" % kanelaVersion.value, - lagomRun := { - val service = runLagomTask.value - // eagerly loads the service - service.reload() - // install a listener that will take care of reloading on classpath's changes - service.addChangeListener(() => service.reload()) - (name.value, service) - }, - ) - - override def buildSettings: Seq[Def.Setting[_]] = inScope(ThisScope in LagomPlugin.extraProjects.head)(Seq( - lagomServiceLocatorStart in ThisBuild := startServiceLocatorTask.value, - lagomServiceLocatorStop in ThisBuild := ServiceLocator.tryStop(new SbtLoggerProxy(state.value.log)) - )) - - private lazy val runLagomTask: Initialize[Task[DevServer]] = Def.taskDyn { - KanelaRunSupport.reloadRunTask(LagomPlugin.managedSettings.value) - } - - private lazy val startServiceLocatorTask = Def.task { - val unmanagedServices: Map[String, String] = - StaticServiceLocations.staticServiceLocations(lagomCassandraPort.value, lagomKafkaAddress.value) ++ lagomUnmanagedServices.value - - val serviceLocatorAddress = lagomServiceLocatorAddress.value - val serviceLocatorPort = lagomServiceLocatorPort.value - val serviceGatewayAddress = lagomServiceGatewayAddress.value - val serviceGatewayHttpPort = lagomServiceGatewayPort.value - val serviceGatewayImpl = lagomServiceGatewayImpl.value - val classpathUrls = (managedClasspath in Compile).value.files.map(_.toURI.toURL).toArray - val scalaInstance = Keys.scalaInstance.value - val log = new SbtLoggerProxy(state.value.log) - - ServiceLocator.start( - log, - scalaInstance.loader, - classpathUrls, - serviceLocatorAddress, - serviceLocatorPort, - serviceGatewayAddress, - serviceGatewayHttpPort, - unmanagedServices, - serviceGatewayImpl - ) - } - - class LagomServiceLocatorClassLoader(urls: Array[URL], parent: ClassLoader) extends URLClassLoader(urls, parent) - - object ServiceLocator extends ServerContainer { - protected type Server = Closeable { - def start( - serviceLocatorAddress: String, - serviceLocatorPort: Int, - serviceGatewayAddress: String, - serviceGatewayHttpPort: Int, - unmanagedServices: JMap[String, String], - gatewayImpl: String - ): Unit - def serviceLocatorAddress: URI - def serviceGatewayAddress: URI - } - - def start( - log: MiniLogger, - parentClassLoader: ClassLoader, - classpath: Array[URL], - serviceLocatorAddress: String, - serviceLocatorPort: Int, - serviceGatewayAddress: String, - serviceGatewayHttpPort: Int, - unmanagedServices: Map[String, String], - gatewayImpl: String - ): Closeable = - synchronized { - if (server == null) { - withContextClassloader(new LagomServiceLocatorClassLoader(classpath, parentClassLoader)) { loader => - val serverClass = loader.loadClass("com.lightbend.lagom.registry.impl.ServiceLocatorServer") - server = serverClass.getDeclaredConstructor().newInstance().asInstanceOf[Server] - try { - server.start( - serviceLocatorAddress, - serviceLocatorPort, - serviceGatewayAddress, - serviceGatewayHttpPort, - unmanagedServices.asJava, - gatewayImpl - ) - } catch { - case e: Exception => - val msg = "Failed to start embedded Service Locator or Service Gateway. " + - s"Hint: Are ports $serviceLocatorPort or $serviceGatewayHttpPort already in use?" - stop() - throw new RuntimeException(msg, e) - } - } - } - if (server != null) { - log.info("Service locator is running at " + server.serviceLocatorAddress) - // TODO: trace all valid locations for the service gateway. - log.info("Service gateway is running at " + server.serviceGatewayAddress) - } - - new Closeable { - override def close(): Unit = stop(log) - } - } - - private def withContextClassloader[T](loader: ClassLoader)(body: ClassLoader => T): T = { - val current = Thread.currentThread().getContextClassLoader - try { - Thread.currentThread().setContextClassLoader(loader) - body(loader) - } finally Thread.currentThread().setContextClassLoader(current) - } - - protected def stop(log: MiniLogger): Unit = - synchronized { - if (server == null) { - log.info("Service locator was already stopped") - } else { - log.info("Stopping service locator") - stop() - } - } - - private def stop(): Unit = - synchronized { - try server.close() - catch { case _: Exception => () } - finally server = null - } - } -} diff --git a/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/sbt/run/KanelaRunSupport.scala b/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/sbt/run/KanelaRunSupport.scala deleted file mode 100644 index 47e07db..0000000 --- a/sbt-kanela-runner-lagom-1.6/src/main/scala/com/lightbend/lagom/sbt/run/KanelaRunSupport.scala +++ /dev/null @@ -1,94 +0,0 @@ -/* - * This file has been copied and modified from the official SBT Lagom Plugin - */ - -package com.lightbend.lagom.sbt.run - -import com.lightbend.lagom.dev.{KanelaReloader, Reloader} -import com.lightbend.lagom.sbt.Internal -import com.lightbend.lagom.sbt.LagomPlugin.autoImport._ -import com.lightbend.lagom.sbt.LagomReloadableService.autoImport._ -import kamon.instrumentation.sbt.SbtKanelaRunner -import sbt._ -import sbt.Keys._ - -private[sbt] object KanelaRunSupport extends RunSupportCompat { - def reloadRunTask( - extraConfigs: Map[String, String] - ): Def.Initialize[Task[Reloader.DevServer]] = Def.task { - val state = Keys.state.value - val scope = resolvedScoped.value.scope - - val reloadCompile = () => - KanelaRunSupport.compile( - () => Project.runTask(lagomReload in scope, state).map(_._2).get, - () => Project.runTask(lagomReloaderClasspath in scope, state).map(_._2).get, - () => Project.runTask(streamsManager in scope, state).map(_._2).get.toEither.right.toOption - ) - - val classpath = (devModeDependencies.value ++ (externalDependencyClasspath in Runtime).value).distinct.files - - KanelaReloader.startDevMode( - scalaInstance.value.loader, - classpath, - reloadCompile, - lagomClassLoaderDecorator.value, - lagomWatchDirectories.value, - lagomFileWatchService.value, - baseDirectory.value, - extraConfigs.toSeq ++ lagomDevSettings.value, - lagomServiceAddress.value, - selectHttpPortToUse.value, - lagomServiceHttpsPort.value, - KanelaRunSupport, - SbtKanelaRunner.Keys.kanelaAgentJar.value - ) - } - - def nonReloadRunTask( - extraConfigs: Map[String, String] - ): Def.Initialize[Task[Reloader.DevServer]] = Def.task { - val classpath = (devModeDependencies.value ++ (fullClasspath in Runtime).value).distinct - - val buildLinkSettings = extraConfigs.toSeq ++ lagomDevSettings.value - KanelaReloader.startNoReload( - scalaInstance.value.loader, - classpath.map(_.data), - baseDirectory.value, - buildLinkSettings, - lagomServiceAddress.value, - selectHttpPortToUse.value, - lagomServiceHttpsPort.value, - SbtKanelaRunner.Keys.kanelaAgentJar.value - ) - } - - /** - * This task will calculate which port should be used for http. - * To keep backward compatibility, we detect if the user have manually configured lagomServicePort in which - * case we read the value set by the user and we don't use generated port. - * - * This task must be removed together with the deprecated lagomServicePort. - */ - private def selectHttpPortToUse = Def.task { - val logger = Keys.sLog.value - val deprecatedServicePort = lagomServicePort.value - val serviceHttpPort = lagomServiceHttpPort.value - val generatedHttpPort = lagomGeneratedServiceHttpPortCache.value - val isUsingGeneratedPort = serviceHttpPort == generatedHttpPort - - // deprecated setting was modified by user. - if (deprecatedServicePort != -1 && isUsingGeneratedPort) { - deprecatedServicePort - } else if (deprecatedServicePort != -1 && !isUsingGeneratedPort) { - logger.warn( - s"Both 'lagomServiceHttpPort' ($serviceHttpPort) and 'lagomServicePort' ($deprecatedServicePort) are configured, 'lagomServicePort' will be ignored" - ) - serviceHttpPort - } else serviceHttpPort - } - - private def devModeDependencies = Def.task { - (managedClasspath in Internal.Configs.DevRuntime).value - } -} diff --git a/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala b/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala deleted file mode 100644 index 524d844..0000000 --- a/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala +++ /dev/null @@ -1,142 +0,0 @@ -package play.sbt.run - -import kamon.instrumentation.sbt.SbtKanelaRunner -import kamon.instrumentation.sbt.play.KanelaReloader -import play.core.BuildLink -import play.dev.filewatch.{SourceModificationWatch => PlaySourceModificationWatch, WatchState => PlayWatchState} -import play.sbt.PlayImport.PlayKeys._ -import play.sbt.PlayInternalKeys._ -import play.sbt.run.PlayRun.{generatedSourceHandlers, getPollInterval, getSourcesFinder, sleepForPoolDelay} -import play.sbt.{Colors, PlayNonBlockingInteractionMode} -import sbt.Keys._ -import sbt.{AttributeKey, Compile, Def, InputTask, Keys, Project, State, TaskKey, Watched} - -import scala.annotation.tailrec - -object KanelaPlayRun extends PlayRunCompat { - - // This file was copied and modified from the URL bellow since there was no other sensible way to our - // current knowledge of changing the Classloaders to support AspectJ as we did for Play 2.4/2.5 - // - // https://raw.githubusercontent.com/playframework/playframework/2.6.23/dev-mode/sbt-plugin/src/main/scala/play/sbt/run/PlayRun.scala - - val playWithKanelaRunTask = playRunTask(playRunHooks, playDependencyClasspath, - playReloaderClasspath, playAssetsClassLoader) - - def playRunTask( - runHooks: TaskKey[Seq[play.sbt.PlayRunHook]], - dependencyClasspath: TaskKey[Classpath], - reloaderClasspath: TaskKey[Classpath], - assetsClassLoader: TaskKey[ClassLoader => ClassLoader] - ): Def.Initialize[InputTask[Unit]] = Def.inputTask { - - val args = Def.spaceDelimited().parsed - - val state = Keys.state.value - val scope = resolvedScoped.value.scope - val interaction = playInteractionMode.value - - val reloadCompile = () => PlayReload.compile( - () => Project.runTask(playReload in scope, state).map(_._2).get, - () => Project.runTask(reloaderClasspath in scope, state).map(_._2).get, - () => Project.runTask(streamsManager in scope, state).map(_._2).get.toEither.right.toOption - ) - - lazy val devModeServer = KanelaReloader.startDevMode( - runHooks.value, - (javaOptions in sbt.Runtime).value, - playCommonClassloader.value, - dependencyClasspath.value.files, - reloadCompile, - assetsClassLoader.value, - playMonitoredFiles.value, - fileWatchService.value, - generatedSourceHandlers, - playDefaultPort.value, - playDefaultAddress.value, - baseDirectory.value, - devSettings.value, - args, - (mainClass in (Compile, Keys.run)).value.get, - KanelaPlayRun, - SbtKanelaRunner.Keys.kanelaAgentJar.value - ) - - interaction match { - case nonBlocking: PlayNonBlockingInteractionMode => - nonBlocking.start(devModeServer) - case blocking => - devModeServer - - println() - println(Colors.green("(Server started, use Enter to stop and go back to the console...)")) - println() - - val maybeContinuous: Option[Watched] = watchContinuously(state, Keys.sbtVersion.value) - - maybeContinuous match { - case Some(watched) => - // ~ run mode - interaction.doWithoutEcho { - twiddleRunMonitor(watched, state, devModeServer.buildLink, Some(PlayWatchState.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[PlayWatchState] = None): Unit = { - val ContinuousState = AttributeKey[PlayWatchState]("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: PlaySourceModificationWatch.PathFinder = getSourcesFinder(watched, state) - val watchState = ws.getOrElse(state.get(ContinuousState).getOrElse(PlayWatchState.empty)) - - val (triggered, newWatchState, newState) = - try { - val (triggered: Boolean, newWatchState: PlayWatchState) = PlaySourceModificationWatch.watch(sourcesFinder, getPollInterval(watched), watchState)(shouldTerminate) - (triggered, newWatchState, state) - } catch { - case e: Exception => - val log = state.log - log.error("Error occurred obtaining files to watch. Terminating continuous execution...") - log.trace(e) - (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 - sleepForPoolDelay - - // Call back myself - twiddleRunMonitor(watched, newState, reloader, Some(newWatchState)) - } else { - () - } - } - -} diff --git a/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala b/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala deleted file mode 100644 index e3ba5fd..0000000 --- a/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala +++ /dev/null @@ -1,511 +0,0 @@ -// File contents partially copied from: -// https://raw.githubusercontent.com/playframework/playframework/2.6.23/dev-mode/run-support/src/main/scala/play/runsupport/Reloader.scala -package kamon.instrumentation.sbt.play - -import java.io.{Closeable, File} -import java.net.{URL, URLClassLoader} -import java.security.{AccessController, PrivilegedAction} -import java.time.Instant -import java.util.concurrent.atomic.AtomicReference -import java.util.{Timer, TimerTask} -import better.files.{File => _, _} -import SbtKanelaRunnerPlay.SbtKanelaClassLoader -import kamon.instrumentation.sbt.SbtKanelaRunner -import org.slf4j.LoggerFactory -import play.core.{Build, BuildLink} -import play.dev.filewatch.FileWatchService -import play.runsupport.{NamedURLClassLoader, ServerStartException} -import play.runsupport.classloader.{ApplicationClassLoaderProvider, DelegatingClassLoader} -import play.runsupport.{AssetsClassLoader, RunHook} -import play.runsupport.Reloader.{CompileFailure, CompileResult, CompileSuccess, GeneratedSourceMapping, Source} - -import scala.collection.JavaConverters._ - -object KanelaReloader { - - type ClassLoaderCreator = (String, Array[URL], ClassLoader) => ClassLoader - - val SystemProperty = "-D([^=]+)=(.*)".r - - private val accessControlContext = AccessController.getContext - - /** - * Execute f with context ClassLoader of Reloader - */ - private def withReloaderContextClassLoader[T](f: => T): T = { - val thread = Thread.currentThread - val oldLoader = thread.getContextClassLoader - // we use accessControlContext & AccessController to avoid a ClassLoader leak (ProtectionDomain class) - AccessController.doPrivileged(new PrivilegedAction[T]() { - def run: T = { - try { - thread.setContextClassLoader(classOf[KanelaReloader].getClassLoader) - f - } finally { - thread.setContextClassLoader(oldLoader) - } - } - }, accessControlContext) - } - - /** - * 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") - */ - def extractSystemProperties(javaOptions: Seq[String]): Seq[(String, String)] = { - javaOptions.collect { case SystemProperty(key, value) => key -> value } - } - - def parsePort(portString: String): Int = { - try { - Integer.parseInt(portString) - } catch { - case e: NumberFormatException => sys.error("Invalid port argument: " + portString) - } - } - - def filterArgs( - args: Seq[String], - defaultHttpPort: Int, - defaultHttpAddress: String, - devSettings: Seq[(String, String)]): (Seq[(String, String)], Option[Int], Option[Int], String) = { - val (propertyArgs, otherArgs) = args.partition(_.startsWith("-D")) - - val properties = propertyArgs.map(_.drop(2).split('=')).map(a => a(0) -> a(1)).toSeq - - val props = properties.toMap - def prop(key: String): Option[String] = props.get(key).orElse(sys.props.get(key)) - - def parsePortValue(portValue: Option[String], defaultValue: Option[Int] = None): Option[Int] = { - portValue match { - case None => defaultValue - case Some("disabled") => None - case Some(s) => Some(parsePort(s)) - } - } - - // http port can be defined as the first non-property argument, or a -Dhttp.port argument or system property - // the http port can be disabled (set to None) by setting any of the input methods to "disabled" - // Or it can be defined in devSettings as "play.server.http.port" - val httpPortString: Option[String] = - otherArgs.headOption.orElse(prop("http.port")).orElse(devSettings.toMap.get("play.server.http.port")) - val httpPort: Option[Int] = parsePortValue(httpPortString, Option(defaultHttpPort)) - - // https port can be defined as a -Dhttps.port argument or system property - val httpsPortString: Option[String] = prop("https.port").orElse(devSettings.toMap.get("play.server.https.port")) - val httpsPort = parsePortValue(httpsPortString) - - // http address can be defined as a -Dhttp.address argument or system property - val httpAddress = - prop("http.address").orElse(devSettings.toMap.get("play.server.http.address")).getOrElse(defaultHttpAddress) - - (properties, httpPort, httpsPort, httpAddress) - } - - def urls(cp: Seq[File]): Array[URL] = cp.map(_.toURI.toURL).toArray - - def assetsClassLoader(allAssets: Seq[(String, File)])(parent: ClassLoader): ClassLoader = new AssetsClassLoader(parent, allAssets) - - def commonClassLoader(classpath: Seq[File]) = { - lazy val commonJars: PartialFunction[java.io.File, java.net.URL] = { - case jar if jar.getName.startsWith("h2-") || jar.getName == "h2.jar" => jar.toURI.toURL - } - - new java.net.URLClassLoader(classpath.collect(commonJars).toArray, null /* important here, don't depend of the sbt classLoader! */ ) { - override def toString = "Common ClassLoader: " + getURLs.map(_.toString).mkString(",") - } - } - - /** - * Dev server - */ - trait DevServer extends Closeable { - val buildLink: BuildLink - - /** Allows to register a listener that will be triggered a monitored file is changed. */ - def addChangeListener(f: () => Unit): Unit - - /** Reloads the application.*/ - def reload(): Unit - - /** URL at which the application is running (if started) */ - def url(): String - } - - /** - * Start the server in dev mode - * - * @return A closeable that can be closed to stop the server - */ - def startDevMode( - runHooks: Seq[RunHook], javaOptions: Seq[String], - commonClassLoader: ClassLoader, dependencyClasspath: Seq[File], - reloadCompile: () => CompileResult, assetsClassLoader: ClassLoader => ClassLoader, - monitoredFiles: Seq[File], fileWatchService: FileWatchService, - generatedSourceHandlers: Map[String, GeneratedSourceMapping], - defaultHttpPort: Int, defaultHttpAddress: String, projectPath: File, - devSettings: Seq[(String, String)], args: Seq[String], - mainClassName: String, - reloadLock: AnyRef, - kanelaAgentJar: File - ): DevServer = { - - val (properties, httpPort, httpsPort, httpAddress) = filterArgs(args, defaultHttpPort, defaultHttpAddress, devSettings) - 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 application. - * - * There are six classloaders: - * - * 1. buildLoader, the classloader of sbt and the 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. 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. - * 6. 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. - */ - - 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, Build.sharedClasses, buildLoader, new ApplicationClassLoaderProvider { - def get: URLClassLoader = { reloader.getClassLoader.orNull } - }) - - lazy val applicationLoader = - new SbtKanelaClassLoader("DependencyClassLoader", urls(dependencyClasspath), delegatingLoader, loadH2Driver = true) - lazy val assetsLoader = assetsClassLoader(applicationLoader) - - lazy val reloader = new KanelaReloader( - reloadCompile, - assetsLoader, - projectPath, - devSettings, - monitoredFiles, - fileWatchService, - generatedSourceHandlers, - reloadLock, - kanelaAgentJar - ) - - try { - // Now we're about to start, let's call the hooks: - runHooks.run(_.beforeStarted()) - - // Related to https://github.com/kamon-io/kanela/issues/116 - // - // There is *some* problem happening when loading the org.apache.logging.log4j.util.PropertiesUtil on SBT 1.4.0+ - // that prevents applications from running on Development Mode. I suspect that there is an issue with either the - // Context ClassLoader we are setting while Kanela gets attached, or with our implementation of ClassLoader - // delegation. - // - // This hack forces the Log4J-related classes to be loaded before we do the ClassLoader trickery and allows - // applications to launch. - val forceLoadingLog4jClasses = LoggerFactory.getLogger("sbt-kanela-runner-hack") - - SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, applicationLoader, clearRegistry = true) - - val server = { - val mainClass = applicationLoader.loadClass(mainClassName) - if (httpPort.isDefined) { - val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[Int], classOf[String]) - mainDev.invoke(null, reloader, httpPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ReloadableServer] - } else { - val mainDev = mainClass.getMethod("mainDevOnlyHttpsMode", classOf[BuildLink], classOf[Int], classOf[String]) - mainDev.invoke(null, reloader, httpsPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ReloadableServer] - } - } - - // Notify hooks - runHooks.run(_.afterStarted(server.mainAddress)) - - new DevServer { - val buildLink = reloader - def addChangeListener(f: () => Unit): Unit = reloader.addChangeListener(f) - def reload(): Unit = server.reload() - def close(): Unit = { - server.stop() - reloader.close() - - // Notify hooks - runHooks.run(_.afterStopped()) - - // Remove Java properties - properties.foreach { - case (key, _) => System.clearProperty(key) - } - } - def url(): String = server.mainAddress().getHostName + ":" + server.mainAddress().getPort - } - } 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. - } - } - // Convert play-server exceptions to our to our ServerStartException - def getRootCause(t: Throwable): Throwable = if (t.getCause == null) t else getRootCause(t.getCause) - if (getRootCause(e).getClass.getName == "play.core.server.ServerListenException") { - throw new ServerStartException(e) - } - throw e - } - } - - /** - * Start the server without hot reloading - */ - def startNoReload(parentClassLoader: ClassLoader, dependencyClasspath: Seq[File], buildProjectPath: File, - devSettings: Seq[(String, String)], httpPort: Int, mainClassName: String): DevServer = { - val buildLoader = this.getClass.getClassLoader - - lazy val delegatingLoader: ClassLoader = new DelegatingClassLoader( - parentClassLoader, - Build.sharedClasses, buildLoader, new ApplicationClassLoaderProvider { - def get: URLClassLoader = { applicationLoader } - }) - - lazy val applicationLoader = new NamedURLClassLoader("DependencyClassLoader", urls(dependencyClasspath), - delegatingLoader) - - val _buildLink = new BuildLink { - private val initialized = new java.util.concurrent.atomic.AtomicBoolean(false) - override def reload(): AnyRef = { - if (initialized.compareAndSet(false, true)) applicationLoader - else null // this means nothing to reload - } - override def projectPath(): File = buildProjectPath - override def settings(): java.util.Map[String, String] = devSettings.toMap.asJava - override def forceReload(): Unit = () - override def findSource(className: String, line: Integer): Array[AnyRef] = null - } - - val mainClass = applicationLoader.loadClass(mainClassName) - val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[Int]) - val server = mainDev.invoke(null, _buildLink, httpPort: java.lang.Integer).asInstanceOf[play.core.server.ReloadableServer] - - server.reload() // it's important to initialize the server - - new KanelaReloader.DevServer { - val buildLink: BuildLink = _buildLink - - /** Allows to register a listener that will be triggered a monitored file is changed. */ - def addChangeListener(f: () => Unit): Unit = () - - /** Reloads the application.*/ - def reload(): Unit = () - - /** URL at which the application is running (if started) */ - def url(): String = server.mainAddress().getHostName + ":" + server.mainAddress().getPort - - def close(): Unit = server.stop() - } - } - -} - -class KanelaReloader( - reloadCompile: () => CompileResult, - baseLoader: ClassLoader, - val projectPath: File, - devSettings: Seq[(String, String)], - monitoredFiles: Seq[File], - fileWatchService: FileWatchService, - generatedSourceHandlers: Map[String, GeneratedSourceMapping], - reloadLock: AnyRef, - kanelaAgentJar: File) extends BuildLink { - - // The current classloader for the application - @volatile private var currentApplicationClassLoader: Option[URLClassLoader] = None - // Flag to force a reload on the next request. - // This is set if a compile error occurs, and also by the forceReload method on BuildLink, which is called for - // example when evolutions have been applied. - @volatile private var forceReloadNextTime = false - // Whether any source files have changed since the last request. - @volatile private var changed = false - // The last successful compile results. Used for rendering nice errors. - @volatile private var currentSourceMap = Option.empty[Map[String, Source]] - // Last time the classpath was modified in millis. Used to determine whether anything on the classpath has - // changed as a result of compilation, and therefore a new classloader is needed and the app needs to be reloaded. - @volatile private var lastModified: Long = 0L - - // Stores the most recent time that a file was changed - private val fileLastChanged = new AtomicReference[Instant]() - - // Create the watcher, updates the changed boolean when a file has changed. - private val watcher = fileWatchService.watch(monitoredFiles, () => { - changed = true - }) - private val classLoaderVersion = new java.util.concurrent.atomic.AtomicInteger(0) - - private val quietTimeTimer = new Timer("reloader-timer", true) - - private val listeners = new java.util.concurrent.CopyOnWriteArrayList[() => Unit]() - - private val quietPeriodMs: Long = 200L - private def onChange(): Unit = { - val now = Instant.now() - fileLastChanged.set(now) - // set timer task - quietTimeTimer.schedule(new TimerTask { - override def run(): Unit = quietPeriodFinished(now) - }, quietPeriodMs) - } - - private def quietPeriodFinished(start: Instant): Unit = { - // If our start time is equal to the most recent start time stored, then execute the handlers and set the most - // recent time to null, otherwise don't do anything. - if (fileLastChanged.compareAndSet(start, null)) { - import scala.collection.JavaConverters._ - listeners.iterator().asScala.foreach(listener => listener()) - } - } - - def addChangeListener(f: () => Unit): Unit = listeners.add(f) - - /** - * Contrary to its name, this doesn't necessarily reload the app. It is invoked on every request, and will only - * trigger a reload of the app if something has changed. - * - * Since this communicates across classloaders, it must return only simple objects. - * - * - * @return Either - * - Throwable - If something went wrong (eg, a compile error). - * - ClassLoader - If the classloader has changed, and the application should be reloaded. - * - null - If nothing changed. - */ - def reload: AnyRef = { - reloadLock.synchronized { - if (changed || forceReloadNextTime || currentSourceMap.isEmpty || currentApplicationClassLoader.isEmpty) { - - val shouldReload = forceReloadNextTime - - changed = false - forceReloadNextTime = false - - // use Reloader context ClassLoader to avoid ClassLoader leaks in sbt/scala-compiler threads - KanelaReloader.withReloaderContextClassLoader { - // Run the reload task, which will trigger everything to compile - reloadCompile() match { - case CompileFailure(exception) => - // We force reload next time because compilation failed this time - forceReloadNextTime = true - exception - - case CompileSuccess(sourceMap, classpath) => - - currentSourceMap = Some(sourceMap) - - // We only want to reload if the classpath has changed. Assets don't live on the classpath, so - // they won't trigger a reload. - val classpathFiles = classpath.iterator.filter(_.exists()).flatMap(_.toScala.listRecursively).map(_.toJava) - val newLastModified = - classpathFiles.foldLeft(0L) { (acc, file) => - math.max(acc, file.lastModified) - } - val triggered = newLastModified > lastModified - lastModified = newLastModified - - if (triggered || shouldReload || currentApplicationClassLoader.isEmpty) { - // Create a new classloader - val version = classLoaderVersion.incrementAndGet - val name = "ReloadableClassLoader(v" + version + ")" - val urls = KanelaReloader.urls(classpath) - val loader = new SbtKanelaClassLoader(name, urls, baseLoader, skipWhenLoadingResources = true) - SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, loader, clearRegistry = false) - currentApplicationClassLoader = Some(loader) - loader - } else { - null // null means nothing changed - } - } - } - } else { - null // null means nothing changed - } - } - } - - lazy val settings = { - import scala.collection.JavaConverters._ - devSettings.toMap.asJava - } - - def forceReload(): Unit = { - forceReloadNextTime = true - } - - def findSource(className: String, line: java.lang.Integer): Array[java.lang.Object] = { - val topType = className.split('$').head - currentSourceMap.flatMap { sources => - sources.get(topType).map { source => - source.original match { - case Some(origFile) if line != null => - generatedSourceHandlers.get(origFile.getName.split('.').drop(1).mkString(".")) match { - case Some(handler) => - Array[java.lang.Object](origFile, handler.getOriginalLine(source.file, line)) - case _ => - Array[java.lang.Object](origFile, line) - } - case Some(origFile) => - Array[java.lang.Object](origFile, null) - case None => - Array[java.lang.Object](source.file, line) - } - } - }.orNull - } - - def close() = { - currentApplicationClassLoader = None - currentSourceMap = None - watcher.stop() - quietTimeTimer.cancel() - } - - def getClassLoader = currentApplicationClassLoader - -} diff --git a/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala b/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala deleted file mode 100644 index 6be9924..0000000 --- a/sbt-kanela-runner-play-2.6/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala +++ /dev/null @@ -1,86 +0,0 @@ -/* - * ========================================================================================= - * Copyright © 2013-2015 the kamon project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - * ========================================================================================= - */ - -package kamon.instrumentation.sbt.play - -import java.net.URL - -import _root_.play.sbt.PlayImport.PlayKeys._ -import _root_.play.sbt.{Colors, Play, PlayRunHook} -import com.lightbend.sbt.javaagent.JavaAgent -import com.lightbend.sbt.javaagent.JavaAgent.JavaAgentKeys.javaAgents -import kamon.instrumentation.sbt.SbtKanelaRunner.Keys.kanelaVersion -import kamon.instrumentation.sbt.{KanelaOnSystemClassLoader, SbtKanelaRunner} -import play.sbt.run.KanelaPlayRun -import sbt.Keys._ -import sbt._ - -object SbtKanelaRunnerPlay extends AutoPlugin { - - override def trigger = AllRequirements - override def requires = Play && SbtKanelaRunner && JavaAgent - - override def projectSettings: Seq[Setting[_]] = Seq( - Keys.run in Compile := KanelaPlayRun.playWithKanelaRunTask.evaluated, - playRunHooks += runningWithKanelaNotice.value, - javaAgents += "io.kamon" % "kanela-agent" % kanelaVersion.value - ) - - def runningWithKanelaNotice: Def.Initialize[Task[RunningWithKanelaNotice]] = Def.task { - new RunningWithKanelaNotice(streams.value.log) - } - - class RunningWithKanelaNotice(log: Logger) extends PlayRunHook { - override def beforeStarted(): Unit = { - log.info(Colors.green("Running the application with the Kanela agent")) - } - } - - /** - * This ClassLoader gives a special treatment to the H2 JDBC Driver classes so that we can both instrument them while - * running on Development mode by loading the JDBC classes from the same ClassLoader as Kamon (the Application - * ClassLoader) and at the same time, allow for the H2 data to persist across runs, by letting loading of all other - * classes just bubble up to the common ClassLoader where the data is stored (in particlar, the Engine class which - * has references to all databases). - */ - class SbtKanelaClassLoader(name: String, urls: Array[URL], parent: ClassLoader, loadH2Driver: Boolean = false, - skipWhenLoadingResources: Boolean = false) extends KanelaOnSystemClassLoader(urls, parent) { - - override def toString = - name + "{" + getURLs.map(_.toString).mkString(", ") + "}" - - override protected def loadClass(name: String, resolve: Boolean): Class[_] = { - if(loadH2Driver && (name.equals("org.h2.Driver") || name.startsWith("org.h2.jdbc"))) { - var loadedClass = findLoadedClass(name) - if (loadedClass == null) - loadedClass = findClass(name) - - if(resolve) - resolveClass(loadedClass) - - loadedClass - - } else super.loadClass(name, resolve) - } - - override def getResources(name: String): java.util.Enumeration[java.net.URL] = { - if(skipWhenLoadingResources) - getParent.getResources(name) - else - super.getResources(name) - } - } -} diff --git a/sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala b/sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala deleted file mode 100644 index ebc452f..0000000 --- a/sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala +++ /dev/null @@ -1,143 +0,0 @@ -package play.sbt.run - -import kamon.instrumentation.sbt.SbtKanelaRunner -import kamon.instrumentation.sbt.play.KanelaReloader -import play.core.BuildLink -import play.dev.filewatch.{SourceModificationWatch => PlaySourceModificationWatch, WatchState => PlayWatchState} -import play.sbt.PlayImport.PlayKeys._ -import play.sbt.PlayInternalKeys._ -import play.sbt.run.PlayRun.{generatedSourceHandlers, getPollInterval, getSourcesFinder, sleepForPoolDelay} -import play.sbt.{Colors, PlayNonBlockingInteractionMode} -import sbt.Keys._ -import sbt.{AttributeKey, Compile, Def, InputTask, Keys, Project, State, TaskKey, Watched} - -import scala.annotation.tailrec - -object KanelaPlayRun extends PlayRunCompat { - - // This file was copied and modified from the URL bellow since there was no other sensible way to our - // current knowledge of changing the ClassLoaders. - // - // https://raw.githubusercontent.com/playframework/playframework/2.7.3/dev-mode/sbt-plugin/src/main/scala/play/sbt/run/PlayRun.scala - - val playWithKanelaRunTask = playRunTask(playRunHooks, playDependencyClasspath, - playReloaderClasspath, playAssetsClassLoader) - - def playRunTask( - runHooks: TaskKey[Seq[play.sbt.PlayRunHook]], - dependencyClasspath: TaskKey[Classpath], - reloaderClasspath: TaskKey[Classpath], - assetsClassLoader: TaskKey[ClassLoader => ClassLoader] - ): Def.Initialize[InputTask[Unit]] = Def.inputTask { - - val args = Def.spaceDelimited().parsed - - val state = Keys.state.value - val scope = resolvedScoped.value.scope - val interaction = playInteractionMode.value - - val reloadCompile = () => PlayReload.compile( - () => Project.runTask(playReload in scope, state).map(_._2).get, - () => Project.runTask(reloaderClasspath in scope, state).map(_._2).get, - () => Project.runTask(streamsManager in scope, state).map(_._2).get.toEither.right.toOption - ) - - lazy val devModeServer = KanelaReloader.startDevMode( - runHooks.value, - (javaOptions in sbt.Runtime).value, - playCommonClassloader.value, - dependencyClasspath.value.files, - reloadCompile, - assetsClassLoader.value, - // avoid monitoring same folder twice or folders that don't exist - playMonitoredFiles.value.distinct.filter(_.exists()), - fileWatchService.value, - generatedSourceHandlers, - playDefaultPort.value, - playDefaultAddress.value, - baseDirectory.value, - devSettings.value, - args, - (mainClass in (Compile, Keys.run)).value.get, - KanelaPlayRun, - SbtKanelaRunner.Keys.kanelaAgentJar.value - ) - - interaction match { - case nonBlocking: PlayNonBlockingInteractionMode => - nonBlocking.start(devModeServer) - case blocking => - devModeServer - - println() - println(Colors.green("(Server started, use Enter to stop and go back to the console...)")) - println() - - val maybeContinuous: Option[Watched] = watchContinuously(state, Keys.sbtVersion.value) - - maybeContinuous match { - case Some(watched) => - // ~ run mode - interaction.doWithoutEcho { - twiddleRunMonitor(watched, state, devModeServer.buildLink, Some(PlayWatchState.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[PlayWatchState] = None): Unit = { - val ContinuousState = AttributeKey[PlayWatchState]("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: PlaySourceModificationWatch.PathFinder = getSourcesFinder(watched, state) - val watchState = ws.getOrElse(state.get(ContinuousState).getOrElse(PlayWatchState.empty)) - - val (triggered, newWatchState, newState) = - try { - val (triggered: Boolean, newWatchState: PlayWatchState) = PlaySourceModificationWatch.watch(sourcesFinder, getPollInterval(watched), watchState)(shouldTerminate) - (triggered, newWatchState, state) - } catch { - case e: Exception => - val log = state.log - log.error("Error occurred obtaining files to watch. Terminating continuous execution...") - log.trace(e) - (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 - sleepForPoolDelay - - // Call back myself - twiddleRunMonitor(watched, newState, reloader, Some(newWatchState)) - } else { - () - } - } - -} diff --git a/sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala b/sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala deleted file mode 100644 index 0a54d2f..0000000 --- a/sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala +++ /dev/null @@ -1,145 +0,0 @@ -package play.sbt.run - -import kamon.instrumentation.sbt.SbtKanelaRunner -import kamon.instrumentation.sbt.play.KanelaReloader -import play.core.BuildLink -import play.dev.filewatch.{SourceModificationWatch => PlaySourceModificationWatch, WatchState => PlayWatchState} -import play.sbt.PlayImport.PlayKeys._ -import play.sbt.PlayInternalKeys._ -import play.sbt.run.PlayRun.{generatedSourceHandlers, getPollInterval, getSourcesFinder, sleepForPoolDelay} -import play.sbt.{Colors, PlayNonBlockingInteractionMode} -import sbt.Keys._ -import sbt.{AttributeKey, Compile, Def, InputTask, Keys, Project, State, TaskKey, Watched} - -import scala.annotation.tailrec - -object KanelaPlayRun extends PlayRunCompat { - - // This file was copied and modified from the URL bellow since there was no other sensible way to our - // current knowledge of changing the ClassLoaders. - // - // https://raw.githubusercontent.com/playframework/playframework/2.7.3/dev-mode/sbt-plugin/src/main/scala/play/sbt/run/PlayRun.scala - - val playWithKanelaRunTask = playRunTask(playRunHooks, playDependencyClasspath, - playReloaderClasspath, playAssetsClassLoader) - - def playRunTask( - runHooks: TaskKey[Seq[play.sbt.PlayRunHook]], - dependencyClasspath: TaskKey[Classpath], - reloaderClasspath: TaskKey[Classpath], - assetsClassLoader: TaskKey[ClassLoader => ClassLoader] - ): Def.Initialize[InputTask[Unit]] = Def.inputTask { - - val args = Def.spaceDelimited().parsed - - val state = Keys.state.value - val scope = resolvedScoped.value.scope - val interaction = playInteractionMode.value - - val reloadCompile = () => PlayReload.compile( - () => Project.runTask(playReload in scope, state).map(_._2).get, - () => Project.runTask(reloaderClasspath in scope, state).map(_._2).get, - () => Project.runTask(streamsManager in scope, state).map(_._2).get.toEither.right.toOption, - state, - scope - ) - - lazy val devModeServer = KanelaReloader.startDevMode( - runHooks.value, - (javaOptions in sbt.Runtime).value, - playCommonClassloader.value, - dependencyClasspath.value.files, - reloadCompile, - assetsClassLoader.value, - // avoid monitoring same folder twice or folders that don't exist - playMonitoredFiles.value.distinct.filter(_.exists()), - fileWatchService.value, - generatedSourceHandlers, - playDefaultPort.value, - playDefaultAddress.value, - baseDirectory.value, - devSettings.value, - args, - (mainClass in (Compile, Keys.run)).value.get, - KanelaPlayRun, - SbtKanelaRunner.Keys.kanelaAgentJar.value - ) - - interaction match { - case nonBlocking: PlayNonBlockingInteractionMode => - nonBlocking.start(devModeServer) - case blocking => - devModeServer - - println() - println(Colors.green("(Server started, use Enter to stop and go back to the console...)")) - println() - - val maybeContinuous: Option[Watched] = watchContinuously(state, Keys.sbtVersion.value) - - maybeContinuous match { - case Some(watched) => - // ~ run mode - interaction.doWithoutEcho { - twiddleRunMonitor(watched, state, devModeServer.buildLink, Some(PlayWatchState.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[PlayWatchState] = None): Unit = { - val ContinuousState = AttributeKey[PlayWatchState]("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: PlaySourceModificationWatch.PathFinder = getSourcesFinder(watched, state) - val watchState = ws.getOrElse(state.get(ContinuousState).getOrElse(PlayWatchState.empty)) - - val (triggered, newWatchState, newState) = - try { - val (triggered: Boolean, newWatchState: PlayWatchState) = PlaySourceModificationWatch.watch(sourcesFinder, getPollInterval(watched), watchState)(shouldTerminate) - (triggered, newWatchState, state) - } catch { - case e: Exception => - val log = state.log - log.error("Error occurred obtaining files to watch. Terminating continuous execution...") - log.trace(e) - (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 - sleepForPoolDelay - - // Call back myself - twiddleRunMonitor(watched, newState, reloader, Some(newWatchState)) - } else { - () - } - } - -} diff --git a/sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala b/sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala new file mode 100644 index 0000000..0a5cf1f --- /dev/null +++ b/sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala @@ -0,0 +1,348 @@ +// File contents partially copied from: +// https://github.com/playframework/playframework/blob/2.9.1/dev-mode/sbt-plugin/src/main/scala/sbt/PlayRun.scala + +package sbt + +import java.nio.file.Files +import scala.annotation.tailrec +import scala.sys.process.* +import sbt.* +import sbt.internal.io.PlaySource +import sbt.util.LoggerContext +import sbt.Keys.* +import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport.* +import com.typesafe.sbt.packager.Keys.executableScriptName +import com.typesafe.sbt.web.SbtWeb.autoImport.* +import kamon.instrumentation.sbt.SbtKanelaRunner +import play.core.BuildLink +import play.dev.filewatch.SourceModificationWatch as PlaySourceModificationWatch +import play.dev.filewatch.WatchState as PlayWatchState +import play.runsupport.AssetsClassLoader +import play.runsupport.Reloader +import play.runsupport.Reloader.GeneratedSourceMapping +import play.sbt.run.PlayReload +import play.sbt.Colors +import play.sbt.PlayImport.* +import play.sbt.PlayImport.PlayKeys.* +import play.sbt.PlayInteractionMode +import play.sbt.PlayInternalKeys.* +import play.sbt.PlayNonBlockingInteractionMode +import play.sbt.PlayRunHook +import play.sbt.StaticPlayNonBlockingInteractionMode +import play.twirl.compiler.MaybeGeneratedSource +import play.twirl.sbt.SbtTwirl + + +/** + * Provides mechanisms for running a Play application in sbt + */ +object KanelaPlayRun { + class TwirlSourceMapping extends GeneratedSourceMapping { + def getOriginalLine(generatedSource: File, line: Integer): Integer = { + MaybeGeneratedSource.unapply(generatedSource).map(_.mapLine(line): java.lang.Integer).orNull + } + } + + val twirlSourceHandler = new TwirlSourceMapping() + + val generatedSourceHandlers = SbtTwirl.defaultFormats.map { case (k, _) => s"scala.$k" -> twirlSourceHandler } + + val playDefaultRunTask = + playRunTask(playRunHooks, playDependencyClasspath, playReloaderClasspath, playAssetsClassLoader) + + private val playDefaultRunTaskNonBlocking = + playRunTask( + playRunHooks, + playDependencyClasspath, + playReloaderClasspath, + playAssetsClassLoader, + Some(StaticPlayNonBlockingInteractionMode) + ) + + val playDefaultBgRunTask = + playBgRunTask() + + def playRunTask( + runHooks: TaskKey[Seq[PlayRunHook]], + dependencyClasspath: TaskKey[Classpath], + reloaderClasspath: TaskKey[Classpath], + assetsClassLoader: TaskKey[ClassLoader => ClassLoader], + interactionMode: Option[PlayInteractionMode] = None + ): Def.Initialize[InputTask[(PlayInteractionMode, Boolean)]] = Def.inputTask { + val args = Def.spaceDelimited().parsed + + val state = Keys.state.value + val scope = resolvedScoped.value.scope + val interaction = interactionMode.getOrElse(playInteractionMode.value) + + val reloadCompile = () => { + // This code and the below Project.runTask(...) run outside of a user-called sbt command/task. + // It gets called much later, by code, not by user, when a request comes in which causes Play to re-compile. + // Since sbt 1.8.0 a LoggerContext closes after command/task that was run by a user is finished. + // Therefore we need to wrap this code with a new, open LoggerContext. + // See https://github.com/playframework/playframework/issues/11527 + var loggerContext: LoggerContext = null + try { + val newState = interaction match { + case _: PlayNonBlockingInteractionMode => + loggerContext = LoggerContext(useLog4J = state.get(Keys.useLog4J.key).getOrElse(false)) + state.put(Keys.loggerContext, loggerContext) + case _ => state + } + PlayReload.compile( + () => Project.runTask(scope / playReload, newState).map(_._2).get, + () => Project.runTask(scope / reloaderClasspath, newState).map(_._2).get, + () => Project.runTask(scope / streamsManager, newState).map(_._2).get.toEither.right.toOption, + newState, + scope + ) + } finally { + interaction match { + case _: PlayNonBlockingInteractionMode => loggerContext.close() + case _ => // no-op + } + } + + } + + lazy val devModeServer = play.runsupport.KanelaReloader.startDevMode( + runHooks.value, + (Runtime / javaOptions).value, + playCommonClassloader.value, + dependencyClasspath.value.files, + reloadCompile, + assetsClassLoader.value, + // avoid monitoring same folder twice or folders that don't exist + playMonitoredFiles.value.distinct.filter(_.exists()), + fileWatchService.value, + generatedSourceHandlers, + playDefaultPort.value, + playDefaultAddress.value, + baseDirectory.value, + devSettings.value, + args, + (Compile / run / mainClass).value.get, + KanelaPlayRun, + SbtKanelaRunner.Keys.kanelaAgentJar.value + ) + + val serverDidStart = interaction match { + case nonBlocking: PlayNonBlockingInteractionMode => nonBlocking.start(devModeServer) + case _ => + devModeServer + + println() + println(Colors.green("(Server started, use Enter to stop and go back to the console...)")) + println() + + try { + watchContinuously(state) match { + case Some(watched) => + // ~ run mode + interaction.doWithoutEcho { + twiddleRunMonitor(watched, state, devModeServer.buildLink, Some(PlayWatchState.empty)) + } + case None => + // run mode + interaction.waitForCancel() + } + } finally { + devModeServer.close() + println() + } + true + } + (interaction, serverDidStart) + } + + def playBgRunTask(): Def.Initialize[InputTask[JobHandle]] = Def.inputTask { + bgJobService.value.runInBackground(resolvedScoped.value, state.value) { (logger, workingDir) => + playDefaultRunTaskNonBlocking.evaluated match { + case (mode: PlayNonBlockingInteractionMode, serverDidStart) => + if (serverDidStart) { + try { + Thread.sleep(Long.MaxValue) // Sleep "forever" ;), gets interrupted by "bgStop " + } catch { + case _: InterruptedException => mode.stop() // shutdown dev server + } + } + } + } + } + + /** + * Monitor changes in ~run mode. + */ + @tailrec private def twiddleRunMonitor( + watched: Watched, + state: State, + reloader: BuildLink, + ws: Option[PlayWatchState] = None + ): Unit = { + val ContinuousState = + AttributeKey[PlayWatchState]("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 = () => { + watched.watchSources(state).iterator.flatMap(new PlaySource(_).getPaths).collect { + case p if Files.exists(p) => better.files.File(p) + } + } + + val watchState = ws.getOrElse(state.get(ContinuousState).getOrElse(PlayWatchState.empty)) + val pollInterval = watched.pollInterval.toMillis.toInt + + val (triggered, newWatchState, newState) = + try { + val (triggered, newWatchState) = + PlaySourceModificationWatch.watch(sourcesFinder, 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...") + log.trace(e) + (false, watchState, state.fail) + } + + if (triggered) { + // Then launch compile + Project.synchronized { + val start = System.currentTimeMillis + Project.runTask(Compile / compile, newState).get._2.toEither.map { _ => + val duration = System.currentTimeMillis - start match { + case ms if ms < 1000 => ms + "ms" + case seconds => (seconds / 1000) + "s" + } + println(s"[${Colors.green("success")}] Compiled in $duration") + } + } + + // Avoid launching too much compilation + Thread.sleep(Watched.PollDelay.toMillis) + + // Call back myself + twiddleRunMonitor(watched, newState, reloader, Some(newWatchState)) + } + } + + private def watchContinuously(state: State): Option[Watched] = { + for { + watched <- state.get(Watched.Configuration) + monitor <- state.get(Watched.ContinuousEventMonitor) + if monitor.state.count > 0 // assume we're in ~ run mode + } yield watched + } + + val playPrefixAndAssetsSetting = { + playPrefixAndAssets := assetsPrefix.value -> (Assets / WebKeys.public).value + } + + val playAllAssetsSetting = playAllAssets := Seq(playPrefixAndAssets.value) + + val playAssetsClassLoaderSetting = { + playAssetsClassLoader := { + val assets = playAllAssets.value + parent => new AssetsClassLoader(parent, assets) + } + } + + val playRunProdCommand = Command.args("runProd", "")(testProd) + + val playTestProdCommand = Command.args("testProd", "") { (state: State, args: Seq[String]) => + state.log.warn("The testProd command is deprecated, and will be removed in a future version of Play.") + state.log.warn("To test your application using production mode, run 'runProd' instead.") + testProd(state, args) + } + + val playStartCommand = Command.args("start", "") { (state: State, args: Seq[String]) => + state.log.warn("The start command is deprecated, and will be removed in a future version of Play.") + state.log.warn( + "To run Play in production mode, run 'stage' instead, and then execute the generated start script in target/universal/stage/bin." + ) + state.log.warn("To test your application using production mode, run 'runProd' instead.") + + testProd(state, args) + } + + private def testProd(state: State, args: Seq[String]): State = { + val extracted = Project.extract(state) + + val interaction = extracted.get(playInteractionMode) + val noExitSbt = args.contains("--no-exit-sbt") + val filtered = args.filterNot(Set("--no-exit-sbt")) + val devSettings = Seq.empty[(String, String)] // there are no dev settings in a prod website + + // Parse HTTP port argument + val (properties, httpPort, httpsPort, _) = + Reloader.filterArgs(filtered, extracted.get(playDefaultPort), extracted.get(playDefaultAddress), devSettings) + require(httpPort.isDefined || httpsPort.isDefined, "You have to specify https.port when http.port is disabled") + + def fail(state: State) = { + println() + println("Cannot start with errors.") + println() + state.fail + } + + Project.runTask(stage, state) match { + case None => fail(state) + case Some((state, Inc(_))) => fail(state) + case Some((state, Value(stagingDir))) => + val stagingBin = { + val path = (stagingDir / "bin" / extracted.get(executableScriptName)).getAbsolutePath + val isWin = System.getProperty("os.name").toLowerCase(java.util.Locale.ENGLISH).contains("win") + if (isWin) s"$path.bat" else path + } + val javaOpts = Project.runTask(Production / javaOptions, state).get._2.toEither.right.getOrElse(Nil) + + // 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" } ++ + javaOpts ++ + Seq(s"-Dhttp.port=${httpPort.getOrElse("disabled")}") + + new Thread { + override def run(): Unit = { + val exitCode = args.! + if (!noExitSbt) System.exit(exitCode) + } + }.start() + + val msg = + """| + |(Starting server. Type Ctrl+D to exit logs, the server will remain in background) + | """.stripMargin + println(Colors.green(msg)) + + interaction.waitForCancel() + println() + + if (noExitSbt) state + else state.exit(ok = true) + } + } + + val playStopProdCommand = Command.args("stopProd", "") { (state, args) => + stop(state) + if (args.contains("--no-exit-sbt")) state else state.copy(remainingCommands = Nil) + } + + def stop(state: State): Unit = { + val pidFile = Project.extract(state).get(Universal / stagingDirectory) / "RUNNING_PID" + if (pidFile.exists) { + val pid = IO.read(pidFile) + s"kill -15 $pid".! + // PID file will be deleted by a shutdown hook attached on start in ProdServerStart.scala + println(s"Stopped application with process ID $pid") + } else println(s"No PID file found at $pidFile. Are you sure the app is running?") + println() + } +} diff --git a/sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala b/sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala similarity index 70% rename from sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala rename to sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala index 9529474..0f181b3 100644 --- a/sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala +++ b/sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala @@ -1,57 +1,60 @@ // File contents partially copied from: -// https://raw.githubusercontent.com/playframework/playframework/2.7.3/dev-mode/run-support/src/main/scala/play/runsupport/Reloader.scala -package kamon.instrumentation.sbt.play - -import java.io.{Closeable, File} -import java.net.{URL, URLClassLoader} -import java.security.{AccessController, PrivilegedAction} +// https://raw.githubusercontent.com/playframework/playframework/2.9.1/dev-mode/play-run-support/src/main/scala/play/runsupport/Reloader.scala +package play.runsupport + +import java.io.Closeable +import java.io.File +import java.net.URL +import java.net.URLClassLoader +import java.security.AccessController +import java.security.PrivilegedAction import java.time.Instant import java.util.concurrent.atomic.AtomicReference -import java.util.{Timer, TimerTask} -import better.files.{File => _, _} -import SbtKanelaRunnerPlay.SbtKanelaClassLoader +import java.util.Timer +import scala.annotation.tailrec +import scala.collection.JavaConverters.* +import better.files.{File as _, *} import kamon.instrumentation.sbt.SbtKanelaRunner +import kamon.instrumentation.sbt.play.SbtKanelaRunnerPlay.SbtKanelaClassLoader import org.slf4j.LoggerFactory -import play.api.PlayException -import play.core.{Build, BuildLink} +import play.core.server.ReloadableServer +import play.core.Build +import play.core.BuildLink import play.dev.filewatch.FileWatchService -import play.runsupport.{NamedURLClassLoader, ServerStartException} -import play.runsupport.classloader.{ApplicationClassLoaderProvider, DelegatingClassLoader} -import play.runsupport.{AssetsClassLoader, RunHook} -import play.runsupport.Reloader.{CompileFailure, CompileResult, CompileSuccess, GeneratedSourceMapping, Source} - -import scala.collection.JavaConverters._ +import play.runsupport.classloader.ApplicationClassLoaderProvider +import play.runsupport.classloader.DelegatingClassLoader object KanelaReloader { - type ClassLoaderCreator = (String, Array[URL], ClassLoader) => ClassLoader - val SystemProperty = "-D([^=]+)=(.*)".r private val accessControlContext = AccessController.getContext /** - * Execute f with context ClassLoader of Reloader + * Execute f with context ClassLoader of KanelaReloader */ private def withReloaderContextClassLoader[T](f: => T): T = { val thread = Thread.currentThread val oldLoader = thread.getContextClassLoader // we use accessControlContext & AccessController to avoid a ClassLoader leak (ProtectionDomain class) - AccessController.doPrivileged(new PrivilegedAction[T]() { - def run: T = { - try { - thread.setContextClassLoader(classOf[KanelaReloader].getClassLoader) - f - } finally { - thread.setContextClassLoader(oldLoader) + AccessController.doPrivileged( + new PrivilegedAction[T]() { + def run: T = { + try { + thread.setContextClassLoader(classOf[KanelaReloader].getClassLoader) + f + } finally { + thread.setContextClassLoader(oldLoader) + } } - } - }, accessControlContext) + }, + accessControlContext + ) } /** - * 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") - */ + * 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") + */ def extractSystemProperties(javaOptions: Seq[String]): Seq[(String, String)] = { javaOptions.collect { case SystemProperty(key, value) => key -> value } } @@ -65,10 +68,11 @@ object KanelaReloader { } def filterArgs( - args: Seq[String], - defaultHttpPort: Int, - defaultHttpAddress: String, - devSettings: Seq[(String, String)]): (Seq[(String, String)], Option[Int], Option[Int], String) = { + args: Seq[String], + defaultHttpPort: Int, + defaultHttpAddress: String, + devSettings: Seq[(String, String)] + ): (Seq[(String, String)], Option[Int], Option[Int], String) = { val (propertyArgs, otherArgs) = args.partition(_.startsWith("-D")) val properties = propertyArgs.map { @@ -83,9 +87,9 @@ object KanelaReloader { def parsePortValue(portValue: Option[String], defaultValue: Option[Int] = None): Option[Int] = { portValue match { - case None => defaultValue + case None => defaultValue case Some("disabled") => None - case Some(s) => Some(parsePort(s)) + case Some(s) => Some(parsePort(s)) } } @@ -123,46 +127,50 @@ object KanelaReloader { def urls(cp: Seq[File]): Array[URL] = cp.map(_.toURI.toURL).toArray - def assetsClassLoader(allAssets: Seq[(String, File)])(parent: ClassLoader): ClassLoader = new AssetsClassLoader(parent, allAssets) + def assetsClassLoader(allAssets: Seq[(String, File)])(parent: ClassLoader): ClassLoader = + new AssetsClassLoader(parent, allAssets) def commonClassLoader(classpath: Seq[File]) = { lazy val commonJars: PartialFunction[java.io.File, java.net.URL] = { case jar if jar.getName.startsWith("h2-") || jar.getName == "h2.jar" => jar.toURI.toURL } - new java.net.URLClassLoader(classpath.collect(commonJars).toArray, null /* important here, don't depend of the sbt classLoader! */ ) { + new java.net.URLClassLoader( + classpath.collect(commonJars).toArray, + null /* important here, don't depend of the sbt classLoader! */ + ) { override def toString = "Common ClassLoader: " + getURLs.map(_.toString).mkString(",") } } /** - * Dev server - */ + * Dev server + */ trait DevServer extends Closeable { - val buildLink: BuildLink + def buildLink: BuildLink /** Allows to register a listener that will be triggered a monitored file is changed. */ def addChangeListener(f: () => Unit): Unit - /** Reloads the application.*/ + /** Reloads the application. */ def reload(): Unit } /** - * Start the server in dev mode - * - * @return A closeable that can be closed to stop the server - */ + * Start the server in dev mode + * + * @return A closeable that can be closed to stop the server + */ def startDevMode( runHooks: Seq[RunHook], javaOptions: Seq[String], commonClassLoader: ClassLoader, dependencyClasspath: Seq[File], - reloadCompile: () => CompileResult, + reloadCompile: () => Reloader.CompileResult, assetsClassLoader: ClassLoader => ClassLoader, monitoredFiles: Seq[File], fileWatchService: FileWatchService, - generatedSourceHandlers: Map[String, GeneratedSourceMapping], + generatedSourceHandlers: Map[String, Reloader.GeneratedSourceMapping], defaultHttpPort: Int, defaultHttpAddress: String, projectPath: File, @@ -172,7 +180,6 @@ object KanelaReloader { reloadLock: AnyRef, kanelaAgentJar: File ): DevServer = { - val (systemPropertiesArgs, httpPort, httpsPort, httpAddress) = filterArgs(args, defaultHttpPort, defaultHttpAddress, devSettings) val systemPropertiesJavaOptions = extractSystemProperties(javaOptions) @@ -183,7 +190,9 @@ object KanelaReloader { // but who knows how they will be set in a future change) also set the actual configs they are shortcuts for. // So when reading the actual (long) keys from the config (play.server.http...) the values match and are correct. val systemPropertiesAddressPorts = Seq("play.server.http.address" -> httpAddress) ++ - httpPort.map(port => Seq("play.server.http.port" -> port.toString)).getOrElse(Nil) ++ + httpPort + .map(port => Seq("play.server.http.port" -> port.toString)) + .getOrElse(Seq("play.server.http.port" -> "disabled")) ++ httpsPort.map(port => Seq("play.server.https.port" -> port.toString)).getOrElse(Nil) // Properties are combined in this specific order so that command line @@ -237,13 +246,18 @@ object KanelaReloader { 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, Build.sharedClasses, buildLoader, new ApplicationClassLoaderProvider { - def get: URLClassLoader = { reloader.getClassLoader.orNull } - }) + * 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, + Build.sharedClasses, + buildLoader, + new ApplicationClassLoaderProvider { + def get: URLClassLoader = { reloader.getClassLoader.orNull } + } + ) lazy val applicationLoader = new SbtKanelaClassLoader("DependencyClassLoader", urls(dependencyClasspath), delegatingLoader, loadH2Driver = true) @@ -279,24 +293,29 @@ object KanelaReloader { SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, applicationLoader, clearRegistry = true) val server = { - val mainClass = applicationLoader.loadClass(mainClassName) + type ServerStart = { + def mainDevHttpAndHttpsMode( + buildLink: BuildLink, + httpPort: Int, + httpsPort: Int, + httpAddress: String + ): ReloadableServer + + def mainDevHttpMode(buildLink: BuildLink, httpPort: Int, httpAddress: String): ReloadableServer + + def mainDevOnlyHttpsMode(buildLink: BuildLink, httpsPort: Int, httpAddress: String): ReloadableServer + } + + import scala.language.reflectiveCalls + val mainClass = applicationLoader.loadClass(mainClassName + "$") + val mainObject = mainClass.getField("MODULE$").get(null).asInstanceOf[ServerStart] + if (httpPort.isDefined && httpsPort.isDefined) { - val mainDev = mainClass.getMethod( - "mainDevHttpAndHttpsMode", - classOf[BuildLink], - classOf[Int], - classOf[Int], - classOf[String] - ) - mainDev - .invoke(null, reloader, httpPort.get: java.lang.Integer, httpsPort.get: java.lang.Integer, httpAddress) - .asInstanceOf[play.core.server.ReloadableServer] + mainObject.mainDevHttpAndHttpsMode(reloader, httpPort.get, httpsPort.get, httpAddress) } else if (httpPort.isDefined) { - val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[Int], classOf[String]) - mainDev.invoke(null, reloader, httpPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ReloadableServer] + mainObject.mainDevHttpMode(reloader, httpPort.get, httpAddress) } else { - val mainDev = mainClass.getMethod("mainDevOnlyHttpsMode", classOf[BuildLink], classOf[Int], classOf[String]) - mainDev.invoke(null, reloader, httpsPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ReloadableServer] + mainObject.mainDevOnlyHttpsMode(reloader, httpsPort.get, httpAddress) } } @@ -304,9 +323,9 @@ object KanelaReloader { runHooks.run(_.afterStarted()) new DevServer { - val buildLink = reloader + val buildLink = reloader def addChangeListener(f: () => Unit): Unit = reloader.addChangeListener(f) - def reload(): Unit = server.reload() + def reload(): Unit = server.reload() def close(): Unit = { server.stop() reloader.close() @@ -331,7 +350,8 @@ object KanelaReloader { } } // Convert play-server exceptions to our to our ServerStartException - def getRootCause(t: Throwable): Throwable = if (t.getCause == null) t else getRootCause(t.getCause) + @tailrec def getRootCause(t: Throwable): Throwable = + if (t.getCause == null) t else getRootCause(t.getCause) if (getRootCause(e).getClass.getName == "play.core.server.ServerListenException") { throw new ServerStartException(e) } @@ -340,20 +360,29 @@ object KanelaReloader { } /** - * Start the server without hot reloading - */ - def startNoReload(parentClassLoader: ClassLoader, dependencyClasspath: Seq[File], buildProjectPath: File, - devSettings: Seq[(String, String)], httpPort: Int, mainClassName: String): DevServer = { + * Start the server without hot reloading + */ + def startNoReload( + parentClassLoader: ClassLoader, + dependencyClasspath: Seq[File], + buildProjectPath: File, + devSettings: Seq[(String, String)], + httpPort: Int, + mainClassName: String + ): DevServer = { val buildLoader = this.getClass.getClassLoader lazy val delegatingLoader: ClassLoader = new DelegatingClassLoader( parentClassLoader, - Build.sharedClasses, buildLoader, new ApplicationClassLoaderProvider { + Build.sharedClasses, + buildLoader, + new ApplicationClassLoaderProvider { def get: URLClassLoader = { applicationLoader } - }) + } + ) - lazy val applicationLoader = new NamedURLClassLoader("DependencyClassLoader", urls(dependencyClasspath), - delegatingLoader) + lazy val applicationLoader = + new NamedURLClassLoader("DependencyClassLoader", urls(dependencyClasspath), delegatingLoader) val _buildLink = new BuildLink { private val initialized = new java.util.concurrent.atomic.AtomicBoolean(false) @@ -361,15 +390,23 @@ object KanelaReloader { if (initialized.compareAndSet(false, true)) applicationLoader else null // this means nothing to reload } - override def projectPath(): File = buildProjectPath - override def settings(): java.util.Map[String, String] = devSettings.toMap.asJava - override def forceReload(): Unit = () + override def projectPath(): File = buildProjectPath + override def settings(): java.util.Map[String, String] = devSettings.toMap.asJava + override def forceReload(): Unit = () override def findSource(className: String, line: Integer): Array[AnyRef] = null } - val mainClass = applicationLoader.loadClass(mainClassName) - val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[Int]) - val server = mainDev.invoke(null, _buildLink, httpPort: java.lang.Integer).asInstanceOf[play.core.server.ReloadableServer] + type ServerStart = { + def mainDevHttpMode( + buildLink: BuildLink, + httpPort: Int, + ): ReloadableServer + } + + import scala.language.reflectiveCalls + val mainClass = applicationLoader.loadClass(mainClassName + "$") + val mainObject = mainClass.getField("MODULE$").get(null).asInstanceOf[ServerStart] + val server = mainObject.mainDevHttpMode(_buildLink, httpPort) server.reload() // it's important to initialize the server @@ -379,26 +416,27 @@ object KanelaReloader { /** Allows to register a listener that will be triggered a monitored file is changed. */ def addChangeListener(f: () => Unit): Unit = () - /** Reloads the application.*/ + /** Reloads the application. */ def reload(): Unit = () def close(): Unit = server.stop() } } - } -class KanelaReloader( - reloadCompile: () => CompileResult, - baseLoader: ClassLoader, - val projectPath: File, - devSettings: Seq[(String, String)], - monitoredFiles: Seq[File], - fileWatchService: FileWatchService, - generatedSourceHandlers: Map[String, GeneratedSourceMapping], - reloadLock: AnyRef, - kanelaAgentJar: File) extends BuildLink { +import play.runsupport.KanelaReloader._ +class KanelaReloader( + reloadCompile: () => Reloader.CompileResult, + baseLoader: ClassLoader, + val projectPath: File, + devSettings: Seq[(String, String)], + monitoredFiles: Seq[File], + fileWatchService: FileWatchService, + generatedSourceHandlers: Map[String, Reloader.GeneratedSourceMapping], + reloadLock: AnyRef, + kanelaAgentJar: File +) extends BuildLink { // The current classloader for the application @volatile private var currentApplicationClassLoader: Option[URLClassLoader] = None // Flag to force a reload on the next request. @@ -408,7 +446,7 @@ class KanelaReloader( // Whether any source files have changed since the last request. @volatile private var changed = false // The last successful compile results. Used for rendering nice errors. - @volatile private var currentSourceMap = Option.empty[Map[String, Source]] + @volatile private var currentSourceMap = Option.empty[Map[String, Reloader.Source]] // Last time the classpath was modified in millis. Used to determine whether anything on the classpath has // changed as a result of compilation, and therefore a new classloader is needed and the app needs to be reloaded. @volatile private var lastModified: Long = 0L @@ -417,25 +455,13 @@ class KanelaReloader( private val fileLastChanged = new AtomicReference[Instant]() // Create the watcher, updates the changed boolean when a file has changed. - private val watcher = fileWatchService.watch(monitoredFiles, () => { - changed = true - }) + private val watcher = fileWatchService.watch(monitoredFiles, () => changed = true) private val classLoaderVersion = new java.util.concurrent.atomic.AtomicInteger(0) private val quietTimeTimer = new Timer("reloader-timer", true) private val listeners = new java.util.concurrent.CopyOnWriteArrayList[() => Unit]() - private val quietPeriodMs: Long = 200L - private def onChange(): Unit = { - val now = Instant.now() - fileLastChanged.set(now) - // set timer task - quietTimeTimer.schedule(new TimerTask { - override def run(): Unit = quietPeriodFinished(now) - }, quietPeriodMs) - } - private def quietPeriodFinished(start: Instant): Unit = { // If our start time is equal to the most recent start time stored, then execute the handlers and set the most // recent time to null, otherwise don't do anything. @@ -448,55 +474,51 @@ class KanelaReloader( def addChangeListener(f: () => Unit): Unit = listeners.add(f) /** - * Contrary to its name, this doesn't necessarily reload the app. It is invoked on every request, and will only - * trigger a reload of the app if something has changed. - * - * Since this communicates across classloaders, it must return only simple objects. - * - * - * @return Either - * - Throwable - If something went wrong (eg, a compile error). - * - ClassLoader - If the classloader has changed, and the application should be reloaded. - * - null - If nothing changed. - */ + * Contrary to its name, this doesn't necessarily reload the app. It is invoked on every request, and will only + * trigger a reload of the app if something has changed. + * + * Since this communicates across classloaders, it must return only simple objects. + * + * @return Either + * - Throwable - If something went wrong (eg, a compile error). + * - ClassLoader - If the classloader has changed, and the application should be reloaded. + * - null - If nothing changed. + */ def reload: AnyRef = { reloadLock.synchronized { if (changed || forceReloadNextTime || currentSourceMap.isEmpty || currentApplicationClassLoader.isEmpty) { - val shouldReload = forceReloadNextTime changed = false forceReloadNextTime = false - // use Reloader context ClassLoader to avoid ClassLoader leaks in sbt/scala-compiler threads + // use KanelaReloader context ClassLoader to avoid ClassLoader leaks in sbt/scala-compiler threads KanelaReloader.withReloaderContextClassLoader { // Run the reload task, which will trigger everything to compile reloadCompile() match { - case CompileFailure(exception) => + case Reloader.CompileFailure(exception) => // We force reload next time because compilation failed this time forceReloadNextTime = true exception - case CompileSuccess(sourceMap, classpath) => - + case Reloader.CompileSuccess(sourceMap, classpath) => currentSourceMap = Some(sourceMap) // We only want to reload if the classpath has changed. Assets don't live on the classpath, so // they won't trigger a reload. - val classpathFiles = classpath.iterator.filter(_.exists()).flatMap(_.toScala.listRecursively).map(_.toJava) + val classpathFiles = + classpath.iterator.filter(_.exists()).flatMap(_.toScala.listRecursively).map(_.toJava) val newLastModified = - classpathFiles.foldLeft(0L) { (acc, file) => - math.max(acc, file.lastModified) - } + classpathFiles.foldLeft(0L) { (acc, file) => math.max(acc, file.lastModified) } val triggered = newLastModified > lastModified lastModified = newLastModified if (triggered || shouldReload || currentApplicationClassLoader.isEmpty) { // Create a new classloader val version = classLoaderVersion.incrementAndGet - val name = "ReloadableClassLoader(v" + version + ")" - val urls = KanelaReloader.urls(classpath) - val loader = new SbtKanelaClassLoader(name, urls, baseLoader, skipWhenLoadingResources = true) + val name = "ReloadableClassLoader(v" + version + ")" + val urls = KanelaReloader.urls(classpath) + val loader = new SbtKanelaClassLoader(name, urls, baseLoader, skipWhenLoadingResources = true) SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, loader, clearRegistry = false) currentApplicationClassLoader = Some(loader) loader @@ -549,5 +571,4 @@ class KanelaReloader( } def getClassLoader = currentApplicationClassLoader - } diff --git a/sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala b/sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala similarity index 95% rename from sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala rename to sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala index a5f8ef1..73b5e34 100644 --- a/sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala +++ b/sbt-kanela-runner-play-2.9/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala @@ -24,7 +24,7 @@ import com.lightbend.sbt.javaagent.JavaAgent import com.lightbend.sbt.javaagent.JavaAgent.JavaAgentKeys.javaAgents import kamon.instrumentation.sbt.SbtKanelaRunner.Keys.kanelaVersion import kamon.instrumentation.sbt.{KanelaOnSystemClassLoader, SbtKanelaRunner} -import play.sbt.run.KanelaPlayRun +import sbt.KanelaPlayRun import sbt.Keys._ import sbt._ @@ -34,7 +34,8 @@ object SbtKanelaRunnerPlay extends AutoPlugin { override def requires = PlayWeb && SbtKanelaRunner && JavaAgent override def projectSettings: Seq[Setting[_]] = Seq( - Keys.run in Compile := KanelaPlayRun.playWithKanelaRunTask.evaluated, + Compile / Keys.run := KanelaPlayRun.playDefaultRunTask.evaluated, + Compile / Keys.bgRun := KanelaPlayRun.playDefaultBgRunTask.evaluated, playRunHooks += runningWithKanelaNotice.value, javaAgents += "io.kamon" % "kanela-agent" % kanelaVersion.value ) diff --git a/sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala b/sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala new file mode 100644 index 0000000..4231b38 --- /dev/null +++ b/sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/KanelaPlayRun.scala @@ -0,0 +1,348 @@ +// File contents partially copied from: +// https://github.com/playframework/playframework/blob/3.0.1/dev-mode/sbt-plugin/src/main/scala/sbt/PlayRun.scala + +package sbt + +import java.nio.file.Files +import scala.annotation.tailrec +import scala.sys.process.* +import sbt.* +import sbt.internal.io.PlaySource +import sbt.util.LoggerContext +import sbt.Keys.* +import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport.* +import com.typesafe.sbt.packager.Keys.executableScriptName +import com.typesafe.sbt.web.SbtWeb.autoImport.* +import kamon.instrumentation.sbt.SbtKanelaRunner +import play.core.BuildLink +import play.dev.filewatch.SourceModificationWatch as PlaySourceModificationWatch +import play.dev.filewatch.WatchState as PlayWatchState +import play.runsupport.AssetsClassLoader +import play.runsupport.Reloader +import play.runsupport.Reloader.GeneratedSourceMapping +import play.sbt.run.PlayReload +import play.sbt.Colors +import play.sbt.PlayImport.* +import play.sbt.PlayImport.PlayKeys.* +import play.sbt.PlayInteractionMode +import play.sbt.PlayInternalKeys.* +import play.sbt.PlayNonBlockingInteractionMode +import play.sbt.PlayRunHook +import play.sbt.StaticPlayNonBlockingInteractionMode +import play.twirl.compiler.MaybeGeneratedSource +import play.twirl.sbt.SbtTwirl + + +/** + * Provides mechanisms for running a Play application in sbt + */ +object KanelaPlayRun { + class TwirlSourceMapping extends GeneratedSourceMapping { + def getOriginalLine(generatedSource: File, line: Integer): Integer = { + MaybeGeneratedSource.unapply(generatedSource).map(_.mapLine(line): java.lang.Integer).orNull + } + } + + val twirlSourceHandler = new TwirlSourceMapping() + + val generatedSourceHandlers = SbtTwirl.defaultFormats.map { case (k, _) => s"scala.$k" -> twirlSourceHandler } + + val playDefaultRunTask = + playRunTask(playRunHooks, playDependencyClasspath, playReloaderClasspath, playAssetsClassLoader) + + private val playDefaultRunTaskNonBlocking = + playRunTask( + playRunHooks, + playDependencyClasspath, + playReloaderClasspath, + playAssetsClassLoader, + Some(StaticPlayNonBlockingInteractionMode) + ) + + val playDefaultBgRunTask = + playBgRunTask() + + def playRunTask( + runHooks: TaskKey[Seq[PlayRunHook]], + dependencyClasspath: TaskKey[Classpath], + reloaderClasspath: TaskKey[Classpath], + assetsClassLoader: TaskKey[ClassLoader => ClassLoader], + interactionMode: Option[PlayInteractionMode] = None + ): Def.Initialize[InputTask[(PlayInteractionMode, Boolean)]] = Def.inputTask { + val args = Def.spaceDelimited().parsed + + val state = Keys.state.value + val scope = resolvedScoped.value.scope + val interaction = interactionMode.getOrElse(playInteractionMode.value) + + val reloadCompile = () => { + // This code and the below Project.runTask(...) run outside of a user-called sbt command/task. + // It gets called much later, by code, not by user, when a request comes in which causes Play to re-compile. + // Since sbt 1.8.0 a LoggerContext closes after command/task that was run by a user is finished. + // Therefore we need to wrap this code with a new, open LoggerContext. + // See https://github.com/playframework/playframework/issues/11527 + var loggerContext: LoggerContext = null + try { + val newState = interaction match { + case _: PlayNonBlockingInteractionMode => + loggerContext = LoggerContext(useLog4J = state.get(Keys.useLog4J.key).getOrElse(false)) + state.put(Keys.loggerContext, loggerContext) + case _ => state + } + PlayReload.compile( + () => Project.runTask(scope / playReload, newState).map(_._2).get, + () => Project.runTask(scope / reloaderClasspath, newState).map(_._2).get, + () => Project.runTask(scope / streamsManager, newState).map(_._2).get.toEither.right.toOption, + newState, + scope + ) + } finally { + interaction match { + case _: PlayNonBlockingInteractionMode => loggerContext.close() + case _ => // no-op + } + } + + } + + lazy val devModeServer = play.runsupport.KanelaReloader.startDevMode( + runHooks.value, + (Runtime / javaOptions).value, + playCommonClassloader.value, + dependencyClasspath.value.files, + reloadCompile, + assetsClassLoader.value, + // avoid monitoring same folder twice or folders that don't exist + playMonitoredFiles.value.distinct.filter(_.exists()), + fileWatchService.value, + generatedSourceHandlers, + playDefaultPort.value, + playDefaultAddress.value, + baseDirectory.value, + devSettings.value, + args, + (Compile / run / mainClass).value.get, + KanelaPlayRun, + SbtKanelaRunner.Keys.kanelaAgentJar.value + ) + + val serverDidStart = interaction match { + case nonBlocking: PlayNonBlockingInteractionMode => nonBlocking.start(devModeServer) + case _ => + devModeServer + + println() + println(Colors.green("(Server started, use Enter to stop and go back to the console...)")) + println() + + try { + watchContinuously(state) match { + case Some(watched) => + // ~ run mode + interaction.doWithoutEcho { + twiddleRunMonitor(watched, state, devModeServer.buildLink, Some(PlayWatchState.empty)) + } + case None => + // run mode + interaction.waitForCancel() + } + } finally { + devModeServer.close() + println() + } + true + } + (interaction, serverDidStart) + } + + def playBgRunTask(): Def.Initialize[InputTask[JobHandle]] = Def.inputTask { + bgJobService.value.runInBackground(resolvedScoped.value, state.value) { (logger, workingDir) => + playDefaultRunTaskNonBlocking.evaluated match { + case (mode: PlayNonBlockingInteractionMode, serverDidStart) => + if (serverDidStart) { + try { + Thread.sleep(Long.MaxValue) // Sleep "forever" ;), gets interrupted by "bgStop " + } catch { + case _: InterruptedException => mode.stop() // shutdown dev server + } + } + } + } + } + + /** + * Monitor changes in ~run mode. + */ + @tailrec private def twiddleRunMonitor( + watched: Watched, + state: State, + reloader: BuildLink, + ws: Option[PlayWatchState] = None + ): Unit = { + val ContinuousState = + AttributeKey[PlayWatchState]("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 = () => { + watched.watchSources(state).iterator.flatMap(new PlaySource(_).getPaths).collect { + case p if Files.exists(p) => better.files.File(p) + } + } + + val watchState = ws.getOrElse(state.get(ContinuousState).getOrElse(PlayWatchState.empty)) + val pollInterval = watched.pollInterval.toMillis.toInt + + val (triggered, newWatchState, newState) = + try { + val (triggered, newWatchState) = + PlaySourceModificationWatch.watch(sourcesFinder, 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...") + log.trace(e) + (false, watchState, state.fail) + } + + if (triggered) { + // Then launch compile + Project.synchronized { + val start = System.currentTimeMillis + Project.runTask(Compile / compile, newState).get._2.toEither.map { _ => + val duration = System.currentTimeMillis - start match { + case ms if ms < 1000 => ms + "ms" + case seconds => (seconds / 1000) + "s" + } + println(s"[${Colors.green("success")}] Compiled in $duration") + } + } + + // Avoid launching too much compilation + Thread.sleep(Watched.PollDelay.toMillis) + + // Call back myself + twiddleRunMonitor(watched, newState, reloader, Some(newWatchState)) + } + } + + private def watchContinuously(state: State): Option[Watched] = { + for { + watched <- state.get(Watched.Configuration) + monitor <- state.get(Watched.ContinuousEventMonitor) + if monitor.state.count > 0 // assume we're in ~ run mode + } yield watched + } + + val playPrefixAndAssetsSetting = { + playPrefixAndAssets := assetsPrefix.value -> (Assets / WebKeys.public).value + } + + val playAllAssetsSetting = playAllAssets := Seq(playPrefixAndAssets.value) + + val playAssetsClassLoaderSetting = { + playAssetsClassLoader := { + val assets = playAllAssets.value + parent => new AssetsClassLoader(parent, assets) + } + } + + val playRunProdCommand = Command.args("runProd", "")(testProd) + + val playTestProdCommand = Command.args("testProd", "") { (state: State, args: Seq[String]) => + state.log.warn("The testProd command is deprecated, and will be removed in a future version of Play.") + state.log.warn("To test your application using production mode, run 'runProd' instead.") + testProd(state, args) + } + + val playStartCommand = Command.args("start", "") { (state: State, args: Seq[String]) => + state.log.warn("The start command is deprecated, and will be removed in a future version of Play.") + state.log.warn( + "To run Play in production mode, run 'stage' instead, and then execute the generated start script in target/universal/stage/bin." + ) + state.log.warn("To test your application using production mode, run 'runProd' instead.") + + testProd(state, args) + } + + private def testProd(state: State, args: Seq[String]): State = { + val extracted = Project.extract(state) + + val interaction = extracted.get(playInteractionMode) + val noExitSbt = args.contains("--no-exit-sbt") + val filtered = args.filterNot(Set("--no-exit-sbt")) + val devSettings = Seq.empty[(String, String)] // there are no dev settings in a prod website + + // Parse HTTP port argument + val (properties, httpPort, httpsPort, _) = + Reloader.filterArgs(filtered, extracted.get(playDefaultPort), extracted.get(playDefaultAddress), devSettings) + require(httpPort.isDefined || httpsPort.isDefined, "You have to specify https.port when http.port is disabled") + + def fail(state: State) = { + println() + println("Cannot start with errors.") + println() + state.fail + } + + Project.runTask(stage, state) match { + case None => fail(state) + case Some((state, Inc(_))) => fail(state) + case Some((state, Value(stagingDir))) => + val stagingBin = { + val path = (stagingDir / "bin" / extracted.get(executableScriptName)).getAbsolutePath + val isWin = System.getProperty("os.name").toLowerCase(java.util.Locale.ENGLISH).contains("win") + if (isWin) s"$path.bat" else path + } + val javaOpts = Project.runTask(Production / javaOptions, state).get._2.toEither.right.getOrElse(Nil) + + // 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" } ++ + javaOpts ++ + Seq(s"-Dhttp.port=${httpPort.getOrElse("disabled")}") + + new Thread { + override def run(): Unit = { + val exitCode = args.! + if (!noExitSbt) System.exit(exitCode) + } + }.start() + + val msg = + """| + |(Starting server. Type Ctrl+D to exit logs, the server will remain in background) + | """.stripMargin + println(Colors.green(msg)) + + interaction.waitForCancel() + println() + + if (noExitSbt) state + else state.exit(ok = true) + } + } + + val playStopProdCommand = Command.args("stopProd", "") { (state, args) => + stop(state) + if (args.contains("--no-exit-sbt")) state else state.copy(remainingCommands = Nil) + } + + def stop(state: State): Unit = { + val pidFile = Project.extract(state).get(Universal / stagingDirectory) / "RUNNING_PID" + if (pidFile.exists) { + val pid = IO.read(pidFile) + s"kill -15 $pid".! + // PID file will be deleted by a shutdown hook attached on start in ProdServerStart.scala + println(s"Stopped application with process ID $pid") + } else println(s"No PID file found at $pidFile. Are you sure the app is running?") + println() + } +} diff --git a/sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala b/sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala similarity index 70% rename from sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala rename to sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala index 9529474..23b684c 100644 --- a/sbt-kanela-runner-play-2.8/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala +++ b/sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/KanelaReloader.scala @@ -1,57 +1,60 @@ // File contents partially copied from: -// https://raw.githubusercontent.com/playframework/playframework/2.7.3/dev-mode/run-support/src/main/scala/play/runsupport/Reloader.scala -package kamon.instrumentation.sbt.play - -import java.io.{Closeable, File} -import java.net.{URL, URLClassLoader} -import java.security.{AccessController, PrivilegedAction} +// https://raw.githubusercontent.com/playframework/playframework/3.0.1/dev-mode/play-run-support/src/main/scala/play/runsupport/Reloader.scala +package play.runsupport + +import java.io.Closeable +import java.io.File +import java.net.URL +import java.net.URLClassLoader +import java.security.AccessController +import java.security.PrivilegedAction import java.time.Instant import java.util.concurrent.atomic.AtomicReference -import java.util.{Timer, TimerTask} -import better.files.{File => _, _} -import SbtKanelaRunnerPlay.SbtKanelaClassLoader +import java.util.Timer +import scala.annotation.tailrec +import scala.collection.JavaConverters.* +import better.files.{File as _, *} import kamon.instrumentation.sbt.SbtKanelaRunner +import kamon.instrumentation.sbt.play.SbtKanelaRunnerPlay.SbtKanelaClassLoader import org.slf4j.LoggerFactory -import play.api.PlayException -import play.core.{Build, BuildLink} +import play.core.server.ReloadableServer +import play.core.Build +import play.core.BuildLink import play.dev.filewatch.FileWatchService -import play.runsupport.{NamedURLClassLoader, ServerStartException} -import play.runsupport.classloader.{ApplicationClassLoaderProvider, DelegatingClassLoader} -import play.runsupport.{AssetsClassLoader, RunHook} -import play.runsupport.Reloader.{CompileFailure, CompileResult, CompileSuccess, GeneratedSourceMapping, Source} - -import scala.collection.JavaConverters._ +import play.runsupport.classloader.ApplicationClassLoaderProvider +import play.runsupport.classloader.DelegatingClassLoader object KanelaReloader { - type ClassLoaderCreator = (String, Array[URL], ClassLoader) => ClassLoader - val SystemProperty = "-D([^=]+)=(.*)".r private val accessControlContext = AccessController.getContext /** - * Execute f with context ClassLoader of Reloader + * Execute f with context ClassLoader of KanelaReloader */ private def withReloaderContextClassLoader[T](f: => T): T = { val thread = Thread.currentThread val oldLoader = thread.getContextClassLoader // we use accessControlContext & AccessController to avoid a ClassLoader leak (ProtectionDomain class) - AccessController.doPrivileged(new PrivilegedAction[T]() { - def run: T = { - try { - thread.setContextClassLoader(classOf[KanelaReloader].getClassLoader) - f - } finally { - thread.setContextClassLoader(oldLoader) + AccessController.doPrivileged( + new PrivilegedAction[T]() { + def run: T = { + try { + thread.setContextClassLoader(classOf[KanelaReloader].getClassLoader) + f + } finally { + thread.setContextClassLoader(oldLoader) + } } - } - }, accessControlContext) + }, + accessControlContext + ) } /** - * 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") - */ + * 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") + */ def extractSystemProperties(javaOptions: Seq[String]): Seq[(String, String)] = { javaOptions.collect { case SystemProperty(key, value) => key -> value } } @@ -65,10 +68,11 @@ object KanelaReloader { } def filterArgs( - args: Seq[String], - defaultHttpPort: Int, - defaultHttpAddress: String, - devSettings: Seq[(String, String)]): (Seq[(String, String)], Option[Int], Option[Int], String) = { + args: Seq[String], + defaultHttpPort: Int, + defaultHttpAddress: String, + devSettings: Seq[(String, String)] + ): (Seq[(String, String)], Option[Int], Option[Int], String) = { val (propertyArgs, otherArgs) = args.partition(_.startsWith("-D")) val properties = propertyArgs.map { @@ -83,9 +87,9 @@ object KanelaReloader { def parsePortValue(portValue: Option[String], defaultValue: Option[Int] = None): Option[Int] = { portValue match { - case None => defaultValue + case None => defaultValue case Some("disabled") => None - case Some(s) => Some(parsePort(s)) + case Some(s) => Some(parsePort(s)) } } @@ -123,46 +127,50 @@ object KanelaReloader { def urls(cp: Seq[File]): Array[URL] = cp.map(_.toURI.toURL).toArray - def assetsClassLoader(allAssets: Seq[(String, File)])(parent: ClassLoader): ClassLoader = new AssetsClassLoader(parent, allAssets) + def assetsClassLoader(allAssets: Seq[(String, File)])(parent: ClassLoader): ClassLoader = + new AssetsClassLoader(parent, allAssets) def commonClassLoader(classpath: Seq[File]) = { lazy val commonJars: PartialFunction[java.io.File, java.net.URL] = { case jar if jar.getName.startsWith("h2-") || jar.getName == "h2.jar" => jar.toURI.toURL } - new java.net.URLClassLoader(classpath.collect(commonJars).toArray, null /* important here, don't depend of the sbt classLoader! */ ) { + new java.net.URLClassLoader( + classpath.collect(commonJars).toArray, + null /* important here, don't depend of the sbt classLoader! */ + ) { override def toString = "Common ClassLoader: " + getURLs.map(_.toString).mkString(",") } } /** - * Dev server - */ + * Dev server + */ trait DevServer extends Closeable { - val buildLink: BuildLink + def buildLink: BuildLink /** Allows to register a listener that will be triggered a monitored file is changed. */ def addChangeListener(f: () => Unit): Unit - /** Reloads the application.*/ + /** Reloads the application. */ def reload(): Unit } /** - * Start the server in dev mode - * - * @return A closeable that can be closed to stop the server - */ + * Start the server in dev mode + * + * @return A closeable that can be closed to stop the server + */ def startDevMode( runHooks: Seq[RunHook], javaOptions: Seq[String], commonClassLoader: ClassLoader, dependencyClasspath: Seq[File], - reloadCompile: () => CompileResult, + reloadCompile: () => Reloader.CompileResult, assetsClassLoader: ClassLoader => ClassLoader, monitoredFiles: Seq[File], fileWatchService: FileWatchService, - generatedSourceHandlers: Map[String, GeneratedSourceMapping], + generatedSourceHandlers: Map[String, Reloader.GeneratedSourceMapping], defaultHttpPort: Int, defaultHttpAddress: String, projectPath: File, @@ -172,7 +180,6 @@ object KanelaReloader { reloadLock: AnyRef, kanelaAgentJar: File ): DevServer = { - val (systemPropertiesArgs, httpPort, httpsPort, httpAddress) = filterArgs(args, defaultHttpPort, defaultHttpAddress, devSettings) val systemPropertiesJavaOptions = extractSystemProperties(javaOptions) @@ -183,7 +190,9 @@ object KanelaReloader { // but who knows how they will be set in a future change) also set the actual configs they are shortcuts for. // So when reading the actual (long) keys from the config (play.server.http...) the values match and are correct. val systemPropertiesAddressPorts = Seq("play.server.http.address" -> httpAddress) ++ - httpPort.map(port => Seq("play.server.http.port" -> port.toString)).getOrElse(Nil) ++ + httpPort + .map(port => Seq("play.server.http.port" -> port.toString)) + .getOrElse(Seq("play.server.http.port" -> "disabled")) ++ httpsPort.map(port => Seq("play.server.https.port" -> port.toString)).getOrElse(Nil) // Properties are combined in this specific order so that command line @@ -237,13 +246,18 @@ object KanelaReloader { 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, Build.sharedClasses, buildLoader, new ApplicationClassLoaderProvider { - def get: URLClassLoader = { reloader.getClassLoader.orNull } - }) + * 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, + Build.sharedClasses, + buildLoader, + new ApplicationClassLoaderProvider { + def get: URLClassLoader = { reloader.getClassLoader.orNull } + } + ) lazy val applicationLoader = new SbtKanelaClassLoader("DependencyClassLoader", urls(dependencyClasspath), delegatingLoader, loadH2Driver = true) @@ -279,24 +293,29 @@ object KanelaReloader { SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, applicationLoader, clearRegistry = true) val server = { - val mainClass = applicationLoader.loadClass(mainClassName) + type ServerStart = { + def mainDevHttpAndHttpsMode( + buildLink: BuildLink, + httpPort: Int, + httpsPort: Int, + httpAddress: String + ): ReloadableServer + + def mainDevHttpMode(buildLink: BuildLink, httpPort: Int, httpAddress: String): ReloadableServer + + def mainDevOnlyHttpsMode(buildLink: BuildLink, httpsPort: Int, httpAddress: String): ReloadableServer + } + + import scala.language.reflectiveCalls + val mainClass = applicationLoader.loadClass(mainClassName + "$") + val mainObject = mainClass.getField("MODULE$").get(null).asInstanceOf[ServerStart] + if (httpPort.isDefined && httpsPort.isDefined) { - val mainDev = mainClass.getMethod( - "mainDevHttpAndHttpsMode", - classOf[BuildLink], - classOf[Int], - classOf[Int], - classOf[String] - ) - mainDev - .invoke(null, reloader, httpPort.get: java.lang.Integer, httpsPort.get: java.lang.Integer, httpAddress) - .asInstanceOf[play.core.server.ReloadableServer] + mainObject.mainDevHttpAndHttpsMode(reloader, httpPort.get, httpsPort.get, httpAddress) } else if (httpPort.isDefined) { - val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[Int], classOf[String]) - mainDev.invoke(null, reloader, httpPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ReloadableServer] + mainObject.mainDevHttpMode(reloader, httpPort.get, httpAddress) } else { - val mainDev = mainClass.getMethod("mainDevOnlyHttpsMode", classOf[BuildLink], classOf[Int], classOf[String]) - mainDev.invoke(null, reloader, httpsPort.get: java.lang.Integer, httpAddress).asInstanceOf[play.core.server.ReloadableServer] + mainObject.mainDevOnlyHttpsMode(reloader, httpsPort.get, httpAddress) } } @@ -304,9 +323,9 @@ object KanelaReloader { runHooks.run(_.afterStarted()) new DevServer { - val buildLink = reloader + val buildLink = reloader def addChangeListener(f: () => Unit): Unit = reloader.addChangeListener(f) - def reload(): Unit = server.reload() + def reload(): Unit = server.reload() def close(): Unit = { server.stop() reloader.close() @@ -331,7 +350,8 @@ object KanelaReloader { } } // Convert play-server exceptions to our to our ServerStartException - def getRootCause(t: Throwable): Throwable = if (t.getCause == null) t else getRootCause(t.getCause) + @tailrec def getRootCause(t: Throwable): Throwable = + if (t.getCause == null) t else getRootCause(t.getCause) if (getRootCause(e).getClass.getName == "play.core.server.ServerListenException") { throw new ServerStartException(e) } @@ -340,20 +360,29 @@ object KanelaReloader { } /** - * Start the server without hot reloading - */ - def startNoReload(parentClassLoader: ClassLoader, dependencyClasspath: Seq[File], buildProjectPath: File, - devSettings: Seq[(String, String)], httpPort: Int, mainClassName: String): DevServer = { + * Start the server without hot reloading + */ + def startNoReload( + parentClassLoader: ClassLoader, + dependencyClasspath: Seq[File], + buildProjectPath: File, + devSettings: Seq[(String, String)], + httpPort: Int, + mainClassName: String + ): DevServer = { val buildLoader = this.getClass.getClassLoader lazy val delegatingLoader: ClassLoader = new DelegatingClassLoader( parentClassLoader, - Build.sharedClasses, buildLoader, new ApplicationClassLoaderProvider { + Build.sharedClasses, + buildLoader, + new ApplicationClassLoaderProvider { def get: URLClassLoader = { applicationLoader } - }) + } + ) - lazy val applicationLoader = new NamedURLClassLoader("DependencyClassLoader", urls(dependencyClasspath), - delegatingLoader) + lazy val applicationLoader = + new NamedURLClassLoader("DependencyClassLoader", urls(dependencyClasspath), delegatingLoader) val _buildLink = new BuildLink { private val initialized = new java.util.concurrent.atomic.AtomicBoolean(false) @@ -361,15 +390,23 @@ object KanelaReloader { if (initialized.compareAndSet(false, true)) applicationLoader else null // this means nothing to reload } - override def projectPath(): File = buildProjectPath - override def settings(): java.util.Map[String, String] = devSettings.toMap.asJava - override def forceReload(): Unit = () + override def projectPath(): File = buildProjectPath + override def settings(): java.util.Map[String, String] = devSettings.toMap.asJava + override def forceReload(): Unit = () override def findSource(className: String, line: Integer): Array[AnyRef] = null } - val mainClass = applicationLoader.loadClass(mainClassName) - val mainDev = mainClass.getMethod("mainDevHttpMode", classOf[BuildLink], classOf[Int]) - val server = mainDev.invoke(null, _buildLink, httpPort: java.lang.Integer).asInstanceOf[play.core.server.ReloadableServer] + type ServerStart = { + def mainDevHttpMode( + buildLink: BuildLink, + httpPort: Int, + ): ReloadableServer + } + + import scala.language.reflectiveCalls + val mainClass = applicationLoader.loadClass(mainClassName + "$") + val mainObject = mainClass.getField("MODULE$").get(null).asInstanceOf[ServerStart] + val server = mainObject.mainDevHttpMode(_buildLink, httpPort) server.reload() // it's important to initialize the server @@ -379,26 +416,27 @@ object KanelaReloader { /** Allows to register a listener that will be triggered a monitored file is changed. */ def addChangeListener(f: () => Unit): Unit = () - /** Reloads the application.*/ + /** Reloads the application. */ def reload(): Unit = () def close(): Unit = server.stop() } } - } -class KanelaReloader( - reloadCompile: () => CompileResult, - baseLoader: ClassLoader, - val projectPath: File, - devSettings: Seq[(String, String)], - monitoredFiles: Seq[File], - fileWatchService: FileWatchService, - generatedSourceHandlers: Map[String, GeneratedSourceMapping], - reloadLock: AnyRef, - kanelaAgentJar: File) extends BuildLink { +import play.runsupport.KanelaReloader._ +class KanelaReloader( + reloadCompile: () => Reloader.CompileResult, + baseLoader: ClassLoader, + val projectPath: File, + devSettings: Seq[(String, String)], + monitoredFiles: Seq[File], + fileWatchService: FileWatchService, + generatedSourceHandlers: Map[String, Reloader.GeneratedSourceMapping], + reloadLock: AnyRef, + kanelaAgentJar: File +) extends BuildLink { // The current classloader for the application @volatile private var currentApplicationClassLoader: Option[URLClassLoader] = None // Flag to force a reload on the next request. @@ -408,7 +446,7 @@ class KanelaReloader( // Whether any source files have changed since the last request. @volatile private var changed = false // The last successful compile results. Used for rendering nice errors. - @volatile private var currentSourceMap = Option.empty[Map[String, Source]] + @volatile private var currentSourceMap = Option.empty[Map[String, Reloader.Source]] // Last time the classpath was modified in millis. Used to determine whether anything on the classpath has // changed as a result of compilation, and therefore a new classloader is needed and the app needs to be reloaded. @volatile private var lastModified: Long = 0L @@ -417,25 +455,13 @@ class KanelaReloader( private val fileLastChanged = new AtomicReference[Instant]() // Create the watcher, updates the changed boolean when a file has changed. - private val watcher = fileWatchService.watch(monitoredFiles, () => { - changed = true - }) + private val watcher = fileWatchService.watch(monitoredFiles, () => changed = true) private val classLoaderVersion = new java.util.concurrent.atomic.AtomicInteger(0) private val quietTimeTimer = new Timer("reloader-timer", true) private val listeners = new java.util.concurrent.CopyOnWriteArrayList[() => Unit]() - private val quietPeriodMs: Long = 200L - private def onChange(): Unit = { - val now = Instant.now() - fileLastChanged.set(now) - // set timer task - quietTimeTimer.schedule(new TimerTask { - override def run(): Unit = quietPeriodFinished(now) - }, quietPeriodMs) - } - private def quietPeriodFinished(start: Instant): Unit = { // If our start time is equal to the most recent start time stored, then execute the handlers and set the most // recent time to null, otherwise don't do anything. @@ -448,55 +474,51 @@ class KanelaReloader( def addChangeListener(f: () => Unit): Unit = listeners.add(f) /** - * Contrary to its name, this doesn't necessarily reload the app. It is invoked on every request, and will only - * trigger a reload of the app if something has changed. - * - * Since this communicates across classloaders, it must return only simple objects. - * - * - * @return Either - * - Throwable - If something went wrong (eg, a compile error). - * - ClassLoader - If the classloader has changed, and the application should be reloaded. - * - null - If nothing changed. - */ + * Contrary to its name, this doesn't necessarily reload the app. It is invoked on every request, and will only + * trigger a reload of the app if something has changed. + * + * Since this communicates across classloaders, it must return only simple objects. + * + * @return Either + * - Throwable - If something went wrong (eg, a compile error). + * - ClassLoader - If the classloader has changed, and the application should be reloaded. + * - null - If nothing changed. + */ def reload: AnyRef = { reloadLock.synchronized { if (changed || forceReloadNextTime || currentSourceMap.isEmpty || currentApplicationClassLoader.isEmpty) { - val shouldReload = forceReloadNextTime changed = false forceReloadNextTime = false - // use Reloader context ClassLoader to avoid ClassLoader leaks in sbt/scala-compiler threads + // use KanelaReloader context ClassLoader to avoid ClassLoader leaks in sbt/scala-compiler threads KanelaReloader.withReloaderContextClassLoader { // Run the reload task, which will trigger everything to compile reloadCompile() match { - case CompileFailure(exception) => + case Reloader.CompileFailure(exception) => // We force reload next time because compilation failed this time forceReloadNextTime = true exception - case CompileSuccess(sourceMap, classpath) => - + case Reloader.CompileSuccess(sourceMap, classpath) => currentSourceMap = Some(sourceMap) // We only want to reload if the classpath has changed. Assets don't live on the classpath, so // they won't trigger a reload. - val classpathFiles = classpath.iterator.filter(_.exists()).flatMap(_.toScala.listRecursively).map(_.toJava) + val classpathFiles = + classpath.iterator.filter(_.exists()).flatMap(_.toScala.listRecursively).map(_.toJava) val newLastModified = - classpathFiles.foldLeft(0L) { (acc, file) => - math.max(acc, file.lastModified) - } + classpathFiles.foldLeft(0L) { (acc, file) => math.max(acc, file.lastModified) } val triggered = newLastModified > lastModified lastModified = newLastModified if (triggered || shouldReload || currentApplicationClassLoader.isEmpty) { // Create a new classloader val version = classLoaderVersion.incrementAndGet - val name = "ReloadableClassLoader(v" + version + ")" - val urls = KanelaReloader.urls(classpath) - val loader = new SbtKanelaClassLoader(name, urls, baseLoader, skipWhenLoadingResources = true) + val name = "ReloadableClassLoader(v" + version + ")" + val urls = KanelaReloader.urls(classpath) + val loader = new SbtKanelaClassLoader(name, urls, baseLoader, skipWhenLoadingResources = true) SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaAgentJar, loader, clearRegistry = false) currentApplicationClassLoader = Some(loader) loader @@ -549,5 +571,4 @@ class KanelaReloader( } def getClassLoader = currentApplicationClassLoader - } diff --git a/sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala b/sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala similarity index 95% rename from sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala rename to sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala index a5f8ef1..73b5e34 100644 --- a/sbt-kanela-runner-play-2.7/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala +++ b/sbt-kanela-runner-play-3.0/src/main/scala/kamon/instrumentation/sbt/play/SbtKanelaRunnerPlay.scala @@ -24,7 +24,7 @@ import com.lightbend.sbt.javaagent.JavaAgent import com.lightbend.sbt.javaagent.JavaAgent.JavaAgentKeys.javaAgents import kamon.instrumentation.sbt.SbtKanelaRunner.Keys.kanelaVersion import kamon.instrumentation.sbt.{KanelaOnSystemClassLoader, SbtKanelaRunner} -import play.sbt.run.KanelaPlayRun +import sbt.KanelaPlayRun import sbt.Keys._ import sbt._ @@ -34,7 +34,8 @@ object SbtKanelaRunnerPlay extends AutoPlugin { override def requires = PlayWeb && SbtKanelaRunner && JavaAgent override def projectSettings: Seq[Setting[_]] = Seq( - Keys.run in Compile := KanelaPlayRun.playWithKanelaRunTask.evaluated, + Compile / Keys.run := KanelaPlayRun.playDefaultRunTask.evaluated, + Compile / Keys.bgRun := KanelaPlayRun.playDefaultBgRunTask.evaluated, playRunHooks += runningWithKanelaNotice.value, javaAgents += "io.kamon" % "kanela-agent" % kanelaVersion.value ) diff --git a/sbt-kanela-runner/src/main/scala-sbt-1.0/kamon/instrumentation/sbt/SbtCross.scala b/sbt-kanela-runner/src/main/scala-sbt-1.0/kamon/instrumentation/sbt/SbtCross.scala deleted file mode 100644 index 13a774b..0000000 --- a/sbt-kanela-runner/src/main/scala-sbt-1.0/kamon/instrumentation/sbt/SbtCross.scala +++ /dev/null @@ -1,34 +0,0 @@ -package kamon.instrumentation.sbt - -import sbt._ -import sbt.internal.inc.classpath._ - -import scala.util.Try - -object SbtCross { - type ScalaInstance = sbt.internal.inc.ScalaInstance - - def directExecute(execute: => Unit, log: Logger):Try[Unit] = { - val result = Try(execute) - result.failed.foreach(e => log.trace(e)) - result - } - - private def javaLibraryPaths: Seq[File] = IO.parseClasspath(System.getProperty("java.library.path")) - - def toLoader(paths: Seq[File], resourceMap: Map[String, String], nativeTemp: File): ClassLoader = - new KanelaOnSystemClassLoader(Path.toURLs(paths), null) with RawResources with NativeCopyLoader { - override def resources = resourceMap - override val config = new NativeCopyConfig(nativeTemp, paths, javaLibraryPaths) - override def toString = - s"""|URLClassLoader with NativeCopyLoader with RawResources( - | urls = $paths, - | resourceMap = ${resourceMap.keySet}, - | nativeTemp = $nativeTemp - |)""".stripMargin - } - - val AppClassPath = ClasspathUtilities.AppClassPath - - val BootClassPath = ClasspathUtilities.BootClassPath -} diff --git a/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/RunAndAttachKanelaOnResolvedClassLoader.scala b/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/RunAndAttachKanelaOnResolvedClassLoader.scala new file mode 100644 index 0000000..5bebb5c --- /dev/null +++ b/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/RunAndAttachKanelaOnResolvedClassLoader.scala @@ -0,0 +1,28 @@ +package sbt + +import kamon.instrumentation.sbt.SbtKanelaRunner + +import scala.util.Try + +/** + * This class overrides the runWithLoader function introduced in SBT 1.3.x with the purpose of capturing the + * ClassLoader and using it to attach the Kanela agent on the current JVM. + */ +class RunAndAttachKanelaOnResolvedClassLoader(kanelaJar: File, newLoader: Seq[File] => ClassLoader, trapExit: Boolean) + extends Run(newLoader, trapExit) { + + def this(kanelaJar: File, run: Run, trapExit: Boolean) = + this(kanelaJar, run.newLoader, trapExit) + + private[sbt] override def runWithLoader( + loader: ClassLoader, + classpath: Seq[File], + mainClass: String, + options: Seq[String], + log: Logger + ): Try[Unit] = { + + SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaJar, loader, true) + super.runWithLoader(loader, classpath, mainClass, options, log) + } +} diff --git a/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/RunWithKanela.scala b/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/RunWithKanela.scala deleted file mode 100644 index 5ad6c24..0000000 --- a/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/RunWithKanela.scala +++ /dev/null @@ -1,100 +0,0 @@ -package sbt - -import java.lang.reflect.Method -import java.lang.reflect.Modifier.{isPublic, isStatic} - -import kamon.instrumentation.sbt.SbtKanelaRunner.attachWithInstrumentationClassLoader -import kamon.instrumentation.sbt.{SbtCross, SbtKanelaRunner} - -import scala.util.Try - -/** - * This class overrides the runWithLoader function introduced in SBT 1.3.x with the purpose of capturing the - * ClassLoader and using it to attach the Kanela agent on the current JVM. - */ -class RunAndAttachKanelaOnResolvedClassLoader(kanelaJar: File, newLoader: Seq[File] => ClassLoader, trapExit: Boolean) - extends Run(newLoader, trapExit) { - - def this(kanelaJar: File, run: Run, trapExit: Boolean) = - this(kanelaJar, run.newLoader, trapExit) - - private[sbt] override def runWithLoader( - loader: ClassLoader, - classpath: Seq[File], - mainClass: String, - options: Seq[String], - log: Logger - ): Try[Unit] = { - - SbtKanelaRunner.attachWithInstrumentationClassLoader(kanelaJar, loader, true) - super.runWithLoader(loader, classpath, mainClass, options, log) - } -} - -/** - * This class is a dirty copy of sbt.Run for SBT 1.2, with all required dependencies to make sure we can attach the - * Kanela agent on runtime. - */ -class RunAndAttachKanela(kanelaAgentJar: File, instance: SbtCross.ScalaInstance, trapExit: Boolean, nativeTmp: File) - extends Run(instance, trapExit, nativeTmp) { - - /** Runs the class 'mainClass' using the given classpath and options using the scala runner.*/ - override def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Try[Unit] = { - log.info("Running " + mainClass + " " + options.mkString(" ")) - - def execute() = - try { run0(mainClass, classpath, options, log) } - catch { case e: java.lang.reflect.InvocationTargetException => throw e.getCause } - - if (trapExit) Run.executeTrapExit(execute(), log) else SbtCross.directExecute(execute(), log) - } - - private def run0(mainClassName: String, classpath: Seq[File], options: Seq[String], log: Logger): Unit = { - log.debug(" Classpath:\n\t" + classpath.mkString("\n\t")) - val applicationLoader = makeLoader(classpath, instance, nativeTmp) - attachWithInstrumentationClassLoader(kanelaAgentJar, applicationLoader, true) - val main = getMainMethod(mainClassName, applicationLoader) - invokeMain(applicationLoader, main, options) - } - - private def createClasspathResources(appPaths: Seq[File], bootPaths: Seq[File]): Map[String, String] = { - def make(name: String, paths: Seq[File]) = name -> Path.makeString(paths) - Map(make(SbtCross.AppClassPath, appPaths), make(SbtCross.BootClassPath, bootPaths)) - } - - private def makeLoader(classpath: Seq[File], instance: SbtCross.ScalaInstance, nativeTemp: File): ClassLoader = { - SbtCross.toLoader(classpath, createClasspathResources(classpath, instance.allJars), nativeTemp) - } - - private def invokeMain(loader: ClassLoader, main: Method, options: Seq[String]): Unit = { - val currentThread = Thread.currentThread - val oldLoader = Thread.currentThread.getContextClassLoader - currentThread.setContextClassLoader(loader) - try { main.invoke(null, options.toArray[String]) } - catch { - case t: Throwable => - t.getCause match { - case e: java.lang.IllegalAccessError => - val msg = s"Error running $main.\n$e\n" + - "If using a layered classloader, this can occur if jvm package private classes are " + - "accessed across layers. This can be fixed by changing to the Flat or " + - "ScalaInstance class loader layering strategies." - throw new IllegalAccessError(msg) - case _ => throw t - } - } - finally { currentThread.setContextClassLoader(oldLoader) } - } - - override def getMainMethod(mainClassName: String, loader: ClassLoader) = { - val mainClass = Class.forName(mainClassName, true, loader) - val method = mainClass.getMethod("main", classOf[Array[String]]) - // jvm allows the actual main class to be non-public and to run a method in the non-public class, - // we need to make it accessible - method.setAccessible(true) - val modifiers = method.getModifiers - if (!isPublic(modifiers)) throw new NoSuchMethodException(mainClassName + ".main is not public") - if (!isStatic(modifiers)) throw new NoSuchMethodException(mainClassName + ".main is not static") - method - } -} diff --git a/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/SbtKanelaRunner.scala b/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/SbtKanelaRunner.scala index 125afe1..6cd2011 100644 --- a/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/SbtKanelaRunner.scala +++ b/sbt-kanela-runner/src/main/scala/kamon/instrumentation/sbt/SbtKanelaRunner.scala @@ -25,7 +25,7 @@ import net.bytebuddy.agent.ByteBuddyAgent object SbtKanelaRunner extends AutoPlugin { val KanelaRunner = config("kanela-runner") - val DefaultKanelaVersion = "1.0.11" + val DefaultKanelaVersion = "1.0.18" val InstrumentationClassLoaderProp = "kanela.instrumentation.classLoader" object Keys { @@ -45,49 +45,37 @@ object SbtKanelaRunner extends AutoPlugin { kanelaAgentJar := findKanelaAgentJar.value, kanelaRunnerJvmForkOptions := jvmForkOptions.value, libraryDependencies += kanelaAgentDependency.value, - runner in run in Compile := kanelaRunnerTask.value + Compile / run / runner := kanelaRunnerTask.value ) - def kanelaAgentDependency = Def.setting { + private def kanelaAgentDependency = Def.setting { "io.kamon" % "kanela-agent" % kanelaVersion.value % KanelaRunner.name } - def findKanelaAgentJar = Def.task { + private def findKanelaAgentJar = Def.task { update.value.matching( moduleFilter(organization = "io.kamon", name = "kanela-agent") && artifactFilter(`type` = "jar") ).head } - def jvmForkOptions = Def.task { + private def jvmForkOptions = Def.task { Seq(s"-javaagent:${kanelaAgentJar.value.getAbsolutePath}") } - def kanelaRunnerTask: Def.Initialize[Task[ScalaRun]] = Def.taskDyn { - if ((fork in run).value) { + private def kanelaRunnerTask: Def.Initialize[Task[ScalaRun]] = Def.taskDyn { + if ((run / fork).value) { Def.task { - val environmentVariables = envVars.value - val runnerForkOptions = ForkOptions( - javaHome = javaHome.value, - outputStrategy = outputStrategy.value, - bootJars = Vector.empty[java.io.File], - workingDirectory = Some(baseDirectory.value), - runJVMOptions = ((javaOptions in run).value ++ kanelaRunnerJvmForkOptions.value).toVector, - connectInput = connectInput.value, - envVars = environmentVariables - ) - - new ForkRun(runnerForkOptions) - } - } else { - if(sbtVersion.value.startsWith("1.2")) { - Def.task { - new RunAndAttachKanela(kanelaAgentJar.value, scalaInstance.value, trapExit.value, taskTemporaryDirectory.value) + val currentForkOptions = forkOptions.value + val runForkOptions = currentForkOptions.withRunJVMOptions { + ((run / javaOptions).value ++ currentForkOptions.runJVMOptions).toVector } - } else { + new ForkRun(runForkOptions) + } + } else { val kanelaJar = kanelaAgentJar.value - val previousRun = (runner in run in Compile).value + val previousRun = (Compile / run / runner).value val trap = trapExit.value Def.task { @@ -108,7 +96,6 @@ object SbtKanelaRunner extends AutoPlugin { } } } - } } } @@ -133,7 +120,7 @@ object SbtKanelaRunner extends AutoPlugin { } } - def withInstrumentationClassLoader[T](classLoader: ClassLoader)(thunk: => T): T = { + private def withInstrumentationClassLoader[T](classLoader: ClassLoader)(thunk: => T): T = { try { System.getProperties.put(InstrumentationClassLoaderProp, classLoader) thunk