summaryrefslogblamecommitdiff
path: root/Plugin.scala
blob: 1249d3a8003ed14df7bd1d5fdecb3e8e6e47c388 (plain) (tree)
1
2
3
4
5
6
7
                             




                                                       
                                   










                                               

                                                                         
                                                    


                                 
                          

                                  
 



                                                                                                                   
                                                                           

                                                                                   
 
















































                                                                                                                                         






                                                           










                                                                                                                                          
                                                                        
















                                                                                               
 
 









                                                                                           

                                  
                                                      


                                                                    
             


                                                       

















                                                                                  
package com.lihaoyi.workbench

import akka.actor.{Props, ActorRef, Actor, ActorSystem}
import akka.io
import akka.util.ByteString
import play.api.libs.json.JsArray
import java.nio.file.{Files, Paths}
import play.api.libs.json.Json
import spray.can.Http
import spray.can.server.websockets.model.Frame
import spray.can.server.websockets.model.OpCode
import spray.can.server.websockets.Sockets
import sbt._
import Keys._

import com.typesafe.config.ConfigFactory
import scala.collection.mutable
import akka.io.Tcp
import spray.http._
import spray.http.HttpHeaders.{`Access-Control-Allow-Origin`, Connection}
import spray.can.server.websockets.model.OpCode.Text
import spray.http.HttpRequest
import play.api.libs.json.JsArray
import spray.http.HttpResponse
import java.io.IOException

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 generateClient = taskKey[File]("generates a .js file that can be embedded in your web page")
  val localUrl = settingKey[(String, Int)]("localUrl")
  private[this] val server = settingKey[ActorRef]("local websocket server")
  val fileName = settingKey[String]("name of the generated javascript file")
  val bootSnippet = settingKey[String]("piece of javascript to make things happen")
 
  implicit val system = ActorSystem(
    "SystemLol",
    config = ConfigFactory.load(ActorSystem.getClass.getClassLoader),
    classLoader = ActorSystem.getClass.getClassLoader
  )

  implicit class pimpedActor(server: ActorRef){
    def send(x: JsArray) = {
      server ! Frame(
        opcode = OpCode.Text,
        data = ByteString(x.toString())
      )
    }
  }

  val buildSettingsX = Seq(
    localUrl := ("localhost", 12345),
    fileName := "workbench.js",
    server := {
      implicit val server = system.actorOf(Props(new SocketServer()))
      val host = localUrl.value
      io.IO(Sockets) ! Http.Bind(server, host._1, host._2)
      server
    },

    extraLoggers := {
      val clientLogger = FullLogger{
        new Logger {
          def log(level: Level.Value, message: => String): Unit =
            if(level >= Level.Info) server.value.send(Json.arr("print", level.toString(), message))
          def success(message: => String): Unit = server.value.send(Json.arr("print", "info", message))
          def trace(t: => Throwable): Unit = server.value.send(Json.arr("print", "error", t.toString))
        }
      }
      clientLogger.setSuccessEnabled(true)
      val currentFunction = extraLoggers.value
      (key: ScopedKey[_]) => clientLogger +: currentFunction(key)
    },
    refreshBrowsers := {
      streams.value.log.info("workbench: Reloading Pages...")
      server.value.send(Json.arr("reload"))
    },
    updateBrowsers := {

      server.value send Json.arr("clear")
      ((crossTarget in Compile).value * "*.js").get.map{ (x: File) =>
        streams.value.log.info("workbench: Checking " + x.getName)
        FileFunction.cached(streams.value.cacheDirectory /  x.getName, FilesInfo.lastModified, FilesInfo.lastModified){ (f: Set[File]) =>
          streams.value.log.info("workbench: Refreshing " + x.getName)
          val cwd = Paths.get(new File("").getAbsolutePath)
          val filePath = Paths.get(f.head.getAbsolutePath)
          server.value send Json.arr(
            "run",
            "/" + cwd.relativize(filePath).toString,
            bootSnippet.value
          )
          f
        }(Set(x))
      }
    },
    generateClient := {
      FileFunction.cached(streams.value.cacheDirectory / "workbench"/ "workbench.js", FilesInfo.full, FilesInfo.exists){ (f: Set[File]) =>
        val transformed =
          IO.read(f.head)
            .replace("<host>", localUrl.value._1)
            .replace("<port>", localUrl.value._2.toString)
            .replace("<bootSnippet>", bootSnippet.value)
        val outputFile = (crossTarget in Compile).value / fileName.value
        IO.write(outputFile, transformed)
        Set(outputFile)
      }(Set(new File(getClass.getClassLoader.getResource("workbench_template.ts").toURI))).head
    }
  )

  class SocketServer() extends Actor{
    val sockets: mutable.Set[ActorRef] = mutable.Set.empty
    def receive = {
      case x: Tcp.Connected => sender ! Tcp.Register(self) // normal Http server init

      case req: HttpRequest =>
        // Upgrade the connection to websockets if you think the incoming
        // request looks good
        if (req.headers.contains(Connection("Upgrade"))){
          sender ! Sockets.UpgradeServer(Sockets.acceptAllFunction(req), self)
        }else{


          try{
            val data = Files.readAllBytes(
              Paths.get(req.uri.path.toString.drop(1))
            )
            val mimeType: ContentType = req.uri.path.toString.split('.').lastOption match {
              case Some("css") => MediaTypes.`text/css`
              case Some("html") => MediaTypes.`text/html`
              case Some("js") => MediaTypes.`application/javascript`
              case _ => ContentTypes.`text/plain`
            }
            sender ! HttpResponse(
              StatusCodes.OK,
              entity=HttpEntity.apply(mimeType, data),
              headers=List(
                `Access-Control-Allow-Origin`(spray.http.AllOrigins)
              )
            )
          }catch{case _: IOException =>
            sender ! HttpResponse(StatusCodes.NotFound)
          }
        }

      case Sockets.Upgraded =>
        sockets.add(sender)
        println("Browser Open n=" + sockets.size)
        self send Json.arr("boot")

      case f @ Frame(fin, rsv, Text, maskingKey, data) =>
        sockets.foreach(_ ! f.copy(maskingKey=None))

      case _: Tcp.ConnectionClosed =>
        if (sockets.contains(sender)) println("Browser Closed n=" + sockets.size )
        sockets.remove(sender)

      case x =>
    }
  }
}