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

sjsonnet.SjsonnetServerMain.scala Maven / Gradle / Ivy

The newest version!
package sjsonnet

import java.io._
import java.net.Socket

import scala.collection.JavaConverters._
import org.scalasbt.ipcsocket._
import sjsonnet.client.{Lock, Locks, ProxyOutputStream, Util => ClientUtil}
import sun.misc.{Signal, SignalHandler}

import scala.util.control.NonFatal

trait SjsonnetServerMain[T] {
  var stateCache = Option.empty[T]
  def main0(
      args: Array[String],
      stateCache: Option[T],
      mainInteractive: Boolean,
      stdin: InputStream,
      stdout: PrintStream,
      stderr: PrintStream,
      env: Map[String, String],
      setIdle: Boolean => Unit,
      wd: os.Path): (Boolean, Option[T])
}

object SjsonnetServerRealMain {
  def main(args0: Array[String]): Unit = {
    SjsonnetServerMain.internalMain(args0)
  }
}

object SjsonnetServerMain extends SjsonnetServerMain[DefaultParseCache] {
  def internalMain(args0: Array[String]): Unit = {
    // Disable SIGINT interrupt signal in the Mill server.
    //
    // This gets passed through from the client to server whenever the user
    // hits `Ctrl-C`, which by default kills the server, which defeats the purpose
    // of running a background server. Furthermore, the background server already
    // can detect when the Mill client goes away, which is necessary to handle
    // the case when a Mill client that did *not* spawn the server gets `CTRL-C`ed
    Signal.handle(
      new Signal("INT"),
      new SignalHandler() {
        def handle(sig: Signal) = {} // do nothing
      }
    )
    new Server(
      args0(0),
      this,
      () => System.exit(0),
      300000,
      sjsonnet.client.Locks.files(args0(0))
    ).run()
  }
  def main0(
      args: Array[String],
      stateCache: Option[DefaultParseCache],
      mainInteractive: Boolean,
      stdin: InputStream,
      stdout: PrintStream,
      stderr: PrintStream,
      env: Map[String, String],
      setIdle: Boolean => Unit,
      wd: os.Path) = {

    val stateCache2 = stateCache.getOrElse {
      val p = new DefaultParseCache
      this.stateCache = Some(p)
      p
    }
    val out = System.out
    val in = System.in
    val err = System.err
    (
      try {
        System.setIn(stdin)
        System.setErr(stderr)
        System.setOut(stdout)
        scala.Console.withIn(stdin) {
          scala.Console.withOut(stdout) {
            scala.Console.withErr(stderr) {
              sjsonnet.SjsonnetMain.main0(args, stateCache2, stdin, stdout, stderr, wd) == 0
            }
          }
        }
      } finally {
        System.setErr(err)
        System.setOut(out)
        System.setIn(in)
      },
      Some(stateCache2)
    )
  }
}

class Server[T](
    lockBase: String,
    sm: SjsonnetServerMain[T],
    interruptServer: () => Unit,
    acceptTimeout: Int,
    locks: Locks) {

  val originalStdout = System.out
  def run() = {
    Server
      .tryLockBlock(locks.processLock) {
        var running = true
        while (running) {
          Server.lockBlock(locks.serverLock) {
            val (serverSocket, socketClose) = if (ClientUtil.isWindows) {
              val socketName = ClientUtil.WIN32_PIPE_PREFIX + new File(lockBase).getName
              (
                new Win32NamedPipeServerSocket(socketName),
                () => new Win32NamedPipeSocket(socketName).close()
              )
            } else {
              val socketName = lockBase + "/io"
              new File(socketName).delete()
              (
                new UnixDomainServerSocket(socketName),
                () => new UnixDomainSocket(socketName).close()
              )
            }

            val sockOpt = Server.interruptWith(
              "SjsonnetSocketTimeoutInterruptThread",
              acceptTimeout,
              socketClose(),
              serverSocket.accept()
            )

            sockOpt match {
              case None => running = false
              case Some(sock) =>
                try {
                  handleRun(sock)
                  serverSocket.close()
                } catch { case NonFatal(e) => e.printStackTrace(originalStdout) }
            }
          }
          // Make sure you give an opportunity for the client to probe the lock
          // and realize the server has released it to signal completion
          Thread.sleep(10)
        }
      }
      .getOrElse(throw new Exception("PID already present"))
  }

  def handleRun(clientSocket: Socket) = {

    val currentOutErr = clientSocket.getOutputStream
    val stdout = new PrintStream(new ProxyOutputStream(currentOutErr, 1), true)
    val stderr = new PrintStream(new ProxyOutputStream(currentOutErr, -1), true)
    val socketIn = clientSocket.getInputStream
    val argStream = new FileInputStream(lockBase + "/run")
    val interactive = argStream.read() != 0
    val clientSjsonnetVersion = ClientUtil.readString(argStream)
    val serverSjsonnetVersion = sys.props("SJSONNET_VERSION")
    if (clientSjsonnetVersion != serverSjsonnetVersion) {
      stdout.println(
        s"Sjsonnet version changed ($serverSjsonnetVersion -> $clientSjsonnetVersion), re-starting server"
      )
      System.exit(0)
    }
    val wd = ClientUtil.readString(argStream)
    val args = ClientUtil.parseArgs(argStream)
    val env = ClientUtil.parseMap(argStream)
    argStream.close()

    @volatile var done = false
    @volatile var idle = false
    val t = new Thread(
      new Runnable {
        override def run(): Unit = {
          try {
            val (result, newStateCache) = sm.main0(
              args,
              sm.stateCache,
              interactive,
              socketIn,
              stdout,
              stderr,
              env.asScala.toMap,
              idle = _,
              os.Path(wd)
            )

            sm.stateCache = newStateCache
            java.nio.file.Files.write(
              java.nio.file.Paths.get(lockBase + "/exitCode"),
              (if (result) 0 else 1).toString.getBytes
            )
          } finally {
            done = true
            idle = true
          }
        }
      },
      "SjsonnetServerActionRunner"
    )
    t.start()
    // We cannot simply use Lock#await here, because the filesystem doesn't
    // realize the clientLock/serverLock are held by different threads in the
    // two processes and gives a spurious deadlock error
    while (!done && !locks.clientLock.probe()) Thread.sleep(3)

    if (!idle) interruptServer()

    t.interrupt()
    t.stop()

    if (ClientUtil.isWindows) {
      // Closing Win32NamedPipeSocket can often take ~5s
      // It seems OK to exit the client early and subsequently
      // start up sjsonnet client again (perhaps closing the server
      // socket helps speed up the process).
      val t = new Thread(() => clientSocket.close())
      t.setDaemon(true)
      t.start()
    } else clientSocket.close()
  }
}
object Server {
  def lockBlock[T](lock: Lock)(t: => T): T = {
    val l = lock.lock()
    try t
    finally l.release()
  }
  def tryLockBlock[T](lock: Lock)(t: => T): Option[T] = {
    lock.tryLock() match {
      case null => None
      case l =>
        try Some(t)
        finally l.release()
    }

  }
  def interruptWith[T](threadName: String, millis: Int, close: => Unit, t: => T): Option[T] = {
    @volatile var interrupt = true
    @volatile var interrupted = false
    val thread = new Thread(
      () => {
        try Thread.sleep(millis)
        catch {
          case t: InterruptedException => /* Do Nothing */
        }
        if (interrupt) {
          interrupted = true
          close
        }
      },
      threadName
    )

    thread.start()
    try {
      val res =
        try Some(t)
        catch { case NonFatal(e) => None }

      if (interrupted) None
      else res

    } finally {
      thread.interrupt()
      interrupt = false
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy