summaryrefslogtreecommitdiff
path: root/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala
blob: e0aa557ed44553ad6126cf42030988c6de772fe5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package 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 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 {

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

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

  protected class AbstractExtRunner(protected val classpath: CompleteClasspath,
      protected val code: VirtualJSFile, protected val logger: Logger,
      protected val console: JSConsole) {

    /** JS files used to setup VM */
    protected def initFiles(): Seq[VirtualJSFile] = Nil

    /** 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() ++ classpath.allCode

    /** 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 */
    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)
        sys.error(s"$vmName exited with code $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)

      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(classpath: CompleteClasspath, code: VirtualJSFile,
      logger: Logger, console: JSConsole
  ) extends AbstractExtRunner(classpath, code, logger, console)
       with JSRunner {

    def run(): Unit = {
      val vmInst = startVM()

      pipeVMData(vmInst)
      waitForVM(vmInst)
    }
  }

  protected class AsyncExtRunner(classpath: CompleteClasspath,
      code: VirtualJSFile, logger: Logger, console: JSConsole
  ) extends AbstractExtRunner(classpath, code, logger, console)
       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 start(): Future[Unit] = {
      require(vmInst == null, "start() may only be called once")
      vmInst = startVM()
      thread.start()
      promise.future
    }

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

    def isRunning(): Boolean = {
      require(vmInst != null, "start() must have been called")
      // Emulate JDK 8 Process.isAlive
      try {
        vmInst.exitValue()
        false
      } catch {
        case e: IllegalThreadStateException =>
          true
      }
    }

    def await(): Unit = {
      require(vmInst != null, "start() must have been called")
      thread.join()
      waitForVM(vmInst)

      // At this point, the VM itself didn't fail. We need to check if
      // anything bad happened while piping the data from the VM

      if (ioThreadEx != null)
        throw ioThreadEx
    }
  }

}