summaryrefslogtreecommitdiff
path: root/Plugin.scala
blob: 1249d3a8003ed14df7bd1d5fdecb3e8e6e47c388 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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 =>
    }
  }
}