summaryrefslogblamecommitdiff
path: root/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala
blob: cd35ff62399ac736f8cc7d6994d0ac036be2a638 (plain) (tree)














































































































































































































































































































                                                                                         
/*                     __                                               *\
**     ________ ___   / /  ___      __ ____  Scala.js sbt plugin        **
**    / __/ __// _ | / /  / _ | __ / // __/  (c) 2013, LAMP/EPFL        **
**  __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \    http://scala-js.org/       **
** /____/\___/_/ |_/____/_/ | |__/ /____/                               **
**                          |/____/                                     **
\*                                                                      */


package scala.scalajs.sbtplugin.env.rhino

import scala.scalajs.tools.sem.Semantics
import scala.scalajs.tools.io._
import scala.scalajs.tools.classpath._
import scala.scalajs.tools.env._
import scala.scalajs.tools.logging._

import scala.io.Source

import scala.collection.mutable

import scala.concurrent.{Future, Promise, Await}
import scala.concurrent.duration.Duration

import org.mozilla.javascript._

class RhinoJSEnv(semantics: Semantics,
    withDOM: Boolean = false) extends ComJSEnv {

  import RhinoJSEnv._

  /** Executes code in an environment where the Scala.js library is set up to
   *  load its classes lazily.
   *
   *  Other .js scripts in the inputs are executed eagerly before the provided
   *  `code` is called.
   */
  override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole): JSRunner = {
    new Runner(classpath, code, logger, console)
  }

  private class Runner(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole) extends JSRunner {
    def run(): Unit = internalRunJS(classpath, code, logger, console, None)
  }

  override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole): AsyncJSRunner = {
    new AsyncRunner(classpath, code, logger, console)
  }

  private class AsyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole) extends AsyncJSRunner {

    private[this] val promise = Promise[Unit]

    private[this] val thread = new Thread {
      override def run(): Unit = {
        try {
          internalRunJS(classpath, code, logger, console, optChannel)
          promise.success(())
        } catch {
          case t: Throwable =>
            promise.failure(t)
        }
      }
    }

    def start(): Future[Unit] = {
      thread.start()
      promise.future
    }

    def stop(): Unit = thread.interrupt()

    def isRunning(): Boolean = !promise.isCompleted

    def await(): Unit = Await.result(promise.future, Duration.Inf)

    protected def optChannel(): Option[Channel] = None
  }

  override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole): ComJSRunner = {
    new ComRunner(classpath, code, logger, console)
  }

  private class ComRunner(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole)
      extends AsyncRunner(classpath, code, logger, console) with ComJSRunner {

    private[this] val channel = new Channel

    override protected def optChannel(): Option[Channel] = Some(channel)

    def send(msg: String): Unit = {
      try {
        channel.sendToJS(msg)
      } catch {
        case _: ChannelClosedException =>
          throw new ComJSEnv.ComClosedException
      }
    }

    def receive(): String = {
      try {
        channel.recvJVM()
      } catch {
        case _: ChannelClosedException =>
          throw new ComJSEnv.ComClosedException
      }
    }

    def close(): Unit = channel.close()

    override def stop(): Unit = {
      close()
      super.stop()
    }

  }

  private def internalRunJS(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole, optChannel: Option[Channel]): Unit = {

    val context = Context.enter()
    try {
      val scope = context.initStandardObjects()

      if (withDOM) {
        // Fetch env.rhino.js from webjar
        val name = "env.rhino.js"
        val path = "/META-INF/resources/webjars/envjs/1.2/" + name
        val resource = getClass.getResource(path)
        assert(resource != null, s"need $name as resource")

        // Rhino can't optimize envjs
        context.setOptimizationLevel(-1)

        // Don't print envjs header
        scope.addFunction("print", args => ())

        // Pipe file to Rhino
        val reader = Source.fromURL(resource).bufferedReader
        context.evaluateReader(scope, reader, name, 1, null);

        // No need to actually define print here: It is captured by envjs to
        // implement console.log, which we'll override in the next statement
      }

      // Make sure Rhino does not do its magic for JVM top-level packages (#364)
      val PackagesObject =
        ScriptableObject.getProperty(scope, "Packages").asInstanceOf[Scriptable]
      val topLevelPackageIds = ScriptableObject.getPropertyIds(PackagesObject)
      for (id <- topLevelPackageIds) (id: Any) match {
        case name: String => ScriptableObject.deleteProperty(scope, name)
        case index: Int   => ScriptableObject.deleteProperty(scope, index)
        case _            => // should not happen, I think, but with Rhino you never know
      }

      // Setup console.log
      val jsconsole = context.newObject(scope)
      jsconsole.addFunction("log", _.foreach(console.log _))
      ScriptableObject.putProperty(scope, "console", jsconsole)

      // Optionally setup scalaJSCom
      var recvCallback: Option[String => Unit] = None
      for (channel <- optChannel) {
        val comObj = context.newObject(scope)

        comObj.addFunction("send", s =>
          channel.sendToJVM(Context.toString(s(0))))

        comObj.addFunction("init", s => s(0) match {
          case f: Function =>
            val cb: String => Unit =
              msg => f.call(context, scope, scope, Array(msg))
            recvCallback = Some(cb)
          case _ =>
            sys.error("First argument to init must be a function")
        })

        comObj.addFunction("close", _ => {
          // Tell JVM side we won't send anything
          channel.close()
          // Internally register that we're done
          recvCallback = None
        })

        ScriptableObject.putProperty(scope, "scalajsCom", comObj)
      }

      try {
        // Make the classpath available. Either through lazy loading or by
        // simply inserting
        classpath match {
          case cp: IRClasspath =>
            // Setup lazy loading classpath and source mapper
            val optLoader = if (cp.scalaJSIR.nonEmpty) {
              val loader = new ScalaJSCoreLib(semantics, cp)

              // Setup sourceMapper
              val scalaJSenv = context.newObject(scope)

              scalaJSenv.addFunction("sourceMapper", args => {
                val trace = Context.toObject(args(0), scope)
                loader.mapStackTrace(trace, context, scope)
              })

              ScriptableObject.putProperty(scope, "__ScalaJSEnv", scalaJSenv)

              Some(loader)
            } else {
              None
            }

            // Load JS libraries
            cp.jsLibs.foreach(dep => context.evaluateFile(scope, dep.lib))

            optLoader.foreach(_.insertInto(context, scope))
          case cp =>
            cp.allCode.foreach(context.evaluateFile(scope, _))
        }

        context.evaluateFile(scope, code)

        // Callback the com channel if necessary (if comCallback = None, channel
        // wasn't initialized on the client)
        for ((channel, callback) <- optChannel zip recvCallback) {
          try {
            while (recvCallback.isDefined)
              callback(channel.recvJS())
          } catch {
            case _: ChannelClosedException =>
              // the JVM side closed the connection
          }
        }

        // Enusre the channel is closed to release JVM side
        optChannel.foreach(_.close)

      } catch {
        case e: RhinoException =>
          // Trace here, since we want to be in the context to trace.
          logger.trace(e)
          sys.error(s"Exception while running JS code: ${e.getMessage}")
      }
    } finally {
      Context.exit()
    }
  }

}

object RhinoJSEnv {

  /** Communication channel between the Rhino thread and the rest of the JVM */
  private class Channel {
    private[this] var _closed = false
    private[this] val js2jvm = mutable.Queue.empty[String]
    private[this] val jvm2js = mutable.Queue.empty[String]

    def sendToJS(msg: String): Unit = synchronized {
      jvm2js.enqueue(msg)
      notify()
    }

    def sendToJVM(msg: String): Unit = synchronized {
      js2jvm.enqueue(msg)
      notify()
    }

    def recvJVM(): String = synchronized {
      while (js2jvm.isEmpty && ensureOpen())
        wait()

      js2jvm.dequeue()
    }

    def recvJS(): String = synchronized {
      while (jvm2js.isEmpty && ensureOpen())
        wait()

      jvm2js.dequeue()
    }

    def close(): Unit = synchronized {
      _closed = true
      notify()
    }

    /** Throws if the channel is closed and returns true */
    private def ensureOpen(): Boolean = {
      if (_closed)
        throw new ChannelClosedException
      true
    }
  }

  private class ChannelClosedException extends Exception

}