├── .gitignore ├── .travis.yml ├── README.md ├── build.sbt ├── library └── src │ └── main │ └── scala │ └── sbt │ ├── AbstractBackgroundJobService.scala │ ├── BackgroundJobService.scala │ ├── InteractionService.scala │ ├── SendEventService.scala │ └── SerializerService.scala ├── project ├── Util.scala ├── build.properties ├── git.sbt └── scalariform.sbt └── sbt-core-next └── src └── main └── scala └── sbt └── plugins ├── BackgroundRunPlugin.scala ├── CommandLineUIServices.scala ├── InteractionServicePlugin.scala ├── SendEventServicePlugin.scala └── SerializersPlugin.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.lock 3 | *.komodoproject 4 | .DS_Store 5 | .history 6 | .idea 7 | .classpath 8 | .cache 9 | .project/ 10 | .project 11 | .idea/ 12 | .idea_modules/ 13 | .settings/ 14 | .target/ 15 | project/boot/ 16 | workspace/ 17 | repository/ 18 | target/ 19 | logs/ 20 | .settings 21 | .classpath 22 | .project 23 | .cache 24 | bin/ 25 | .sbtserver 26 | project/.sbtserver 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Use Docker-based container (instead of OpenVZ) 2 | sudo: false 3 | 4 | cache: 5 | directories: 6 | - $HOME/.ivy2/cache 7 | 8 | # At the moment, sbt 0.13.5 is preinstalled in Travis VM image, 9 | # which fortunately corresponds to current scalaz settings. 10 | # The line below can be used to cache a given sbt version. 11 | # - $HOME/.sbt/launchers/0.13.x 12 | 13 | # The line below is used to cache the scala version used by the build 14 | # job, as these versions might be replaced after a Travis CI build 15 | # environment upgrade (e.g. scala 2.11.2 could be replaced by scala 2.11.4). 16 | - $HOME/.sbt/boot/scala-$TRAVIS_SCALA_VERSION 17 | 18 | language: scala 19 | 20 | scala: 21 | - 2.10.3 22 | jdk: 23 | - openjdk6 24 | - openjdk7 25 | - oraclejdk7 26 | notifications: 27 | email: 28 | - qbranch@typesafe.com 29 | 30 | script: 31 | - sbt clean compile 32 | 33 | # Tricks to avoid unnecessary cache updates 34 | - find $HOME/.sbt -name "*.lock" | xargs rm 35 | - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This repository contains sbt plugins which are targeted for 3 | eventual inclusion in sbt core. They are semi-experimental: we 4 | think they are ready to use, and we intend to keep these ABIs 5 | stable. However, they may change a little bit when they make their 6 | way into sbt proper. 7 | 8 | These new APIs were mostly created to enable 9 | [sbt server](https://github.com/sbt/sbt-remote-control), but they 10 | have null or fallback behavior in traditional non-server sbt, so 11 | they are safe to use unconditionally. 12 | 13 | # Using the plugins 14 | 15 | These plugins are all in the same jar, to depend on it create a 16 | file in `project` such as `project/core-next.sbt` containing: 17 | `addSbtPlugin("org.scala-sbt" % "sbt-core-next" % "0.1.1")` 18 | 19 | They are all 20 | [auto plugins](http://www.scala-sbt.org/0.13/docs/Plugins.html) so 21 | there's nothing to do other than that. 22 | 23 | # Intro to the plugins in core-next 24 | 25 | ## BackgroundRunPlugin 26 | 27 | This plugin introduces the concept of _background jobs_, which are 28 | threads or processes which exist in the background (outside of any 29 | task execution). Jobs can be managed similar to OS processes: 30 | there are tasks `jobList`, `jobStop`, `jobWaitFor`. To create a 31 | job from a task, use the `jobService` setting to obtain a 32 | `BackgroundJobService` and then call the `runInBackgroundThread` 33 | method. 34 | 35 | The `BackgroundRunPlugin` also changes `run` and `runMain` to be 36 | blocking wrappers around `backgroundRun` and 37 | `backgroundRunMain`. `backgroundRun` starts a job to run an 38 | application, then returns to the sbt command loop, while the 39 | traditional `run` task blocks the sbt command loop. 40 | 41 | The purpose of this is to allow you to run multiple processes from 42 | the same sbt instance, or to run a process and then also do 43 | something else in sbt. 44 | 45 | Unlike tasks, background jobs do not yield a value; they have no 46 | result. 47 | 48 | ## InteractionServicePlugin 49 | 50 | This plugin abstracts asking for a string, or asking for 51 | confirmation, through a setting `interactionService` which returns 52 | an `InteractionService` instance. This keeps tasks from relying on 53 | direct interaction with standard input, and allows tasks to 54 | interact with the user even if the only client is a GUI client. 55 | 56 | ## SendEventServicePlugin 57 | 58 | This plugin adds the `sendEventService` key returning a 59 | `SendEventService` instance. You would use this to send structured 60 | events to clients of sbt server; an event can be anything you can 61 | serialize with 62 | [sbt.serialization](https://github.com/sbt/serialization). Events 63 | might be for displaying progress, to show errors as they happen, 64 | or for any other purpose. Unlike task results, events stream 65 | immediately. 66 | 67 | Events have no effect unless some client knows about the event 68 | type and chooses to do something with it. When running outside of 69 | sbt server, events are dropped and go nowhere. 70 | 71 | ## SerializersPlugin 72 | 73 | This is a workaround for our inability to change the ABI of the 74 | sbt 0.13.x series. We need a pickler and unpickler for task 75 | results. In 0.13.x, we can't modify 76 | [TaskKey](http://www.scala-sbt.org/0.13/api/sbt/TaskKey.html) and 77 | [SettingKey](http://www.scala-sbt.org/0.13/api/index.html#sbt.SettingKey), 78 | and the associated macros `taskKey` and `settingKey`, to require 79 | an implicit pickler. 80 | 81 | So the solution is to allow plugins to register picklers, and we 82 | look them up by runtime class, since `TaskKey` and `SettingKey` do 83 | already capture the type manifest of the result type. 84 | 85 | If you're curious, you can see where the runtime lookup of task 86 | result picklers happens in sbt server 87 | [here](https://github.com/sbt/sbt-remote-control/blob/master/server/src/main/scala/sbt/server/TaskProgressShim.scala#L79). 88 | 89 | For plugins who want their results to be available to sbt clients, 90 | they would register a pickler with the `registeredSerializers` 91 | key. There's also a `registeredProtocolConversions` key, which 92 | transforms the result to another type prior to serialization. 93 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.SbtGit._ 2 | 3 | val serializationVersion = "0.1.0" 4 | val serialization = "org.scala-sbt" %% "serialization" % serializationVersion 5 | val sbtMothership = "org.scala-sbt" % "sbt" % "0.13.7" 6 | 7 | val scala210Version = "2.10.4" 8 | // val scala211Version = "2.11.5" 9 | 10 | lazy val commonSettings = Util.settings ++ versionWithGit ++ Seq( 11 | organization := "org.scala-sbt", 12 | git.baseVersion := "0.1.0", 13 | isSnapshot := true, 14 | version := { 15 | val old = version.value 16 | if (isSnapshot.value) old 17 | else git.baseVersion.value 18 | }, 19 | scalaVersion := scala210Version, 20 | crossScalaVersions := Seq(scala210Version) 21 | ) 22 | 23 | lazy val root = (project in file(".")). 24 | aggregate(coreNext, sbtCoreNext). 25 | settings(commonSettings: _*). 26 | settings( 27 | publishArtifact := false, 28 | publish := {}, 29 | publishLocal := {} 30 | ) 31 | 32 | lazy val coreNext = (project in file("library")). 33 | settings(commonSettings: _*). 34 | settings( 35 | name := "Core Next", 36 | libraryDependencies ++= Seq(serialization, sbtMothership % "provided") 37 | ) 38 | 39 | lazy val sbtCoreNext = (project in file("sbt-core-next")). 40 | dependsOn(coreNext). 41 | settings(commonSettings: _*). 42 | settings( 43 | sbtPlugin := true, 44 | name := "sbt-core-next" 45 | ) 46 | -------------------------------------------------------------------------------- /library/src/main/scala/sbt/AbstractBackgroundJobService.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | 3 | /** 4 | * Interface between sbt and a thing running in the background. 5 | */ 6 | private[sbt] trait BackgroundJob { 7 | def humanReadableName: String 8 | def awaitTermination(): Unit 9 | def shutdown(): Unit 10 | // this should be true on construction and stay true until 11 | // the job is complete 12 | def isRunning(): Boolean 13 | // called after stop or on spontaneous exit, closing the result 14 | // removes the listener 15 | def onStop(listener: () => Unit)(implicit ex: concurrent.ExecutionContext): java.io.Closeable 16 | // do we need this or is the spawning task good enough? 17 | // def tags: SomeType 18 | } 19 | 20 | private class BackgroundThreadPool extends java.io.Closeable { 21 | 22 | private val nextThreadId = new java.util.concurrent.atomic.AtomicInteger(1) 23 | private val threadGroup = Thread.currentThread.getThreadGroup() 24 | 25 | private val threadFactory = new java.util.concurrent.ThreadFactory() { 26 | override def newThread(runnable: Runnable): Thread = { 27 | val thread = new Thread(threadGroup, runnable, s"sbt-bg-threads-${nextThreadId.getAndIncrement}") 28 | // Do NOT setDaemon because then the code in TaskExit.scala in sbt will insta-kill 29 | // the backgrounded process, at least for the case of the run task. 30 | thread 31 | } 32 | } 33 | 34 | private val executor = new java.util.concurrent.ThreadPoolExecutor(0, /* corePoolSize */ 35 | 32, /* maxPoolSize, max # of bg tasks */ 36 | 2, java.util.concurrent.TimeUnit.SECONDS, /* keep alive unused threads this long (if corePoolSize < maxPoolSize) */ 37 | new java.util.concurrent.SynchronousQueue[Runnable](), 38 | threadFactory) 39 | 40 | private class BackgroundRunnable(val taskName: String, body: () => Unit) 41 | extends Runnable with BackgroundJob { 42 | 43 | private val finishedLatch = new java.util.concurrent.CountDownLatch(1) 44 | 45 | private sealed trait Status 46 | private case object Waiting extends Status 47 | private final case class Running(thread: Thread) extends Status 48 | // the oldThread is None if we never ran 49 | private final case class Stopped(oldThread: Option[Thread]) extends Status 50 | 51 | // synchronize to read/write this, no sync to just read 52 | @volatile 53 | private var status: Status = Waiting 54 | 55 | // double-finally for extra paranoia that we will finishedLatch.countDown 56 | override def run() = try { 57 | val go = synchronized { 58 | status match { 59 | case Waiting => 60 | status = Running(Thread.currentThread()) 61 | true 62 | case Stopped(_) => 63 | false 64 | case Running(_) => 65 | throw new RuntimeException("Impossible status of bg thread") 66 | } 67 | } 68 | try { if (go) body() } 69 | finally cleanup() 70 | } finally finishedLatch.countDown() 71 | 72 | private class StopListener(val callback: () => Unit, val executionContext: concurrent.ExecutionContext) extends java.io.Closeable { 73 | override def close(): Unit = removeListener(this) 74 | override def hashCode: Int = System.identityHashCode(this) 75 | override def equals(other: Any): Boolean = other match { 76 | case r: AnyRef => this eq r 77 | case _ => false 78 | } 79 | } 80 | 81 | // access is synchronized 82 | private var stopListeners = Set.empty[StopListener] 83 | 84 | private def removeListener(listener: StopListener): Unit = synchronized { 85 | stopListeners -= listener 86 | } 87 | 88 | def cleanup(): Unit = { 89 | // avoid holding any lock while invoking callbacks, and 90 | // handle callbacks being added by other callbacks, just 91 | // to be all fancy. 92 | while (synchronized { stopListeners.nonEmpty }) { 93 | val listeners = synchronized { 94 | val list = stopListeners.toList 95 | stopListeners = Set.empty 96 | list 97 | } 98 | listeners.foreach { l => 99 | l.executionContext.prepare().execute(new Runnable { override def run = l.callback() }) 100 | } 101 | } 102 | } 103 | 104 | override def onStop(listener: () => Unit)(implicit ex: concurrent.ExecutionContext): java.io.Closeable = synchronized { 105 | val result = new StopListener(listener, ex) 106 | stopListeners += result 107 | result 108 | } 109 | 110 | override def awaitTermination(): Unit = finishedLatch.await() 111 | override def humanReadableName: String = taskName 112 | override def isRunning(): Boolean = status match { 113 | case Waiting => true // we start as running from BackgroundJob perspective 114 | case Running(thread) => thread.isAlive() 115 | case Stopped(threadOption) => threadOption.map(_.isAlive()).getOrElse(false) 116 | } 117 | override def shutdown(): Unit = synchronized { 118 | status match { 119 | case Waiting => 120 | status = Stopped(None) // makes run() not run the body 121 | case Running(thread) => 122 | status = Stopped(Some(thread)) 123 | thread.interrupt() 124 | case Stopped(threadOption) => 125 | // try to interrupt again! woot! 126 | threadOption.foreach(_.interrupt()) 127 | } 128 | } 129 | } 130 | 131 | def run(manager: AbstractBackgroundJobService, spawningTask: ScopedKey[_])(work: (Logger, SendEventService) => Unit): BackgroundJobHandle = { 132 | def start(logger: Logger, uiContext: SendEventService): BackgroundJob = { 133 | val runnable = new BackgroundRunnable(spawningTask.key.label, { () => 134 | work(logger, uiContext) 135 | }) 136 | 137 | executor.execute(runnable) 138 | 139 | runnable 140 | } 141 | 142 | manager.runInBackground(spawningTask, start) 143 | } 144 | 145 | override def close(): Unit = { 146 | executor.shutdown() 147 | } 148 | } 149 | 150 | // Shared by command line and UI implementation 151 | private[sbt] abstract class AbstractBackgroundJobService extends SbtPrivateBackgroundJobService { 152 | private val nextId = new java.util.concurrent.atomic.AtomicLong(1) 153 | private val pool = new BackgroundThreadPool() 154 | 155 | // hooks for sending start/stop events 156 | protected def onAddJob(uiContext: SendEventService, job: BackgroundJobHandle): Unit = {} 157 | protected def onRemoveJob(uiContext: SendEventService, job: BackgroundJobHandle): Unit = {} 158 | 159 | // this mutable state could conceptually go on State except 160 | // that then every task that runs a background job would have 161 | // to be a command, so not sure what to do here. 162 | @volatile 163 | private final var jobs = Set.empty[Handle] 164 | private def addJob(uiContext: SendEventService, job: Handle): Unit = synchronized { 165 | onAddJob(uiContext, job) 166 | jobs += job 167 | } 168 | 169 | private def removeJob(uiContext: SendEventService, job: Handle): Unit = synchronized { 170 | onRemoveJob(uiContext, job) 171 | jobs -= job 172 | } 173 | 174 | private abstract trait AbstractHandle extends SbtPrivateBackgroundJobHandle { 175 | override def toString = s"BackgroundJobHandle(${id},${humanReadableName},${Def.showFullKey(spawningTask)})" 176 | } 177 | 178 | private final class Handle(override val id: Long, override val spawningTask: ScopedKey[_], 179 | val logger: Logger with java.io.Closeable, val uiContext: SendEventService, val job: BackgroundJob) 180 | extends AbstractHandle { 181 | 182 | def humanReadableName: String = job.humanReadableName 183 | 184 | // EC for onStop handler below 185 | import concurrent.ExecutionContext.Implicits.global 186 | job.onStop { () => 187 | logger.close() 188 | removeJob(uiContext, this) 189 | } 190 | 191 | addJob(uiContext, this) 192 | 193 | override final def equals(other: Any): Boolean = other match { 194 | case handle: BackgroundJobHandle if handle.id == id => true 195 | case _ => false 196 | } 197 | 198 | override final def hashCode(): Int = id.hashCode 199 | } 200 | 201 | // we use this if we deserialize a handle for a job that no longer exists 202 | private final class DeadHandle(override val id: Long, override val humanReadableName: String) 203 | extends AbstractHandle { 204 | override val spawningTask: ScopedKey[_] = Keys.streams // just a dummy value 205 | } 206 | 207 | protected def makeContext(id: Long, spawningTask: ScopedKey[_]): (Logger with java.io.Closeable, SendEventService) 208 | 209 | def runInBackground(spawningTask: ScopedKey[_], start: (Logger, SendEventService) => BackgroundJob): BackgroundJobHandle = { 210 | val id = nextId.getAndIncrement() 211 | val (logger, uiContext) = makeContext(id, spawningTask) 212 | val job = try new Handle(id, spawningTask, logger, uiContext, start(logger, uiContext)) 213 | catch { 214 | case e: Throwable => 215 | logger.close() 216 | throw e 217 | } 218 | job 219 | } 220 | 221 | override def runInBackgroundThread(spawningTask: ScopedKey[_], start: (Logger, SendEventService) => Unit): BackgroundJobHandle = { 222 | pool.run(this, spawningTask)(start) 223 | } 224 | 225 | override def close(): Unit = { 226 | while (jobs.nonEmpty) { 227 | jobs.headOption.foreach { job => 228 | job.job.shutdown() 229 | job.job.awaitTermination() 230 | } 231 | } 232 | pool.close() 233 | } 234 | 235 | override def list(): Seq[BackgroundJobHandle] = 236 | jobs.toList 237 | 238 | private def withHandle(job: BackgroundJobHandle)(f: Handle => Unit): Unit = job match { 239 | case handle: Handle => f(handle) 240 | case dead: DeadHandle => () // nothing to stop or wait for 241 | case other => sys.error(s"BackgroundJobHandle does not originate with the current BackgroundJobService: $other") 242 | } 243 | 244 | override def stop(job: BackgroundJobHandle): Unit = 245 | withHandle(job)(_.job.shutdown()) 246 | 247 | override def waitFor(job: BackgroundJobHandle): Unit = 248 | withHandle(job)(_.job.awaitTermination()) 249 | 250 | override def toString(): String = s"BackgroundJobService(jobs=${list().map(_.id).mkString})" 251 | 252 | override val handleFormat: sbinary.Format[BackgroundJobHandle] = { 253 | import sbinary.DefaultProtocol._ 254 | wrap[BackgroundJobHandle, (Long, String)](h => (h.id, h.humanReadableName), 255 | { 256 | case (id, humanReadableName) => 257 | // resurrect the actual handle, or use a dead placeholder 258 | jobs.find(_.id == id).getOrElse(new DeadHandle(id, humanReadableName + " ")) 259 | }) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /library/src/main/scala/sbt/BackgroundJobService.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | 3 | /** 4 | * Interface between tasks and jobs; tasks aren't allowed 5 | * to directly mess with the BackgroundJob above. Methods 6 | * on this interface should all be pure (conceptually this 7 | * is immutable). 8 | */ 9 | sealed trait BackgroundJobHandle { 10 | def id: Long 11 | def humanReadableName: String 12 | def spawningTask: ScopedKey[_] 13 | // def tags: SomeType 14 | } 15 | 16 | sealed trait BackgroundJobService extends java.io.Closeable { 17 | 18 | /** 19 | * Launch a background job which is a function that runs inside another thread; 20 | * killing the job will interrupt() the thread. If your thread blocks on a process, 21 | * then you should get an InterruptedException while blocking on the process, and 22 | * then you could process.destroy() for example. 23 | * 24 | * TODO if we introduce a ServiceManager, we can pass that in to start instead of 25 | * two hardcoded services. 26 | */ 27 | def runInBackgroundThread(spawningTask: ScopedKey[_], start: (Logger, SendEventService) => Unit): BackgroundJobHandle 28 | 29 | def list(): Seq[BackgroundJobHandle] 30 | def stop(job: BackgroundJobHandle): Unit 31 | def waitFor(job: BackgroundJobHandle): Unit 32 | 33 | // TODO we aren't using this anymore; do we want it? 34 | def handleFormat: sbinary.Format[BackgroundJobHandle] 35 | } 36 | 37 | object BackgroundJobServiceKeys { 38 | // jobService is a setting not a task because semantically it's required to always be the same one 39 | // TODO create a separate kind of key to lookup services separately from tasks 40 | val jobService = settingKey[BackgroundJobService]("Job manager used to run background jobs.") 41 | val jobList = taskKey[Seq[BackgroundJobHandle]]("List running background jobs.") 42 | val jobStop = inputKey[Unit]("Stop a background job by providing its ID.") 43 | val jobWaitFor = inputKey[Unit]("Wait for a background job to finish by providing its ID.") 44 | val backgroundRun = inputKey[BackgroundJobHandle]("Start an application's default main class as a background job") 45 | val backgroundRunMain = inputKey[BackgroundJobHandle]("Start a provided main class as a background job") 46 | } 47 | 48 | private[sbt] trait SbtPrivateBackgroundJobService extends BackgroundJobService 49 | private[sbt] trait SbtPrivateBackgroundJobHandle extends BackgroundJobHandle 50 | -------------------------------------------------------------------------------- /library/src/main/scala/sbt/InteractionService.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | 3 | sealed trait InteractionService { 4 | /** Prompts the user for input, optionally with a mask for characters. */ 5 | def readLine(prompt: String, mask: Boolean): Option[String] 6 | /** Ask the user to confirm something (yes or no) before continuing. */ 7 | def confirm(msg: String): Boolean 8 | 9 | // TODO - Ask for input with autocomplete? 10 | } 11 | 12 | object InteractionServiceKeys { 13 | // TODO create a separate kind of key to lookup services separately from tasks 14 | val interactionService = taskKey[InteractionService]("Service used to ask for user input through the current user interface(s).") 15 | } 16 | 17 | private[sbt] trait SbtPrivateInteractionService extends InteractionService 18 | -------------------------------------------------------------------------------- /library/src/main/scala/sbt/SendEventService.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | 3 | import sbt.serialization._ 4 | 5 | sealed trait SendEventService { 6 | /** Sends an event out to all registered event listeners. */ 7 | def sendEvent[T: Pickler](event: T): Unit 8 | } 9 | 10 | object SendEventService { 11 | private val detachedKey = AttributeKey[SendEventService]("SendEventService that sends DetachedEvent") 12 | 13 | /** 14 | * To send a detached plugin event, do getDetached(state).foreach(_.sendEvent(event)). 15 | * This should never be done from inside a task or background job where a better 16 | * SendEventService is available already. "Detached" means not associated with a task 17 | * or job. 18 | */ 19 | def getDetached(state: State): Option[SendEventService] = state get detachedKey 20 | /** To be called only by sbt server */ 21 | private[sbt] def putDetached(state: State, service: SendEventService): State = state.put(detachedKey, service) 22 | } 23 | 24 | object SendEventServiceKeys { 25 | // TODO create a separate kind of key to lookup services separately from tasks 26 | val sendEventService = taskKey[SendEventService]("Service used to send events to the current user interface(s).") 27 | } 28 | 29 | private[sbt] trait SbtPrivateSendEventService extends SendEventService 30 | -------------------------------------------------------------------------------- /library/src/main/scala/sbt/SerializerService.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | 3 | import sbt.serialization._ 4 | 5 | /** 6 | * Represents a Manifest/Serializer pair we can use 7 | * to serialize task values + events later. 8 | */ 9 | sealed trait RegisteredSerializer { 10 | type T 11 | def manifest: Manifest[T] 12 | def serializer: Pickler[T] with Unpickler[T] 13 | } 14 | object RegisteredSerializer { 15 | def apply[U](implicit pickler: Pickler[U], unpickler: Unpickler[U], mf: Manifest[U]): RegisteredSerializer = 16 | new RegisteredSerializer { 17 | type T = U 18 | override val serializer = PicklerUnpickler[U](pickler, unpickler) 19 | override val manifest = mf 20 | } 21 | } 22 | /** 23 | * Represents a dynamic type conversion to be applied 24 | * prior to selecting a RegisteredSerializer when sending over 25 | * the wire protocol. 26 | */ 27 | sealed trait RegisteredProtocolConversion { 28 | type From 29 | type To 30 | def fromManifest: Manifest[From] 31 | def toManifest: Manifest[To] 32 | def convert(from: From): To 33 | } 34 | object RegisteredProtocolConversion { 35 | def apply[F, T](convert: F => T)(implicit fromMf: Manifest[F], toMf: Manifest[T]): RegisteredProtocolConversion = 36 | new RegisteredProtocolConversion { 37 | override type From = F 38 | override type To = T 39 | override def fromManifest = fromMf 40 | override def toManifest = toMf 41 | override def convert(from: F): T = convert(from) 42 | } 43 | } 44 | 45 | object SerializersKeys { 46 | val registeredProtocolConversions = settingKey[Seq[RegisteredProtocolConversion]]("Conversions to apply before serializing task results.") 47 | val registeredSerializers = settingKey[Seq[RegisteredSerializer]]("All the serializers needed for task result values.") 48 | } 49 | -------------------------------------------------------------------------------- /project/Util.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import com.typesafe.sbt.SbtScalariform 4 | import com.typesafe.sbt.SbtScalariform.ScalariformKeys 5 | import com.typesafe.sbt.SbtGit 6 | 7 | object Util { 8 | 9 | def baseVersions: Seq[Setting[_]] = SbtGit.versionWithGit 10 | 11 | def formatPrefs = { 12 | import scalariform.formatter.preferences._ 13 | FormattingPreferences() 14 | .setPreference(IndentSpaces, 2) 15 | } 16 | // val typesafeMvnReleases = "typesafe-mvn-private-releases" at "http://private-repo.typesafe.com/typesafe/maven-releases/" 17 | val typesafeIvyReleases = Resolver.url("typesafe-ivy-private-releases", new URL("http://private-repo.typesafe.com/typesafe/ivy-releases/"))(Resolver.ivyStylePatterns) 18 | 19 | def settings: Seq[Setting[_]] = 20 | SbtScalariform.scalariformSettings ++ 21 | Seq( 22 | resolvers += Resolver.typesafeIvyRepo("releases"), 23 | publishTo := Some(typesafeIvyReleases), 24 | publishMavenStyle := false, 25 | scalacOptions <<= (scalaVersion) map { sv => 26 | Seq("-unchecked", "-deprecation", "-Xmax-classfile-name", "72") ++ 27 | { if (sv.startsWith("2.9")) Seq.empty else Seq("-feature") } 28 | }, 29 | javacOptions in Compile := Seq("-target", "1.6", "-source", "1.6"), 30 | javacOptions in (Compile, doc) := Seq("-source", "1.6"), 31 | // Scaladoc is slow as molasses. 32 | Keys.publishArtifact in (Compile, packageDoc) := false, 33 | // scalaBinaryVersion <<= scalaVersion apply { sv => 34 | // CrossVersion.binaryScalaVersion(sv) 35 | // }, 36 | ScalariformKeys.preferences in Compile := formatPrefs, 37 | ScalariformKeys.preferences in Test := formatPrefs 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8-M3 2 | -------------------------------------------------------------------------------- /project/git.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.6.1") 2 | -------------------------------------------------------------------------------- /project/scalariform.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.2.1") 2 | -------------------------------------------------------------------------------- /sbt-core-next/src/main/scala/sbt/plugins/BackgroundRunPlugin.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | package plugins 3 | 4 | import complete.Parser 5 | import complete.DefaultParsers 6 | import Keys._ 7 | import Def.Initialize 8 | import sbinary.DefaultProtocol.StringFormat 9 | import Cache.seqFormat 10 | import Attributed.data 11 | import BackgroundJobServiceKeys._ 12 | 13 | object BackgroundRunPlugin extends AutoPlugin { 14 | override def trigger = AllRequirements 15 | override def requires = plugins.JvmPlugin 16 | 17 | override val globalSettings: Seq[Setting[_]] = Seq( 18 | jobService := { new CommandLineBackgroundJobService() }, 19 | onUnload := { s => try onUnload.value(s) finally jobService.value.close() }, 20 | jobList := { jobService.value.list() }, 21 | jobStop <<= jobStopTask(), 22 | jobWaitFor <<= jobWaitForTask()) 23 | 24 | override val projectSettings = inConfig(Compile)(Seq( 25 | // note that we use the same runner and mainClass as plain run 26 | backgroundRunMain <<= backgroundRunMainTask(fullClasspath, runner in run), 27 | backgroundRun <<= backgroundRunTask(fullClasspath, mainClass in run, runner in run), 28 | runMain <<= runMainTask(), 29 | run <<= runTask())) 30 | 31 | def backgroundRunMainTask(classpath: Initialize[Task[Classpath]], scalaRun: Initialize[Task[ScalaRun]]): Initialize[InputTask[BackgroundJobHandle]] = 32 | { 33 | import DefaultParsers._ 34 | val parser = Defaults.loadForParser(discoveredMainClasses)((s, names) => Defaults.runMainParser(s, names getOrElse Nil)) 35 | Def.inputTask { 36 | val (mainClass, args) = parser.parsed 37 | jobService.value.runInBackgroundThread(resolvedScoped.value, { (logger, uiContext) => 38 | toError(scalaRun.value.run(mainClass, data(classpath.value), args, logger)) 39 | }) 40 | } 41 | } 42 | 43 | def backgroundRunTask(classpath: Initialize[Task[Classpath]], mainClassTask: Initialize[Task[Option[String]]], scalaRun: Initialize[Task[ScalaRun]]): Initialize[InputTask[BackgroundJobHandle]] = 44 | { 45 | import Def.parserToInput 46 | import sys.error 47 | 48 | val parser = Def.spaceDelimited() 49 | Def.inputTask { 50 | val mainClass = mainClassTask.value getOrElse error("No main class detected.") 51 | jobService.value.runInBackgroundThread(Keys.resolvedScoped.value, { (logger, uiContext) => 52 | // TODO - Copy the classpath into some tmp directory so we don't immediately die if a recompile happens. 53 | toError(scalaRun.value.run(mainClass, data(classpath.value), parser.parsed, logger)) 54 | }) 55 | } 56 | } 57 | 58 | private def runMainTask(): Initialize[InputTask[Unit]] = 59 | Def.inputTask { 60 | val handle = backgroundRunMain.evaluated 61 | // TODO it would be better to use the jobWaitFor task in case someone 62 | // customizes that task, but heck if I can figure out how to do it. 63 | val service = jobService.value 64 | service.waitFor(handle) 65 | } 66 | 67 | private def runTask(): Initialize[InputTask[Unit]] = 68 | Def.inputTask { 69 | val handle = backgroundRun.evaluated 70 | // TODO it would be better to use the jobWaitFor task in case someone 71 | // customizes that task, but heck if I can figure out how to do it. 72 | val service = jobService.value 73 | service.waitFor(handle) 74 | } 75 | 76 | private def jobIdParser: (State, Seq[BackgroundJobHandle]) => Parser[Seq[BackgroundJobHandle]] = { 77 | import DefaultParsers._ 78 | (state, handles) => { 79 | val stringIdParser: Parser[Seq[String]] = Space ~> token(NotSpace examples handles.map(_.id.toString).toSet, description = "").+ 80 | stringIdParser.map { strings => 81 | strings.map(Integer.parseInt(_)).flatMap(id => handles.find(_.id == id)) 82 | } 83 | } 84 | } 85 | 86 | private def foreachJobTask(f: (BackgroundJobService, BackgroundJobHandle) => Unit): Initialize[InputTask[Unit]] = { 87 | import DefaultParsers._ 88 | val parser: State => Parser[Seq[BackgroundJobHandle]] = { state => 89 | val extracted = Project.extract(state) 90 | val service = extracted.get(jobService) 91 | // you might be tempted to use the jobList task here, but the problem 92 | // is that its result gets cached during execution and therefore stale 93 | jobIdParser(state, service.list()) 94 | } 95 | Def.inputTask { 96 | val handles = parser.parsed 97 | for (handle <- handles) { 98 | f(jobService.value, handle) 99 | } 100 | } 101 | } 102 | 103 | private def jobStopTask(): Initialize[InputTask[Unit]] = 104 | foreachJobTask { (manager, handle) => manager.stop(handle) } 105 | 106 | private def jobWaitForTask(): Initialize[InputTask[Unit]] = 107 | foreachJobTask { (manager, handle) => manager.waitFor(handle) } 108 | } 109 | 110 | private[sbt] class CommandLineBackgroundJobService extends AbstractBackgroundJobService { 111 | override def makeContext(id: Long, spawningTask: ScopedKey[_]) = { 112 | // TODO this is no good; what we need to do is replicate how sbt 113 | // gets loggers from Streams, but without the thing where they 114 | // are all closed when the Streams is closed. So we need "detached" 115 | // loggers. Potentially on command line we also want to avoid 116 | // showing the logs on the console as they arrive and only store 117 | // them in the file for retrieval with "last" - except "last" 118 | // takes a task name which we don't have. 119 | val logger = new Logger with java.io.Closeable { 120 | // TODO 121 | override def close(): Unit = () 122 | // TODO 123 | override def log(level: sbt.Level.Value, message: => String): Unit = System.err.println(s"background log: $level: $message") 124 | // TODO 125 | override def success(message: => String): Unit = System.out.println(s"bg job success: $message") 126 | // TODO 127 | override def trace(t: => Throwable): Unit = t.printStackTrace(System.err) 128 | } 129 | (logger, CommandLineUIServices) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /sbt-core-next/src/main/scala/sbt/plugins/CommandLineUIServices.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | package plugins 3 | 4 | import sbt.serialization._ 5 | 6 | private[sbt] object CommandLineUIServices extends SbtPrivateInteractionService with SbtPrivateSendEventService { 7 | override def readLine(prompt: String, mask: Boolean): Option[String] = { 8 | val maskChar = if (mask) Some('*') else None 9 | SimpleReader.readLine(prompt, maskChar) 10 | } 11 | // TODO - Implement this better! 12 | def confirm(msg: String): Boolean = { 13 | object Assent { 14 | def unapply(in: String): Boolean = { 15 | (in == "y" || in == "yes") 16 | } 17 | } 18 | SimpleReader.readLine(msg + " (yes/no): ", None) match { 19 | case Some(Assent()) => true 20 | case _ => false 21 | } 22 | } 23 | override def sendEvent[T: Pickler](event: T): Unit = () 24 | } 25 | -------------------------------------------------------------------------------- /sbt-core-next/src/main/scala/sbt/plugins/InteractionServicePlugin.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | package plugins 3 | 4 | import InteractionServiceKeys._ 5 | 6 | /** 7 | * This plugin provides the basic settings used by plugins that want to be able to communicate with a UI. 8 | * 9 | * Basically, we just stub out the setting you can use to look up the current UI context. 10 | */ 11 | object InteractionServicePlugin extends AutoPlugin { 12 | override def trigger = AllRequirements 13 | override def requires = plugins.CorePlugin 14 | 15 | override val globalSettings: Seq[Setting[_]] = Seq( 16 | interactionService in Global <<= (interactionService in Global) ?? CommandLineUIServices) 17 | } 18 | -------------------------------------------------------------------------------- /sbt-core-next/src/main/scala/sbt/plugins/SendEventServicePlugin.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | package plugins 3 | 4 | import SendEventServiceKeys._ 5 | 6 | /** 7 | * This plugin provides the basic settings used by plugins that want to be able to communicate with a UI. 8 | * 9 | * Basically, we just stub out the setting you can use to look up the current UI context. 10 | */ 11 | object SendEventServicePlugin extends AutoPlugin { 12 | override def trigger = AllRequirements 13 | override def requires = plugins.CorePlugin 14 | 15 | override val globalSettings: Seq[Setting[_]] = Seq( 16 | sendEventService in Global <<= (sendEventService in Global) ?? CommandLineUIServices 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /sbt-core-next/src/main/scala/sbt/plugins/SerializersPlugin.scala: -------------------------------------------------------------------------------- 1 | package sbt 2 | package plugins 3 | 4 | import sbt.serialization._ 5 | import SerializersKeys._ 6 | 7 | object SerializersPlugin extends AutoPlugin { 8 | override def trigger = AllRequirements 9 | override def requires = plugins.CorePlugin 10 | 11 | override val globalSettings: Seq[Setting[_]] = Seq( 12 | registeredProtocolConversions in Global <<= (registeredProtocolConversions in Global) ?? Nil, 13 | registeredSerializers in Global <<= (registeredSerializers in Global) ?? Nil) 14 | 15 | def registerTaskSerialization[T](key: TaskKey[T])(implicit pickler: Pickler[T], unpickler: Unpickler[T], mf: Manifest[T]): Setting[_] = 16 | registeredSerializers in Global += RegisteredSerializer(pickler, unpickler, mf) 17 | def registerSettingSerialization[T](key: SettingKey[T])(implicit pickler: Pickler[T], unpickler: Unpickler[T]): Setting[_] = 18 | registeredSerializers in Global += RegisteredSerializer(pickler, unpickler, key.key.manifest) 19 | } 20 | --------------------------------------------------------------------------------