From 2f96da023d9172deb646de9bd1cf0a05c4201df7 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 3 Sep 2014 23:06:31 -0700 Subject: lols --- src/main/scala/workbench/Plugin.scala | 74 +++++++++++++++++++++++++++++++ src/main/scala/workbench/Server.scala | 83 +++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/main/scala/workbench/Plugin.scala create mode 100644 src/main/scala/workbench/Server.scala (limited to 'src/main') diff --git a/src/main/scala/workbench/Plugin.scala b/src/main/scala/workbench/Plugin.scala new file mode 100644 index 0000000..8e122fe --- /dev/null +++ b/src/main/scala/workbench/Plugin.scala @@ -0,0 +1,74 @@ +package com.lihaoyi.workbench +import sbt._ +import sbt.Keys._ +import autowire._ +import upickle.Js +import scala.concurrent.ExecutionContext.Implicits.global +object Plugin extends sbt.Plugin { + + val refreshBrowsers = taskKey[Unit]("Sends a message to all connected web pages asking them to refresh the page") + val updateBrowsers = taskKey[Unit]("partially resets some of the stuff in the browser") + val localUrl = settingKey[(String, Int)]("localUrl") + private[this] val server = settingKey[Server]("local websocket server") + + + val bootSnippet = settingKey[String]("piece of javascript to make things happen") + val updatedJS = taskKey[List[String]]("Provides the addresses of the JS files that have changed") + + val workbenchSettings = Seq( + localUrl := ("localhost", 12345), + updatedJS := { + var files: List[String] = Nil + ((crossTarget in Compile).value * "*.js").get.foreach { + (x: File) => + streams.value.log.info("workbench: Checking " + x.getName) + FileFunction.cached(streams.value.cacheDirectory / x.getName, FilesInfo.lastModified, FilesInfo.lastModified) { + (f: Set[File]) => + val fsPath = f.head.getAbsolutePath.drop(new File("").getAbsolutePath.length) + files = fsPath :: files + f + }(Set(x)) + } + files + }, + updatedJS <<= (updatedJS, localUrl) map { (paths, localUrl) => + paths.map { path => + s"http://${localUrl._1}:${localUrl._2}$path" + } + }, + (extraLoggers in ThisBuild) := { + val clientLogger = FullLogger{ + new Logger { + def log(level: Level.Value, message: => String) = + if(level >= Level.Info) server.value.Wire[Api].print(level.toString, message).call() + def success(message: => String) = server.value.Wire[Api].print("info", message).call() + def trace(t: => Throwable) = server.value.Wire[Api].print("error", t.toString).call() + } + } + clientLogger.setSuccessEnabled(true) + val currentFunction = extraLoggers.value + (key: ScopedKey[_]) => clientLogger +: currentFunction(key) + }, + refreshBrowsers := { + streams.value.log.info("workbench: Reloading Pages...") + server.value.Wire[Api].reload().call() + }, + updateBrowsers := { + val changed = updatedJS.value + // There is no point in clearing the browser if no js files have changed. + if (changed.length > 0) { + server.value.Wire[Api].clear().call() + + changed.foreach { path => + streams.value.log.info("workbench: Refreshing " + path) + server.value.Wire[Api].run(path, Some(bootSnippet.value)).call() + } + } + }, + server := new Server(localUrl.value._1, localUrl.value._2, bootSnippet.value), + (onUnload in Global) := { (onUnload in Global).value.compose{ state => + server.value.kill() + state + }} + ) +} diff --git a/src/main/scala/workbench/Server.scala b/src/main/scala/workbench/Server.scala new file mode 100644 index 0000000..96ac736 --- /dev/null +++ b/src/main/scala/workbench/Server.scala @@ -0,0 +1,83 @@ +package com.lihaoyi.workbench + +import akka.actor.{ActorRef, Actor, ActorSystem} +import com.typesafe.config.ConfigFactory +import sbt.IO +import spray.routing.SimpleRoutingApp +import akka.actor.ActorDSL._ + +import upickle.{Reader, Writer, Js} +import spray.http.{AllOrigins, HttpResponse} +import spray.http.HttpHeaders.`Access-Control-Allow-Origin` +import concurrent.duration._ +import scala.concurrent.Future + +class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingApp{ + implicit val system = ActorSystem( + "SystemLol", + config = ConfigFactory.load(ActorSystem.getClass.getClassLoader), + classLoader = ActorSystem.getClass.getClassLoader + ) + object Wire extends autowire.Client[Js.Value, upickle.Reader, upickle.Writer]{ + def doCall(req: Request): Future[Js.Value] = { + pubSub ! Js.Arr(Js.Str(req.path.mkString(".")), Js.Obj(req.args.toSeq:_*)) + Future.successful(Js.Null) + } + def write[Result: Writer](r: Result) = upickle.writeJs(r) + def read[Result: Reader](p: Js.Value) = upickle.readJs[Result](p) + } + private val pubSub = actor(new Actor{ + var waitingActor: Option[ActorRef] = None + var queuedMessages = List[Js.Value]() + case object Clear + import system.dispatcher + + system.scheduler.schedule(0 seconds, 10 seconds, self, Clear) + def respond(a: ActorRef, s: String) = { + a ! HttpResponse( + entity = s, + headers = List(`Access-Control-Allow-Origin`(AllOrigins)) + ) + } + def receive = (x: Any) => (x, waitingActor, queuedMessages) match { + case (a: ActorRef, _, Nil) => + // Even if there's someone already waiting, + // a new actor waiting replaces the old one + waitingActor = Some(a) + case (a: ActorRef, None, msgs) => + + respond(a, upickle.json.write(Js.Arr(msgs:_*))) + queuedMessages = Nil + case (msg: Js.Arr, None, msgs) => + queuedMessages = msg :: msgs + case (msg: Js.Arr, Some(a), Nil) => + respond(a, upickle.json.write(Js.Arr(msg))) + waitingActor = None + case (Clear, Some(a), Nil) => + respond(a, upickle.json.write(Js.Arr())) + waitingActor = None + } + }) + + startServer(url, port) { + get { + path("workbench.js") { + complete { + IO.readStream( + getClass.getClassLoader + .getResourceAsStream("client-opt.js") + ) + s"\nMain.main(${upickle.write(bootSnippet)}, ${upickle.write(url)}, ${upickle.write(port)})" + } + } ~ + getFromDirectory(".") + } ~ + post { + path("notifications") { ctx => + pubSub ! ctx.responder + } + } + } + def kill() = { + system.shutdown() + } +} \ No newline at end of file -- cgit v1.2.3