All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalajs.jsenv.nodejs.AbstractNodeJSEnv.scala Maven / Gradle / Ivy

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


package org.scalajs.jsenv.nodejs

import java.io.{Console => _, _}
import java.net._

import org.scalajs.core.ir.Utils.escapeJS
import org.scalajs.core.tools.io._
import org.scalajs.core.tools.jsdep.ResolvedJSDependency
import org.scalajs.core.tools.logging.NullLogger
import org.scalajs.jsenv._
import org.scalajs.jsenv.Utils.OptDeadline

import scala.concurrent.TimeoutException
import scala.concurrent.duration._

abstract class AbstractNodeJSEnv(nodejsPath: String, addArgs: Seq[String],
    addEnv: Map[String, String], val sourceMap: Boolean)
    extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv {

  /** True, if the installed node executable supports source mapping.
   *
   *  Do `npm install source-map-support` if you need source maps.
   */
  lazy val hasSourceMapSupport: Boolean = {
    val code = new MemVirtualJSFile("source-map-support-probe.js")
      .withContent("""require('source-map-support').install();""")

    try {
      jsRunner(code).run(NullLogger, NullJSConsole)
      true
    } catch {
      case t: ExternalJSEnv.NonZeroExitException =>
        false
    }
  }

  protected def executable: String = nodejsPath

  /** Retry-timeout to wait for the JS VM to connect */
  protected val acceptTimeout = 5000

  protected trait AbstractNodeRunner extends AbstractExtRunner with JSInitFiles {

    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] = {
      if (sourceMap) Seq(
        new MemVirtualJSFile("sourceMapSupport.js").withContent(
          """
            |try {
            |  require('source-map-support').install();
            |} catch (e) {}
          """.stripMargin
        )
      ) else Seq()
    }

    /** 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() {
          |  function startsWithAnyOf(s, prefixes) {
          |    for (var i = 0; i < prefixes.length; i++) {
          |      // ES5 does not have .startsWith() on strings
          |      if (s.substring(0, prefixes[i].length) === prefixes[i])
          |        return true;
          |    }
          |    return false;
          |  }
          |  var nodeWillDeduplicateEvenForOneArgument = startsWithAnyOf(
          |      process.version, ["v0.", "v1.", "v2.0."]);
          |  var oldLog = console.log;
          |  var newLog = function() {
          |    var args = arguments;
          |    if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) {
          |      var argStr = args[0].toString();
          |      if (args.length > 1 || nodeWillDeduplicateEvenForOneArgument)
          |        argStr = argStr.replace(/%/g, "%%");
          |      args[0] = argStr;
          |    }
          |    oldLog.apply(console, args);
          |  };
          |  console.log = newLog;
          |})();
        """.stripMargin
      )
    )


    /** 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); }
          |};
        """.stripMargin
      )
    )

    override protected def initFiles(): Seq[VirtualJSFile] =
      installSourceMap() ++ fixPercentConsole() ++ runtimeEnv()

    /** 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 = file.file.getAbsolutePath
          writer.write(s"""require("${escapeJS(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
    }
  }

  protected trait NodeComJSRunner extends ComJSRunner with JSInitFiles {

    private[this] val serverSocket =
      new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address
    private var comSocket: Socket = _
    private var jvm2js: DataOutputStream = _
    private var js2jvm: DataInputStream = _

    abstract override protected def initFiles(): Seq[VirtualJSFile] =
      super.initFiles :+ comSetup

    private def comSetup(): VirtualJSFile = {
      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() {
             |    while (inBuffer.length >= 4) {
             |      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);
             |      socket.on('error', function(err) {
             |        // Whatever happens, this closes the Com
             |        socket.end();
             |
             |        // Expected errors:
             |        // - EPIPE on write: JVM closes
             |        // - ECONNREFUSED on connect: JVM closes before JS opens
             |        var expected = (
             |            err.syscall === "write"   && err.code === "EPIPE" ||
             |            err.syscall === "connect" && err.code === "ECONNREFUSED"
             |        );
             |
             |        if (!expected) {
             |          console.error("Scala.js Com failed: " + err);
             |          // We must terminate with an error
             |          process.exit(-1);
             |        }
             |      });
             |    },
             |    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.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2);
             |      socket.write(buf);
             |    },
             |    close: function() {
             |      if (socket === null) throw new Error("Com not open");
             |      socket.end();
             |    }
             |  }
             |}).call(this);
          """.stripMargin)
    }

    def send(msg: String): Unit = {
      if (awaitConnection()) {
        jvm2js.writeInt(msg.length)
        jvm2js.writeChars(msg)
        jvm2js.flush()
      }
    }

    def receive(timeout: Duration): String = {
      if (!awaitConnection())
        throw new ComJSEnv.ComClosedException("Node.js isn't connected")

      js2jvm.mark(Int.MaxValue)
      val savedSoTimeout = comSocket.getSoTimeout()
      try {
        val optDeadline = OptDeadline(timeout)

        comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt)
        val len = js2jvm.readInt()
        val carr = Array.fill(len) {
          comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt)
          js2jvm.readChar()
        }

        js2jvm.mark(0)
        String.valueOf(carr)
      } catch {
        case e: EOFException =>
          throw new ComJSEnv.ComClosedException(e)
        case e: SocketTimeoutException =>
          js2jvm.reset()
          throw new TimeoutException("Timeout expired")
      } finally {
        comSocket.setSoTimeout(savedSoTimeout)
      }
    }

    def close(): Unit = {
      serverSocket.close()
      if (jvm2js != null)
        jvm2js.close()
      if (js2jvm != null)
        js2jvm.close()
      if (comSocket != null)
        comSocket.close()
    }

    /** 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 finalize(): Unit = close()
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy