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, systemProperties: Map[String, String]): (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( lockBase = args0(0), this, () => System.exit(MillClientMain.ExitServerCodeWhenIdle()), 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, systemProperties: Map[String, String]) = { MillMain.main0( args, stateCache, mainInteractive = mainInteractive, DummyInputStream, stdout, stderr, env, setIdle = setIdle, systemProperties ) } } 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) { // FIXME: exiting with 0 isn't correct, see https://github.com/lihaoyi/mill/issues/557 stdout.println(s"Mill version changed ($serverMillVersion -> $clientMillVersion), re-starting server") java.nio.file.Files.write( java.nio.file.Paths.get(lockBase + "/exitCode"), s"${MillClientMain.ExitServerCodeWhenVersionMismatch()}".getBytes() ) System.exit(MillClientMain.ExitServerCodeWhenVersionMismatch()) } val args = Util.parseArgs(argStream) val env = Util.parseMap(argStream) val systemProperties = 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 = _, systemProperties.asScala.toMap ) 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 } } }