Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to write an sbt plugin to launch the app with an agent

Tags:

plugins

scala

sbt

I would like to create an sbt plugin for my project before I open source it.

The project attaches a Java agent to the start of running an application, to instrument it for various types of profiling. The agent writes out text files for later processing.

I would like to be able to write an sbt plugin that can

  • have an alternative to run called runWithProfiling which launches a new java process, with the agent added to the argument list, and passing all the users commands.
  • on exit, I then want to invoke some arbitrary post-processing code to produce an HTML report

I know roughly how to create the new command, but I don't know how to best implement an alternative to run... I don't want to re-invent the wheel by copying all the code that run does. Is there a way I can invoke run but ensure that my parameters are passed (one time) and that it is definitely a new java process?

Also, being able to do the same for the tests would be great.

UPDATE: this is the code that I currently have, but it suffers from several problems, marked up as TODOs

import sbt._
import Keys._
import sbt.Attributed.data

object LionPlugin extends Plugin {

  val lion = TaskKey[Unit]("lion", "Run a main class with lions-share profiling.")

  override val projectSettings = Seq(
    fork := true,
    javaOptions ++= Seq(
      "-Xloggc:gc.log", "-XX:+PrintGCDetails", "-XX:+PrintGCDateStamps",
      "-XX:+PrintTenuringDistribution", "-XX:+PrintHeapAtGC"
      // TODO: need to get hold of the local jar file for a particular artifact
      // IMPL: pass the jar as the agent
    ),
    lion <<= (
      runner,
      fullClasspath in Runtime,
      mainClass in Runtime,
      streams in Runtime
      ) map runLion
  )

  // TODO: update to a task that can take parameters (e.g. number of repeats, profiling settings)
  def runLion(runner: ScalaRun, cp: Classpath, main: Option[String], streams: TaskStreams): Unit = {
    assert(runner.isInstanceOf[ForkRun], "didn't get a forked runner... SBT is b0rk3d")
    println("RUNNING with " + runner.getClass)

    // TODO: ask user if main is None, like 'run' does
    val m = main.getOrElse("Scratch")

    // TODO: get the user's arguments
    val args = Nil

    runner.run(m, data(cp), args, streams.log)

    // IMPL: post-process and produce the report

    println("FINISHED")
  }

}
like image 670
fommil Avatar asked Apr 12 '14 11:04

fommil


People also ask

What is an SBT plugin?

A plugin can define a sequence of sbt settings that are automatically added to all projects or that are explicitly declared for selected projects. For example, a plugin might add a proguard task and associated (overridable) settings. Finally, a plugin can define new commands (via the commands setting).

Where can I find plugins SBT?

Plugins can be installed for all your projects at once by declaring them in $HOME/. sbt/1.0/plugins/ .


2 Answers

Plugin authors need to abide by an unwritten Hippocratic Oath, which is "first, do no harm." Your implementation currently forces itself to every subproject and mutates fork and javaOptions of the default behavior, which I think is dangerous. I think you need to duplicate the run parameters scoped to your task so the default settings are unharmed.

// TODO: update to a task that can take parameters (e.g. number of repeats, profiling settings)

See Plugins Best Practices and existing plugins like sbt-appengine for an example.

In sbt-appengine devServer is an input task that you can set a bunch of parameters.

gae.devServer       := {
  val args = startArgsParser.parsed
  val x = (products in Compile).value
  AppEngine.restartDevServer(streams.value, (gae.reLogTag in gae.devServer).value,
    thisProjectRef.value, (gae.reForkOptions in gae.devServer).value,
    (mainClass in gae.devServer).value, (fullClasspath in gae.devServer).value,
    (gae.reStartArgs in gae.devServer).value, args,
    packageWar.value,
    (gae.onStartHooks in gae.devServer).value, (gae.onStopHooks in gae.devServer).value)
}

As you see the gut of the code is actually implemented in a method under AppEngine object, so someone else can reuse your stuff potentially. Many of the parameters into the method (in this case restartDevServer) are scoped to gae.devServer task like (mainClass in gae.devServer).

How do you intend the plugin to be set up by the build users? Are they going to enable it once as a global plugin and use the same settings everywhere, or are they going to enable it for each build in project/lion.sbt? Plugins Best Practices's recommendation is to provide baseLionSettings and lionSettings so the build user can pick and chose which subproject would have the lion task enabled.

As per actual running goes, you might want to take a look at reusing the code from sbt-revolver similar to what I did in sbt-appengine.

like image 150
Eugene Yokota Avatar answered Oct 14 '22 05:10

Eugene Yokota


See “How can I create a custom run task, in addition to run?” at http://www.scala-sbt.org/0.13.0/docs/faq.html . In your custom task, you'll want to set fork to true in order to launch a fresh JVM.

like image 43
Seth Tisue Avatar answered Oct 14 '22 06:10

Seth Tisue