/* __ *\ ** ________ ___ / / ___ __ ____ Scala.js sbt plugin ** ** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL ** ** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** ** /____/\___/_/ |_/____/_/ | |__/ /____/ ** ** |/____/ ** \* */ package scala.scalajs.sbtplugin.env.nodejs import scala.scalajs.sbtplugin.env._ import scala.scalajs.sbtplugin.JSUtils.toJSstr import scala.scalajs.tools.io._ import scala.scalajs.tools.classpath._ import scala.scalajs.tools.env._ import scala.scalajs.tools.jsdep._ import scala.scalajs.tools.logging._ import scala.scalajs.sbtplugin.JSUtils._ import java.io.{ Console => _, _ } import java.net._ import scala.io.Source class NodeJSEnv( nodejsPath: String = "node", addArgs: Seq[String] = Seq.empty, addEnv: Map[String, String] = Map.empty ) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv { protected def vmName: String = "node.js" protected def executable: String = nodejsPath override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): JSRunner = { new NodeRunner(classpath, code, logger, console) } override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): AsyncJSRunner = { new AsyncNodeRunner(classpath, code, logger, console) } override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole): ComJSRunner = { new ComNodeRunner(classpath, code, logger, console) } protected class NodeRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole ) extends ExtRunner(classpath, code, logger, console) with AbstractNodeRunner protected class AsyncNodeRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole ) extends AsyncExtRunner(classpath, code, logger, console) with AbstractNodeRunner protected class ComNodeRunner(classpath: CompleteClasspath, code: VirtualJSFile, logger: Logger, console: JSConsole ) extends AsyncNodeRunner(classpath, code, logger, console) with ComJSRunner { /** Retry-timeout to wait for the JS VM to connect */ private final val acceptTimeout = 1000 private[this] val serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address private[this] var comSocket: Socket = _ private[this] var jvm2js: DataOutputStream = _ private[this] var js2jvm: DataInputStream = _ private def comSetup = new MemVirtualJSFile("comSetup.js").withContent( s""" (function() { // The socket for communication var socket = null; // The callback where received messages go var recvCallback = null; // Buffers received data var inBuffer = new Buffer(0); function onData(data) { inBuffer = Buffer.concat([inBuffer, data]); tryReadMsg(); } function tryReadMsg() { if (inBuffer.length < 4) return; var msgLen = inBuffer.readInt32BE(0); var byteLen = 4 + msgLen * 2; if (inBuffer.length < byteLen) return; var res = ""; for (var i = 0; i < msgLen; ++i) res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); inBuffer = inBuffer.slice(byteLen); recvCallback(res); } global.scalajsCom = { init: function(recvCB) { if (socket !== null) throw new Error("Com already open"); var net = require('net'); recvCallback = recvCB; socket = net.connect(${serverSocket.getLocalPort}); socket.on('data', onData); }, send: function(msg) { if (socket === null) throw new Error("Com not open"); var len = msg.length; var buf = new Buffer(4 + len * 2); buf.writeInt32BE(len, 0); for (var i = 0; i < len; ++i) buf.writeInt16BE(msg.charCodeAt(i), 4 + i * 2); socket.write(buf); }, close: function() { if (socket === null) throw new Error("Com not open"); socket.end(); } } }).call(this); """ ) def send(msg: String): Unit = { if (awaitConnection()) { jvm2js.writeInt(msg.length) jvm2js.writeChars(msg) jvm2js.flush() } } def receive(): String = { if (!awaitConnection()) throw new ComJSEnv.ComClosedException try { val len = js2jvm.readInt() val carr = Array.fill(len)(js2jvm.readChar()) String.valueOf(carr) } catch { case e: EOFException => throw new ComJSEnv.ComClosedException } } def close(): Unit = { serverSocket.close() if (jvm2js != null) jvm2js.close() if (js2jvm != null) js2jvm.close() if (comSocket != null) comSocket.close() } override def stop(): Unit = { close() super.stop() } /** Waits until the JS VM has established a connection or terminates * @return true if the connection was established */ private def awaitConnection(): Boolean = { serverSocket.setSoTimeout(acceptTimeout) while (comSocket == null && isRunning) { try { comSocket = serverSocket.accept() jvm2js = new DataOutputStream( new BufferedOutputStream(comSocket.getOutputStream())) js2jvm = new DataInputStream( new BufferedInputStream(comSocket.getInputStream())) } catch { case to: SocketTimeoutException => } } comSocket != null } override protected def initFiles(): Seq[VirtualJSFile] = super.initFiles :+ comSetup override protected def finalize(): Unit = close() } protected trait AbstractNodeRunner extends AbstractExtRunner { protected[this] val libCache = new VirtualFileMaterializer(true) /** File(s) to automatically install source-map-support. * Is used by [[initFiles]], override to change/disable. */ protected def installSourceMap(): Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("sourceMapSupport.js").withContent( """ try { require('source-map-support').install(); } catch (e) {} """ ) ) /** File(s) to hack console.log to prevent if from changing `%%` to `%`. * Is used by [[initFiles]], override to change/disable. */ protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("nodeConsoleHack.js").withContent( """ // Hack console log to duplicate double % signs (function() { var oldLog = console.log; var newLog = function() { var args = arguments; if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) { args[0] = args[0].toString().replace(/%/g, "%%"); } oldLog.apply(console, args); }; console.log = newLog; })(); """ ) ) /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`. * Is used by [[initFiles]], override to change/disable. */ protected def runtimeEnv(): Seq[VirtualJSFile] = Seq( new MemVirtualJSFile("scalaJSEnvInfo.js").withContent( """ __ScalaJSEnv = { exitFunction: function(status) { process.exit(status); } }; """ ) ) /** Concatenates results from [[installSourceMap]], [[fixPercentConsole]] and * [[runtimeEnv]] (in this order). */ override protected def initFiles(): Seq[VirtualJSFile] = installSourceMap() ++ fixPercentConsole() ++ runtimeEnv() /** Libraries are loaded via require in Node.js */ override protected def getLibJSFiles(): Seq[VirtualJSFile] = { initFiles() ++ classpath.jsLibs.map(requireLibrary) :+ classpath.scalaJSCode } /** Rewrites a library virtual file to a require statement if possible */ protected def requireLibrary(dep: ResolvedJSDependency): VirtualJSFile = { dep.info.commonJSName.fold(dep.lib) { varname => val fname = dep.lib.name libCache.materialize(dep.lib) new MemVirtualJSFile(s"require-$fname").withContent( s"""$varname = require(${toJSstr(fname)});""" ) } } // Send code to Stdin override protected def sendVMStdin(out: OutputStream): Unit = { sendJS(getJSFiles(), out) } /** write a single JS file to a writer using an include fct if appropriate * uses `require` if the file exists on the filesystem */ override protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = { file match { case file: FileVirtualJSFile => val fname = toJSstr(file.file.getAbsolutePath) writer.write(s"require($fname);\n") case _ => super.writeJSFile(file, writer) } } // Node.js specific (system) environment override protected def getVMEnv(): Map[String, String] = { val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty) val nodePath = libCache.cacheDir.getAbsolutePath + baseNodePath.fold("")(p => File.pathSeparator + p) sys.env ++ Seq( "NODE_MODULE_CONTEXTS" -> "0", "NODE_PATH" -> nodePath ) ++ additionalEnv } } }