diff options
Diffstat (limited to 'examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala')
-rw-r--r-- | examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala new file mode 100644 index 0000000..cd35ff6 --- /dev/null +++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala @@ -0,0 +1,303 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ 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 + +} |