summaryrefslogtreecommitdiff
path: root/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs
diff options
context:
space:
mode:
Diffstat (limited to 'examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs')
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala126
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala466
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala63
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala10
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala10
5 files changed, 675 insertions, 0 deletions
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala
new file mode 100644
index 0000000..3dec79c
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala
@@ -0,0 +1,126 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+import javax.servlet.http.HttpServletRequest
+
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.nio.SelectChannelConnector
+import org.eclipse.jetty.websocket.{WebSocket, WebSocketHandler}
+import org.eclipse.jetty.util.component.{LifeCycle, AbstractLifeCycle}
+import org.eclipse.jetty.util.log
+
+private[phantomjs] final class JettyWebsocketManager(
+ wsListener: WebsocketListener) extends WebsocketManager { thisMgr =>
+
+ private[this] var webSocketConn: WebSocket.Connection = null
+ private[this] var closed = false
+
+ // We can just set the logger here, since we are supposed to be protected by
+ // the private ClassLoader that loads us reflectively.
+ log.Log.setLog(new WSLogger("root"))
+
+ private[this] val connector = new SelectChannelConnector
+
+ connector.setHost("localhost")
+ connector.setPort(0)
+
+ private[this] val server = new Server()
+
+ server.addConnector(connector)
+ server.setHandler(new WebSocketHandler {
+ // Support Hixie 76 for Phantom.js
+ getWebSocketFactory().setMinVersion(-1)
+
+ override def doWebSocketConnect(
+ request: HttpServletRequest, protocol: String): WebSocket =
+ new ComWebSocketListener
+ })
+
+ server.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener {
+ override def lifeCycleStarted(event: LifeCycle): Unit = {
+ if (event.isRunning())
+ wsListener.onRunning()
+ }
+ })
+
+ private class ComWebSocketListener extends WebSocket.OnTextMessage {
+ override def onOpen(connection: WebSocket.Connection): Unit = {
+ thisMgr.synchronized {
+ if (isConnected)
+ throw new IllegalStateException("Client connected twice")
+ webSocketConn = connection
+ }
+ wsListener.onOpen()
+ }
+
+ override def onClose(statusCode: Int, reason: String): Unit = {
+ thisMgr.synchronized {
+ webSocketConn = null
+ closed = true
+ }
+ wsListener.onClose()
+ server.stop()
+
+ if (statusCode != 1000) {
+ throw new Exception("Abnormal closing of connection. " +
+ s"Code: $statusCode, Reason: $reason")
+ }
+ }
+
+ override def onMessage(message: String): Unit =
+ wsListener.onMessage(message)
+ }
+
+ private class WSLogger(fullName: String) extends log.AbstractLogger {
+ private[this] var debugEnabled = false
+
+ def debug(msg: String, args: Object*): Unit =
+ if (debugEnabled) log("DEBUG", msg, args)
+
+ def debug(msg: String, thrown: Throwable): Unit =
+ if (debugEnabled) log("DEBUG", msg, thrown)
+
+ def debug(thrown: Throwable): Unit =
+ if (debugEnabled) log("DEBUG", thrown)
+
+ def getName(): String = fullName
+
+ def ignore(ignored: Throwable): Unit = ()
+
+ def info(msg: String, args: Object*): Unit = log("INFO", msg, args)
+ def info(msg: String, thrown: Throwable): Unit = log("INFO", msg, thrown)
+ def info(thrown: Throwable): Unit = log("INFO", thrown)
+
+ def warn(msg: String, args: Object*): Unit = log("WARN", msg, args)
+ def warn(msg: String, thrown: Throwable): Unit = log("WARN", msg, thrown)
+ def warn(thrown: Throwable): Unit = log("WARN", thrown)
+
+ def isDebugEnabled(): Boolean = debugEnabled
+ def setDebugEnabled(enabled: Boolean): Unit = debugEnabled = enabled
+
+ private def log(lvl: String, msg: String, args: Object*): Unit =
+ wsListener.log(s"$lvl: $msg " + args.mkString(", "))
+
+ private def log(lvl: String, msg: String, thrown: Throwable): Unit =
+ wsListener.log(s"$lvl: $msg $thrown\n{$thrown.getStackStrace}")
+
+ private def log(lvl: String, thrown: Throwable): Unit =
+ wsListener.log(s"$lvl: $thrown\n{$thrown.getStackStrace}")
+
+ protected def newLogger(fullName: String) = new WSLogger(fullName)
+ }
+
+ def start(): Unit = server.start()
+
+ def stop(): Unit = server.stop()
+
+ def isConnected: Boolean = webSocketConn != null && !closed
+ def isClosed: Boolean = closed
+
+ def localPort: Int = connector.getLocalPort()
+
+ def sendMessage(msg: String) = synchronized {
+ if (webSocketConn != null)
+ webSocketConn.sendMessage(msg)
+ }
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala
new file mode 100644
index 0000000..7bb47d2
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala
@@ -0,0 +1,466 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env.phantomjs
+
+import scala.scalajs.sbtplugin.env._
+
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.logging._
+
+import scala.scalajs.sbtplugin.JSUtils._
+
+import java.io.{ Console => _, _ }
+import java.net._
+
+import scala.io.Source
+import scala.collection.mutable
+import scala.annotation.tailrec
+
+class PhantomJSEnv(
+ phantomjsPath: String = "phantomjs",
+ addArgs: Seq[String] = Seq.empty,
+ addEnv: Map[String, String] = Map.empty,
+ val autoExit: Boolean = true,
+ jettyClassLoader: ClassLoader = getClass().getClassLoader()
+) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv {
+
+ import PhantomJSEnv._
+
+ protected def vmName: String = "PhantomJS"
+ protected def executable: String = phantomjsPath
+
+ override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): JSRunner = {
+ new PhantomRunner(classpath, code, logger, console)
+ }
+
+ override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): AsyncJSRunner = {
+ new AsyncPhantomRunner(classpath, code, logger, console)
+ }
+
+ override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): ComJSRunner = {
+ new ComPhantomRunner(classpath, code, logger, console)
+ }
+
+ protected class PhantomRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends ExtRunner(classpath, code, logger, console)
+ with AbstractPhantomRunner
+
+ protected class AsyncPhantomRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncExtRunner(classpath, code, logger, console)
+ with AbstractPhantomRunner
+
+ protected class ComPhantomRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncPhantomRunner(classpath, code, logger, console)
+ with ComJSRunner with WebsocketListener {
+
+ private def loadMgr() = {
+ val clazz = jettyClassLoader.loadClass(
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager")
+
+ val ctors = clazz.getConstructors()
+ assert(ctors.length == 1, "JettyWebsocketManager may only have one ctor")
+
+ val mgr = ctors.head.newInstance(this)
+
+ mgr.asInstanceOf[WebsocketManager]
+ }
+
+ val mgr: WebsocketManager = loadMgr()
+
+ def onRunning(): Unit = synchronized(notifyAll())
+ def onOpen(): Unit = synchronized(notifyAll())
+ def onClose(): Unit = synchronized(notifyAll())
+
+ def onMessage(msg: String): Unit = synchronized {
+ recvBuf.enqueue(msg)
+ notifyAll()
+ }
+
+ def log(msg: String): Unit = logger.debug(s"PhantomJS WS Jetty: $msg")
+
+ private[this] val recvBuf = mutable.Queue.empty[String]
+
+ mgr.start()
+
+ /** The websocket server starts asynchronously, but we need the port it is
+ * running on. This method waits until the port is non-negative and
+ * returns its value.
+ */
+ private def waitForPort(): Int = {
+ while (mgr.localPort < 0)
+ wait()
+ mgr.localPort
+ }
+
+ private def comSetup = {
+ def maybeExit(code: Int) =
+ if (autoExit)
+ s"window.callPhantom({ action: 'exit', returnValue: $code });"
+ else
+ ""
+
+ val serverPort = waitForPort()
+
+ val code = s"""
+ |(function() {
+ | var MaxPayloadSize = $MaxCharPayloadSize;
+ |
+ | // The socket for communication
+ | var websocket = null;
+ |
+ | // Buffer for messages sent before socket is open
+ | var outMsgBuf = null;
+ |
+ | function sendImpl(msg) {
+ | var frags = (msg.length / MaxPayloadSize) | 0;
+ |
+ | for (var i = 0; i < frags; ++i) {
+ | var payload = msg.substring(
+ | i * MaxPayloadSize, (i + 1) * MaxPayloadSize);
+ | websocket.send("1" + payload);
+ | }
+ |
+ | websocket.send("0" + msg.substring(frags * MaxPayloadSize));
+ | }
+ |
+ | function recvImpl(recvCB) {
+ | var recvBuf = "";
+ |
+ | return function(evt) {
+ | var newData = recvBuf + evt.data.substring(1);
+ | if (evt.data.charAt(0) == "0") {
+ | recvBuf = "";
+ | recvCB(newData);
+ | } else if (evt.data.charAt(0) == "1") {
+ | recvBuf = newData;
+ | } else {
+ | throw new Error("Bad fragmentation flag in " + evt.data);
+ | }
+ | };
+ | }
+ |
+ | window.scalajsCom = {
+ | init: function(recvCB) {
+ | if (websocket !== null) throw new Error("Com already open");
+ |
+ | outMsgBuf = [];
+ |
+ | websocket = new WebSocket("ws://localhost:$serverPort");
+ |
+ | websocket.onopen = function(evt) {
+ | for (var i = 0; i < outMsgBuf.length; ++i)
+ | sendImpl(outMsgBuf[i]);
+ | outMsgBuf = null;
+ | };
+ | websocket.onclose = function(evt) {
+ | websocket = null;
+ | ${maybeExit(0)}
+ | };
+ | websocket.onmessage = recvImpl(recvCB);
+ | websocket.onerror = function(evt) {
+ | websocket = null;
+ | throw new Error("Websocket failed: " + evt);
+ | };
+ |
+ | // Take over responsibility to auto exit
+ | window.callPhantom({
+ | action: 'setAutoExit',
+ | autoExit: false
+ | });
+ | },
+ | send: function(msg) {
+ | if (websocket === null)
+ | return; // we are closed already. ignore message
+ |
+ | if (outMsgBuf !== null)
+ | outMsgBuf.push(msg);
+ | else
+ | sendImpl(msg);
+ | },
+ | close: function() {
+ | if (websocket === null)
+ | return; // we are closed already. all is well.
+ |
+ | if (outMsgBuf !== null)
+ | // Reschedule ourselves to give onopen a chance to kick in
+ | window.setTimeout(window.scalajsCom.close, 10);
+ | else
+ | websocket.close();
+ | }
+ | }
+ |}).call(this);""".stripMargin
+
+ new MemVirtualJSFile("comSetup.js").withContent(code)
+ }
+
+ def send(msg: String): Unit = synchronized {
+ if (awaitConnection()) {
+ val fragParts = msg.length / MaxCharPayloadSize
+
+ for (i <- 0 until fragParts) {
+ val payload = msg.substring(
+ i * MaxCharPayloadSize, (i + 1) * MaxCharPayloadSize)
+ mgr.sendMessage("1" + payload)
+ }
+
+ mgr.sendMessage("0" + msg.substring(fragParts * MaxCharPayloadSize))
+ }
+ }
+
+ def receive(): String = synchronized {
+ if (recvBuf.isEmpty && !awaitConnection())
+ throw new ComJSEnv.ComClosedException
+
+ @tailrec
+ def loop(acc: String): String = {
+ val frag = receiveFrag()
+ val newAcc = acc + frag.substring(1)
+
+ if (frag(0) == '0')
+ newAcc
+ else if (frag(0) == '1')
+ loop(newAcc)
+ else
+ throw new AssertionError("Bad fragmentation flag in " + frag)
+ }
+
+ loop("")
+ }
+
+ private def receiveFrag(): String = {
+ while (recvBuf.isEmpty && !mgr.isClosed)
+ wait()
+
+ if (recvBuf.isEmpty)
+ throw new ComJSEnv.ComClosedException
+ else
+ recvBuf.dequeue()
+ }
+
+ def close(): Unit = mgr.stop()
+
+ override def stop(): Unit = {
+ close()
+ super.stop()
+ }
+
+ /** Waits until the JS VM has established a connection, or the VM
+ * terminated. Returns true if a connection was established.
+ */
+ private def awaitConnection(): Boolean = {
+ while (!mgr.isConnected && !mgr.isClosed && isRunning)
+ wait(200) // We sleep-wait for isRunning
+
+ mgr.isConnected
+ }
+
+ override protected def initFiles(): Seq[VirtualJSFile] =
+ super.initFiles :+ comSetup
+ }
+
+ protected trait AbstractPhantomRunner extends AbstractExtRunner {
+
+ override protected def getVMArgs() =
+ // Add launcher file to arguments
+ additionalArgs :+ createTmpLauncherFile().getAbsolutePath
+
+ /** In phantom.js, we include JS using HTML */
+ override protected def writeJSFile(file: VirtualJSFile, writer: Writer) = {
+ file match {
+ case file: FileVirtualJSFile =>
+ val fname = htmlEscape(file.file.getAbsolutePath)
+ writer.write(
+ s"""<script type="text/javascript" src="$fname"></script>""" + "\n")
+ case _ =>
+ writer.write("""<script type="text/javascript">""" + "\n")
+ writer.write(s"// Virtual File: ${file.path}\n")
+ writer.write(file.content)
+ writer.write("</script>\n")
+ }
+ }
+
+ /**
+ * PhantomJS doesn't support Function.prototype.bind. We polyfill it.
+ * https://github.com/ariya/phantomjs/issues/10522
+ */
+ override protected def initFiles(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("bindPolyfill.js").withContent(
+ """
+ |// Polyfill for Function.bind from Facebook react:
+ |// https://github.com/facebook/react/blob/3dc10749080a460e48bee46d769763ec7191ac76/src/test/phantomjs-shims.js
+ |// Originally licensed under Apache 2.0
+ |(function() {
+ |
+ | var Ap = Array.prototype;
+ | var slice = Ap.slice;
+ | var Fp = Function.prototype;
+ |
+ | if (!Fp.bind) {
+ | // PhantomJS doesn't support Function.prototype.bind natively, so
+ | // polyfill it whenever this module is required.
+ | Fp.bind = function(context) {
+ | var func = this;
+ | var args = slice.call(arguments, 1);
+ |
+ | function bound() {
+ | var invokedAsConstructor = func.prototype && (this instanceof func);
+ | return func.apply(
+ | // Ignore the context parameter when invoking the bound function
+ | // as a constructor. Note that this includes not only constructor
+ | // invocations using the new keyword but also calls to base class
+ | // constructors such as BaseClass.call(this, ...) or super(...).
+ | !invokedAsConstructor && context || this,
+ | args.concat(slice.call(arguments))
+ | );
+ | }
+ |
+ | // The bound function must share the .prototype of the unbound
+ | // function so that any object created by one constructor will count
+ | // as an instance of both constructors.
+ | bound.prototype = func.prototype;
+ |
+ | return bound;
+ | };
+ | }
+ |
+ |})();
+ |""".stripMargin
+ ),
+ new MemVirtualJSFile("scalaJSEnvInfo.js").withContent(
+ """
+ |__ScalaJSEnv = {
+ | exitFunction: function(status) {
+ | window.callPhantom({
+ | action: 'exit',
+ | returnValue: status | 0
+ | });
+ | }
+ |};
+ """.stripMargin
+ )
+ )
+
+ protected def writeWebpageLauncher(out: Writer): Unit = {
+ out.write("<html>\n<head>\n<title>Phantom.js Launcher</title>\n")
+ sendJS(getLibJSFiles(), out)
+ writeCodeLauncher(code, out)
+ out.write("</head>\n<body></body>\n</html>\n")
+ }
+
+ protected def createTmpLauncherFile(): File = {
+ val webF = createTmpWebpage()
+
+ val launcherTmpF = File.createTempFile("phantomjs-launcher", ".js")
+ launcherTmpF.deleteOnExit()
+
+ val out = new FileWriter(launcherTmpF)
+
+ try {
+ out.write(
+ s"""// Scala.js Phantom.js launcher
+ |var page = require('webpage').create();
+ |var url = ${toJSstr(webF.getAbsolutePath)};
+ |var autoExit = $autoExit;
+ |page.onConsoleMessage = function(msg) {
+ | console.log(msg);
+ |};
+ |page.onError = function(msg, trace) {
+ | console.error(msg);
+ | if (trace && trace.length) {
+ | console.error('');
+ | trace.forEach(function(t) {
+ | console.error(' ' + t.file + ':' + t.line + (t.function ? ' (in function "' + t.function +'")' : ''));
+ | });
+ | }
+ |
+ | phantom.exit(2);
+ |};
+ |page.onCallback = function(data) {
+ | if (!data.action) {
+ | console.error('Called callback without action');
+ | phantom.exit(3);
+ | } else if (data.action === 'exit') {
+ | phantom.exit(data.returnValue || 0);
+ | } else if (data.action === 'setAutoExit') {
+ | if (typeof(data.autoExit) === 'boolean')
+ | autoExit = data.autoExit;
+ | else
+ | autoExit = true;
+ | } else {
+ | console.error('Unknown callback action ' + data.action);
+ | phantom.exit(4);
+ | }
+ |};
+ |page.open(url, function (status) {
+ | if (autoExit || status !== 'success')
+ | phantom.exit(status !== 'success');
+ |});
+ |""".stripMargin)
+ } finally {
+ out.close()
+ }
+
+ logger.debug(
+ "PhantomJS using launcher at: " + launcherTmpF.getAbsolutePath())
+
+ launcherTmpF
+ }
+
+ protected def createTmpWebpage(): File = {
+ val webTmpF = File.createTempFile("phantomjs-launcher-webpage", ".html")
+ webTmpF.deleteOnExit()
+
+ val out = new BufferedWriter(new FileWriter(webTmpF))
+ try {
+ writeWebpageLauncher(out)
+ } finally {
+ out.close()
+ }
+
+ logger.debug(
+ "PhantomJS using webpage launcher at: " + webTmpF.getAbsolutePath())
+
+ webTmpF
+ }
+
+ protected def writeCodeLauncher(code: VirtualJSFile, out: Writer): Unit = {
+ out.write("""<script type="text/javascript">""" + "\n")
+ out.write("// Phantom.js code launcher\n")
+ out.write(s"// Origin: ${code.path}\n")
+ out.write("window.addEventListener('load', function() {\n")
+ out.write(code.content)
+ out.write("}, false);\n")
+ out.write("</script>\n")
+ }
+ }
+
+ protected def htmlEscape(str: String): String = str.flatMap {
+ case '<' => "&lt;"
+ case '>' => "&gt;"
+ case '"' => "&quot;"
+ case '&' => "&amp;"
+ case c => c :: Nil
+ }
+
+}
+
+object PhantomJSEnv {
+ private final val MaxByteMessageSize = 32768 // 32 KB
+ private final val MaxCharMessageSize = MaxByteMessageSize / 2 // 2B per char
+ private final val MaxCharPayloadSize = MaxCharMessageSize - 1 // frag flag
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala
new file mode 100644
index 0000000..02c229b
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala
@@ -0,0 +1,63 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+import scala.scalajs.tools.io.IO
+
+/** A special [[ClassLoader]] to load the Jetty 8 dependency of [[PhantomJSEnv]]
+ * in a private space.
+ *
+ * It loads everything that belongs to [[JettyWebsocketManager]] itself (while
+ * retrieving the requested class file from its parent.
+ * For all other classes, it first tries to load them from [[jettyLoader]],
+ * which should only contain the Jetty 8 classpath.
+ * If this fails, it delegates to its parent.
+ *
+ * The rationale is, that [[JettyWebsocketManager]] and its dependees can use
+ * the classes on the Jetty 8 classpath, while they remain hidden from the rest
+ * of the Java world. This allows to load another version of Jetty in the same
+ * JVM for the rest of the project.
+ */
+private[sbtplugin] class PhantomJettyClassLoader(jettyLoader: ClassLoader,
+ parent: ClassLoader) extends ClassLoader(parent) {
+
+ def this(loader: ClassLoader) =
+ this(loader, ClassLoader.getSystemClassLoader())
+
+ /** Classes needed to bridge private jetty classpath and public PhantomJS
+ * Basically everything defined in JettyWebsocketManager.
+ */
+ private val bridgeClasses = Set(
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$WSLogger",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$ComWebSocketListener",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$$anon$1",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$$anon$2"
+ )
+
+ override protected def loadClass(name: String, resolve: Boolean): Class[_] = {
+ if (bridgeClasses.contains(name)) {
+ // Load bridgeClasses manually since they must be associated to this
+ // class loader, rather than the parent class loader in order to find the
+ // jetty classes
+
+ // First check if we have loaded it already
+ Option(findLoadedClass(name)) getOrElse {
+ val wsManager =
+ parent.getResourceAsStream(name.replace('.', '/') + ".class")
+
+ if (wsManager == null) {
+ throw new ClassNotFoundException(name)
+ } else {
+ val buf = IO.readInputStreamToByteArray(wsManager)
+ defineClass(name, buf, 0, buf.length)
+ }
+ }
+ } else {
+ try {
+ jettyLoader.loadClass(name)
+ } catch {
+ case _: ClassNotFoundException =>
+ super.loadClass(name, resolve)
+ }
+ }
+ }
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala
new file mode 100644
index 0000000..4faac64
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala
@@ -0,0 +1,10 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+private[phantomjs] trait WebsocketListener {
+ def onRunning(): Unit
+ def onOpen(): Unit
+ def onClose(): Unit
+ def onMessage(msg: String): Unit
+
+ def log(msg: String): Unit
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala
new file mode 100644
index 0000000..a466841
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala
@@ -0,0 +1,10 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+private[phantomjs] trait WebsocketManager {
+ def start(): Unit
+ def stop(): Unit
+ def sendMessage(msg: String): Unit
+ def localPort: Int
+ def isConnected: Boolean
+ def isClosed: Boolean
+}