diff options
Diffstat (limited to 'main/src/main/MillServerMain.scala')
-rw-r--r-- | main/src/main/MillServerMain.scala | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/main/src/main/MillServerMain.scala b/main/src/main/MillServerMain.scala new file mode 100644 index 00000000..26ca99e6 --- /dev/null +++ b/main/src/main/MillServerMain.scala @@ -0,0 +1,227 @@ +package mill.main + +import java.io._ +import java.net.Socket + +import mill.MillMain + +import scala.collection.JavaConverters._ +import org.scalasbt.ipcsocket._ +import mill.main.client._ +import mill.eval.Evaluator +import mill.api.DummyInputStream +import sun.misc.{Signal, SignalHandler} + +trait MillServerMain[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): (Boolean, Option[T]) +} + +object MillServerMain extends mill.main.MillServerMain[Evaluator.State]{ + def main(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, + mill.main.client.Locks.files(args0(0)) + ).run() + } + def main0(args: Array[String], + stateCache: Option[Evaluator.State], + mainInteractive: Boolean, + stdin: InputStream, + stdout: PrintStream, + stderr: PrintStream, + env : Map[String, String], + setIdle: Boolean => Unit) = { + MillMain.main0( + args, + stateCache, + mainInteractive, + DummyInputStream, + stdout, + stderr, + env, + setIdle + ) + } +} + + +class Server[T](lockBase: String, + sm: MillServerMain[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 (Util.isWindows) { + val socketName = Util.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( + "MillSocketTimeoutInterruptThread", + acceptTimeout, + socketClose(), + serverSocket.accept() + ) + + sockOpt match{ + case None => running = false + case Some(sock) => + try { + handleRun(sock) + serverSocket.close() + } + catch{case e: Throwable => 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 clientMillVersion = Util.readString(argStream) + val serverMillVersion = sys.props("MILL_VERSION") + if (clientMillVersion != serverMillVersion) { + stdout.println(s"Mill version changed ($serverMillVersion -> $clientMillVersion), re-starting server") + System.exit(0) + } + val args = Util.parseArgs(argStream) + val env = Util.parseMap(argStream) + argStream.close() + + @volatile var done = false + @volatile var idle = false + val t = new Thread(() => + try { + val (result, newStateCache) = sm.main0( + args, + sm.stateCache, + interactive, + socketIn, + stdout, + stderr, + env.asScala.toMap, + idle = _ + ) + + 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 + }, + "MillServerActionRunner" + ) + 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 (Util.isWindows) { + // Closing Win32NamedPipeSocket can often take ~5s + // It seems OK to exit the client early and subsequently + // start up mill 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 e: Throwable => None} + + if (interrupted) None + else res + + } finally { + thread.interrupt() + interrupt = false + } + } +} + + |