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

org.scalajs.jsenv.ExternalJSEnv.scala Maven / Gradle / Ivy

package org.scalajs.jsenv

import org.scalajs.core.tools.io._
import org.scalajs.core.tools.logging.Logger
import org.scalajs.core.tools.jsdep.ResolvedJSDependency

import java.io.{ Console => _, _ }
import scala.io.Source

import scala.concurrent.{Future, Promise}
import scala.util.Try

abstract class ExternalJSEnv(
  final protected val additionalArgs: Seq[String],
  final protected val additionalEnv:  Map[String, String]) extends AsyncJSEnv {

  import ExternalJSEnv._

  def name: String = s"ExternalJSEnv for $vmName"

  /** Printable name of this VM */
  protected def vmName: String

  /** Command to execute (on shell) for this VM */
  protected def executable: String

  /** Custom initialization scripts. */
  protected def customInitFiles(): Seq[VirtualJSFile] = Nil

  protected class AbstractExtRunner(
      protected val libs: Seq[ResolvedJSDependency],
      protected val code: VirtualJSFile) extends JSInitFiles {

    private[this] var _logger: Logger = _
    private[this] var _console: JSConsole = _

    protected def logger: Logger = _logger
    protected def console: JSConsole = _console

    protected def setupLoggerAndConsole(logger: Logger, console: JSConsole) = {
      require(_logger == null && _console == null)
      _logger = logger
      _console = console
    }

    /** Custom initialization scripts, defined by the environment. */
    final protected def customInitFiles(): Seq[VirtualJSFile] =
      ExternalJSEnv.this.customInitFiles()

    /** Sends required data to VM Stdin (can throw) */
    protected def sendVMStdin(out: OutputStream): Unit = {}

    /** VM arguments excluding executable. Override to adapt.
     *  Overrider is responsible to add additionalArgs.
     */
    protected def getVMArgs(): Seq[String] = additionalArgs

    /** VM environment. Override to adapt.
     *
     *  Default is `sys.env` and [[additionalEnv]]
     */
    protected def getVMEnv(): Map[String, String] =
      sys.env ++ additionalEnv

    /** Get files that are a library (i.e. that do not run anything) */
    protected def getLibJSFiles(): Seq[VirtualJSFile] =
      initFiles() ++ customInitFiles() ++ libs.map(_.lib)

    /** Get all files that are passed to VM (libraries and code) */
    protected def getJSFiles(): Seq[VirtualJSFile] =
      getLibJSFiles() :+ code

    /** write a single JS file to a writer using an include fct if appropriate */
    protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = {
      // The only platform-independent way to do this in JS is to dump the file.
      writer.write(file.content)
      writer.write('\n')
    }

    /** Pipe stdin and stdout from/to VM */
    final protected def pipeVMData(vmInst: Process): Unit = {
      // Send stdin to VM.
      val out = vmInst.getOutputStream()
      try { sendVMStdin(out) }
      finally { out.close() }

      // Pipe stdout to console
      pipeToConsole(vmInst.getInputStream(), console)

      // We are probably done (stdin is closed). Report any errors
      val errSrc = Source.fromInputStream(vmInst.getErrorStream(), "UTF-8")
      try { errSrc.getLines.foreach(err => logger.error(err)) }
      finally { errSrc.close }
    }

    /** Wait for the VM to terminate, verify exit code
     *
     *  @throws ExternalJSEnv.NonZeroExitException if VM returned a non-zero code
     */
    final protected def waitForVM(vmInst: Process): Unit = {
      // Make sure we are done.
      vmInst.waitFor()

      // Get return value and return
      val retVal = vmInst.exitValue
      if (retVal != 0)
        throw new NonZeroExitException(vmName, retVal)
    }

    protected def startVM(): Process = {
      val vmArgs = getVMArgs()
      val vmEnv  = getVMEnv()

      val allArgs = executable +: vmArgs
      val pBuilder = new ProcessBuilder(allArgs: _*)

      pBuilder.environment().clear()
      for ((name, value) <- vmEnv)
        pBuilder.environment().put(name, value)

      logger.debug("Starting process: " + allArgs.mkString(" "))

      pBuilder.start()
    }

    /** send a bunch of JS files to an output stream */
    final protected def sendJS(files: Seq[VirtualJSFile],
        out: OutputStream): Unit = {
      val writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"))
      try sendJS(files, writer)
      finally writer.close()
    }

    /** send a bunch of JS files to a writer */
    final protected def sendJS(files: Seq[VirtualJSFile], out: Writer): Unit =
      files.foreach { writeJSFile(_, out) }

    /** pipe lines from input stream to JSConsole */
    final protected def pipeToConsole(in: InputStream, console: JSConsole) = {
      val source = Source.fromInputStream(in, "UTF-8")
      try { source.getLines.foreach(console.log _) }
      finally { source.close() }
    }

  }

  protected class ExtRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile)
      extends AbstractExtRunner(libs, code) with JSRunner {

    def run(logger: Logger, console: JSConsole): Unit = {
      setupLoggerAndConsole(logger, console)

      val vmInst = startVM()

      pipeVMData(vmInst)
      waitForVM(vmInst)
    }
  }

  protected class AsyncExtRunner(libs: Seq[ResolvedJSDependency], code: VirtualJSFile)
      extends AbstractExtRunner(libs, code) with AsyncJSRunner {

    private[this] var vmInst: Process = null
    private[this] var ioThreadEx: Throwable = null
    private[this] val promise = Promise[Unit]

    private[this] val thread = new Thread {
      override def run(): Unit = {
        // This thread should not be interrupted, so it is safe to use Trys
        val pipeResult = Try(pipeVMData(vmInst))
        val vmComplete = Try(waitForVM(vmInst))

        // Store IO exception
        pipeResult recover {
          case e => ioThreadEx = e
        }

        // Chain Try's the other way: We want VM failure first, then IO failure
        promise.complete(pipeResult orElse vmComplete)
      }
    }

    def future: Future[Unit] = promise.future

    def start(logger: Logger, console: JSConsole): Future[Unit] = {
      setupLoggerAndConsole(logger, console)
      startExternalJSEnv()
      future
    }

    /** Core functionality of [[start]].
     *
     *  Same as [[start]] but without a call to [[setupLoggerAndConsole]] and
     *  not returning [[future]].
     *  Useful to be called in overrides of [[start]].
     */
    protected def startExternalJSEnv(): Unit = {
      require(vmInst == null, "start() may only be called once")
      vmInst = startVM()
      thread.start()
    }

    def stop(): Unit = {
      require(vmInst != null, "start() must have been called")
      vmInst.destroy()
    }
  }

}

object ExternalJSEnv {
  final case class NonZeroExitException(vmName: String, retVal: Int)
      extends Exception(s"$vmName exited with code $retVal")
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy